ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。它的主要思想就是利用构造函数,原型和实例之间的关系。来实现一个引用类型继承另一个引用类型的属性和方法。我们知道,每个函数都有一个prototype
属性,这个属性指向该函数的原型对象。而当这个函数被当作构造函数进行实例化的时候,它的实例内部有一个[[Prototype]]
属性,这个属性指向这个构造函数的原型对象。如果将一个构造函数的原型等于另一个实例,那么这个构造函数的原型就会包含一个指向另一个原型对象的指针。这种原型和实例之间的关系,正是我们借用原型链实现继承的基本思想。
举例说明:
function SuperType() {
this.superType = 'SuperType';
}
SuperType.prototype.getSuper = function() {
return this.superType;
}
function ChildType() {
this.childType = ';childType'
}
// 继承了SuperType
ChildType.prototype = new SuperType();
let instance1 = new ChildType();
console.log(instance1.getSuper()); // 'SuperType'
上面的例子定义了两个函数SuperType
和ChildType
,而它们的主要区别是ChildType
继承了SuperType
。而这个继承的实现是通过创建SuperType的实例,并将这个实例赋值给ChildType.prototype实现的。
通过原型链继承的问题
原型链很强大,可以用它来实现继承,但是它也偶一些问题,在通过原型实现继承的时候,原型实际上会变成另一个构造函数的实例,那么原先的实例属性就会变成了原型的属性,从而这个属性变会称为共享的属性,下面这个例子会很好的讲述这个问题:
function SuperType() {
this.colors = ['red', 'green', 'blue']
}
function ChildType() {
}
ChildType.prototype = new SuperType();
let instance1 = new ChildType();
instance1.colors.push('yellow');
console.log(instance1.colors); // ["red", "green", "blue", "yellow"]
let instance2 = new ChildType();
console.log(instance2.colors); // ["red", "green", "blue", "yellow"]
在这个例子中SuperType
中定义了一个colors
属性,SuperType
的每个实例都会包含这个colors属性,当ChildType.prototype
成为SuperType
的实例的时候,ChildType.prototype
中也包含这个colors
属性,因此,当我们通过instance1.colors
进行操作的时候,引擎会顺着原型链找到ChildType.prototype
中的colors
属性,因此我们对colors
属性进行的操作,也会影响到ChildType
的其他实例。因此实践中很少会单独使用原型链进行实现继承。
借用构造函数有时也被称为伪造对象或经典继承,它的基本思想是在子类型构造函数中使用apply()
或call()
调用超类型(父类型)构造函数。举例说明:
function SuperType() {
this.colors = ['red', 'green', 'blue']
}
function ChildType() {
SuperType.call(this);
}
let instance1 = new ChildType();
instance1.colors.push('yellow');
console.log(instance1.colors); // ["red", "green", "blue", "yellow"]
let instance2 = new ChildType();
console.log(instance2.colors); // ["red", "green", "blue"]
通过使用call()
方法,我们实际上是在新创建的ChildType
实例的环境下调用了SuperType
,这样就会在新的ChildType
的实例上执行SuperType
中的初始化代码,这样每个实例都会有自己的colors
属性副本。而借助构造函数相比于借用原型链的一大优势就是,子类型构造函数可以向超类型构造函数传递参数。看下面这个例子:
function SuperType(name) {
this.name = name;
}
function ChildType(name) {
SuperType.call(this, name);
}
let instance1 = new ChildType('Nick');
console.log(instance1.name); // 'Nick'
借用构造函数的问题
如果仅仅借用构造函数,那么方法都只能在构造函数中进行定义,那么函数的复用性将不复存在,而且对于子类型构造函数来讲,定义在超类型原型上的方法对于子类型构造函数是不可见的。结果所有的类型都只能使用构造函数模式。因此借用构造函数的这个办法也很少单独使用。
组合继承有时候也叫伪经典继承,它是组合了原型链和构造函数两种方式,从而发挥二者之长的一种继承方式。基本思想是,利用原型链实现原型属性和方法的继承,从而通过构造函数来实现实例属性的继承(保证每个实例都有单独的实例属性,而不互相影响),举例说明:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green', 'yellow'];
}
SuperType.prototype.getColors = function() {
return this.colors;
}
function ChildType(name) {
// 继承属性
SuperType.call(this, name);
}
// 继承方法
ChildType.prototype = new SuperType();
let instance1 = new ChildType('Nick');
let instance2 = new ChildType('Cherry');
instance1.colors.push('black');
console.log(instance1.name); // 'Nick'
console.log(instance1.colors); // ' ["red", "green", "yellow", "black"]'
instance1.getColors(); // ' ["red", "green", "yellow", "black"]'
console.log(instance2.name); // 'Cherry'
console.log(instance2.colors); // ["red", "green", "yellow"]
组合继承避免了单独使用原型链和构造函数实现继承时的缺点,时JavaScript中最常用的一种继承方式。
原型式继承是2006年道格拉斯.克罗克福德提出的,他的基本思想是借助原型基于已有的对象创建一个新对象,同时还不必因此创建自定义类型。他给出了如下的函数:
function object (o) {
function F() {};
F.prototype = o;
return new F();
}
let person = {
name: 'Nick',
friends: ['cherry', 'july'],
};
let anotherPerson = object(person);
anotherPerson.name = 'lily';
anotherPerson.friends.push('Tom');
let person2 = object(person);
person2.name = 'Jone';
person2.friends.push('Linda');
console.log(person.friends); // ["cherry", "july", "Tom", "Linda"]
原型式继承要求你必须有一个对象作为另一个对象的基础,在这个例子中,我们把person对象作为基础,将person对象传入到object()
函数中,这样它就会返回另一个新的对象,这个新对象将person作为原型,所以它的原型中就包含一个name属性和一个friends属性。这意味着,person.friends不仅是person所有,也被anotherPerson和person2所共享。
ES5通过新增的Object.create()
方法规范化了原型式继承。这个方法接受两个参数:一个用于作为新对象原型的对象和(可选的)为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和上面的object()方法相同。
let person = {
name: 'Nick',
friends: ['cherry', 'july'],
};
let person1 = Object.create(person);
person1.name = 'Jhon';
person1.friends.push('cherry');
let person2 = Object.create(person);
person2.name = 'Lily';
person2.friends.push('Bob');
console.log(person.friends); // ["cherry", "july", "cherry", "Bob"]
Object.create()
方法的第二个参数和使用Object.defineProperties()
方法的第二个参数相同:每个属性都是通过属性描述符添加的。
let person = {
name: 'Nick',
friends: ['cherry', 'july'],
};
let anotherPerson = Object.create(person, {
name: {
value: 'Lily',
}
})
console.log(anotherPerson.name); // Lily
console.log(person.name); // Nick
在没有必要兴师动众的创建构造函数,而只是想让一个对象与另一个对象保持上面的这种关系,原型式继承是完全胜任的。不过,包含引用类型的属性始终是共享的,就像使用原型链继承一样。
寄生式继承是与原型式继承紧密相连的一种思想,它的基本思路是:创建一个仅用于封装继承过程的函数,在函数内部已某种方式来增强对象,最后在真的像它做了所有工作一样返回对象。示例:
function object(o) {`
function F() {};
F.prototype = o;
return new F();
}
function createObject(original) {
let clone = object(original);
clone.sayHi = function() {
console.log('hi');
};
return clone;
}
let person = {
name: 'Nick',
friends: ['Tom', 'Jhon'],
}
let person1 = createObject(person);
person1.sayHi(); // 'hi'
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种比较有用的模式。使用寄生式继承来为对象添加函数,会由于造成函数不能被复用而降低效率
前面说过,组合继承是JavaScript中的经典继承方式,但是它也有自己的不足。组合式继承最大的问题就是无论什么情况下都会调用两次构造函数:一次是在创建子类型原型的时候,一次是在子类型构造函数的内部。看一下下面的这个示例:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'yellow', 'blue'];
}
SuperType.prototype.sayName = function() {
console.lofg(this.name)
}
function ChildType(name, age) {
SuperType.call(this, name); // 第二次调用SuperType()
this.age = age;
}
ChildType.prototype = new SuperType(); // 第一次调用SuperType()
ChildType.prototype.constructor = ChildType;
ChildType.prototype.sayAge = function() {
console.log(this.age);
}
在第一次调用SuperType
构造函数的时候,此时ChildType.prototype
会获得两个属性:name和colors,它们都是SuperType的实例属性。只不过位于ChildType的原型中,当调用ChildType
进行实例化的时候,又会调用一次SuperType函数,这一次又在新对象上创建了name
和colors
属性。于是这两个属性就会屏蔽掉ChildType原型中的两个同名属性。因此就会造成有两组name和colors属性:一组在ChildType的原型中,一组在ChildType的实例中。这就是调用两次构造函数的结果。而解决这个问题的方法就是——寄生式组合继承。
寄生式组合继承的基本模式如下:
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function inheritPrototype(childType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = childType; // 增强对象
childType.prototype = prototype; // 指定对象
}
实例中的inheritPrototype
完成了寄生组合式继承最简单的形式。它接受两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本,第二步是为创建的副本添加constructor
属性,弥补重写原型后constructor属性的丢失。最后一步将新创建的原型副本赋值给子类型的原型。这样我们就可以调用inheritPrototype
来替换之前例子中为子类型原型赋值的那一步,例如:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'yellow', 'blue'];
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function ChildType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(ChildType, SuperType);
ChildType.prototype.sayAge = function() {
console.log(this.age);
}
let instance1 = new ChildType('cherry', 20);
instance1.sayName(); // 'cherry'
instance1.sayAge(); // 20
这个例子高效率的体现了它只调用了一次SuperType(
),并且因此避免了在ChildType原型上
创建不必要的属性。于此同时,原型链还能保持不变,可以正常使用instanceo
f和isPrototypeof()
。普遍认为寄生组合式继承是最理想的继承方式。