通过原型链实现继承
ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用
类型的属性和方法。
实现原型链涉及如下代码模式:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
instance(通过内部的[[Prototype]])指向 SubType.prototype,而 SubType.prototype(作为 SuperType 的实例又通过内部的[[Prototype]])指向 SuperType.prototype。
在读取实例上的属性时,首先会在实例上搜索 这个属性。如果没找到,则会继承搜索实例的原型。在通过原型链实现继承之后,搜索就可以继承向上, 搜索原型的原型。对前面的例子而言,调用 instance.getSuperValue()经过了 3 步搜索:instance、 SubType.prototype 和 SuperType.prototype,最后一步才找到这个方法
原型与继承关系
原型与实例的关系可以通过两种方式来确定。第一种方式是使用 instanceof 操作符,如果一个实 例的原型链中出现过相应的构造函数,则 instanceof 返回 true。如下例所示:
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
从技术上讲,instance 是 Object、SuperType 和 SubType 的实例,因为 instance 的原型链
中包含这些构造函数的原型。结果就是 instanceof 对所有这些构造函数都返回 true。
第二种方式是使用 isPrototypeOf()方法。原型链中的每个原型都可以调用这个 方法,如下例所示:
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
原型链上的方法
子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后
再添加到原型上,如下:
function SuperType() { this.property = true; }
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
SubType.prototype.test = function(){
console.log('test')
}
// 继承 SuperType
SubType.prototype = new SuperType();
// 新方法
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
// 覆盖已有的方法
SubType.prototype.getSuperValue = function () {
return false;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // false
instance.test()//instance.test is not a function
原型链的问题
前面分析到,通过原型模式创建的对象,当原型对象上有引用值时,会出现在所用实例间共享的情况。而我们通过原型链实现继承时,原型链上的实例属性就变成了原型属性,这个属性也会在多个实例间共享。如下面的例子:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.names = ['a','b','c']
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
instance1.names.push("d"); //SuperType实例属性变成了SubType的原型属性
let instance2 = new SubType();
console.log(instance2.colors); //['red', 'blue', 'green', 'black']
console.log(instance2.names); // ['a', 'b', 'c', 'd']
这里就发现 SuperType实例上的属性和原型对象的属性 ,都被SubType给继承了下来。且SubType的实例之间会共享引用类型的属性。
还有另外一个问题就是 子类型在实例化时不能给父类型的构造函数传参。这两个问题就导致原型链基本不会被单独使用。
盗用构造函数实现继承
为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技
术在开发社区流行起来。如下:
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承 SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
通过使用 call()方法,SuperType构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性,不会在多个实例间存在共享colors 属性。
另外一个好处是,我么可以向父类够造函数传递参数,如下:
function SuperType(name){
this.name = name;
}
function SubType() {
// 继承 SuperType 并传参
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
盗用构造函数的问题
盗用构造函数虽然能解决 引用属性之间共享和不能给父类构造函数传递参数的问题。但同样有构造函数模式创建自定义类型的问题,即 必须在构造函数中定义方法,这样就导致方法不能重用。基于这个原因,盗用构造函数基本上也不能单独使用
组合继承
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基
本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方
法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
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;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继
承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。
组合继承的问题
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次是在
创建子类原型时调用,另一次是在子类构造函数中调用。如下代码所示:
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);
}
此时就会有两组 name 和 colors 属性:一组在实例上,另一组在 SubType 的原型上。这是调用两次 SuperType 构造函数的结果。
原型式继承
在es5之前,社区通过如下的方式实现在已有的对象基础上创建一个新的对象,并且在这个对象基础上创建的多个新对象之间可以共享变量。如下:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log("anotherPerson:",anotherPerson.name);// Greg
console.log("anotherPerson:",anotherPerson.friends);//['Shelby', 'Court', 'Van', 'Rob']
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log("yetAnotherPerson:",yetAnotherPerson.name);//Linda
console.log("yetAnotherPerson:",yetAnotherPerson.friends); //['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
这种方式适用于,我们有一个对象,且不想自定义类型,就想达到在对象之间共享信息的目的。
ECMAScript 5 通过增加 Object.create()方法将这种原型式继承的概念规范化了。
这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的 object()方法效果相同:
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log("anotherPerson:",anotherPerson.name);// Greg
console.log("anotherPerson:",anotherPerson.friends);//['Shelby', 'Court', 'Van', 'Rob']
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log("yetAnotherPerson:",yetAnotherPerson.name);//Linda
console.log("yetAnotherPerson:",yetAnotherPerson.friends); //['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
寄生式组合继承
为了解决组合继承重复调用两次父类构造函数,造成子类原型上不必要的重复属性(例子中的name 和colors)。我们可以用寄生式组合继承解决这个歌问题,如下:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
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);
};
const subType1 = new SuperType('chentao',30)
subType1.colors.push('hahhaha')
const subType2 = new SuperType('刘建',30)
subType2.colors.push('hehhehhe')
//拥有它们各自的实例属性,原型链上已经没有了
console.log(subType1.colors)//['red', 'blue', 'green', 'hahhaha']
console.log(subType2.colors)//['red', 'blue', 'green', 'hehhehhe']
inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接收两个参数:子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype 对象设置constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。
这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。