js 中的继承
(一)原型链继承
-
原理: 通过修改构造函数的原型指针指向一个类的实例
-
不足:不能给父类构造函数传递参数,父类中的引用类型属性共享
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function () { return this.property; }; function SubType() { this.subproperty = false; } // 继承 SuperType SubType.prototype = new SuperType(); // 原型变成了SuperType的实例 // 相当于给SuperType实例添加一个方法 SubType.prototype.getSubValue = function () { return this.subproperty; } let instance = new SubType(); console.log(instance);
分析:subType 的原型指向了 superType 实例
(二)盗用构造函数继承
- 原理:通过在一个类的构造函数中调用目标类的构造函数并传递this为当前类的this
- 特点:可以向目标类的构造函数传参,且父类的属性不会共享,但是不能访问目标类的原型,只能获得目标类的构造函数中定义的属性,并保存到当前类的实例中
// 盗用构造函数
function SuperType(name) {
this.name = name;
this.sayName = function(){
console.log(name);
}
}
// 原型上
SuperType.prototype.foo = function(){
console.log('foo');
}
function SubType() {
// 继承 SuperType 并传参
SuperType.call(this, "Nicholas");
// SuperType实例上的属性和方法会复制到本实例中,但是原型上的不会
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance);
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
分析:subType 盗用了 superType 的构造函数,把该对象本身的属性都复制了下来,并保存了自身实例当中,而 superType 原型的 foo 没有被"盗用" (复制)下来
(三)原型盗用组合继承
-
原理:结合1,2的使用
-
特点:既可以传参,又可以访问父类的原型,但是父类会创建两个实例
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function () { console.log(this.name); }; function SubType(name, age) { SuperType.call(this, name); // 第二次调用 SuperType() this.age = age; } SubType.prototype = new SuperType(); // 第一次调用 SuperType() SubType.prototype.constructor = SubType;// 不写是没有这个属性的 SubType.prototype.sayAge = function () { console.log(this.age); }; let instance = new SubType('abc',29); console.log(instance);
分析:盗用构造函数可以在创建原型实例时可以传递参数,但是为了弥补不能盗用原型的原型属性和方法,又适用创建 superType 实例设置 prototype 的方法,虽然现在弥补上述的不足,但是又会多创建原型自身的属性,浪费的内存空间。
(四)原型式继承
- 原理:还是使用原型进行继承,只是把这个过程封装成一个函数,返回一个自定义对象
- 特点:和原型链继承一样
function subType(superObj) {
function F() { }
F.prototype = superObj;
return new F();
}
function superType(name, friends) {
this.name= name,
this.friends= friends
};
let subInstance = subType(superType("Nicholas",["Shelby", "Court", "Van"])); // 原型传参实例化
subInstance.age = 18; // 目标对象,手动添加属性
console.log(subInstance);
分析:与方法一相比,好处是,可以传入一个定义实例作为原型(原型可以传参实例化),但是鱼与熊掌不可兼得,现在目标对象,又不能传参一次性创建实例了,只是手动添加实例。
(五)寄生式继承
- 原理:在原型式的基础上,在函数中为自定义对象添加新的属性和方法
- 特点:和原型链继承一样
function subType(superObj) {
function F() { }
F.prototype = superObj;
return new F();
}
function superType(name, friends) {
this.name= name,
this.friends= friends
};
// 传入了原型,实例的属性和方法
function createSubType(superObj, {age, sayHi}){
let sub = subType(superObj);
sub.age = age;
sub.sayHi = sayHi;
return sub;
}
let subInstance = createSubType(new superType("Nicholas",["Shelby", "Court", "Van"]), {age:18, sayHi(){
console.log('hi, I am ' + this.age + " years old.");
}});
subInstance.sayHi();
分析:其实就是在方法四的基础上,又封装了一层,把原来创建实例后的手动写法变成了传入一个对象参数给 createSubType
工厂函数来创建而已,反而变得越来越绕
(六)寄生式组合继承
- 原理:结合了寄生式和盗用构造函数的原理
- 特点:在类出现前最优的继承方式,可以传参给父类构造函数,可以访问父类的原型,且只创建一个父类实例
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
// 返回 subType -> object -> supertype.prototype 的三层原型链
function inheritPrototype(subType, superType) {
//相当与创建superType实例,但是是间接创建, 该实例上没有superType实例应有的属性,但是有原型上的
let prototype = object(superType.prototype); // 创建对象, 将 superType的原型作为目标实例原型
prototype.constructor = subType; // 增强对象,
subType.prototype = prototype; // 赋值对象,subtype 原型设置为 object() 返回的对象
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name); //盗用构造函数
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
};
let instance = new SubType('kobe', 29);
分析:这个方法结合 ①盗用构造函数复制supertype本身属性到 subtype,②寄生式继承创建supertype.prototype为原型的中间实例,③将中间实例设置为 subtype.prototype 的方法形成了一条 subType -> object -> supertype.prototype
的三层原型链,从而克服盗用构造函数不能复制原型的原型属性的弊端,这个方法比较完美,但是会比较复杂。
(七)class 关键字 继承语法糖
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return `(${this.x}, ${this.y})`
}
}
class ColorPoint extends Point{
constructor(x,y,color){
super(x,y); // 调用 父类构造方法
this.color = color;
}
toString(){
return this.color + ' ' + super.toString(); // 调用父类的 toString 方法
}
}
const colorPoint = new ColorPoint(2,3,'red');
console.log(colorPoint);
分析:class 关键字的继承方式使用 extends
关键字,子类的构造函数的 super()
方法,是调用父类的构造函数,并且把父类自身属性复制到子类实例中,这特性与 盗用构造函数 call()
方法相同,当父类的方法与子类的相同时,要想调用 父类的,可以使用 super.方法名
来调用,super
指向的是父类的原型对象,具体 class的继承方式请看 阮一峰老师ES6的class继承文章