Appearance
继承(Inheritance)
认识构造函数、原型对象和实例
每个构造函数都有一个原型对象,这个原型对象中又存在一个 constructor
属性,值为该构造函数。
每个实例对象都有一个指向该原型对象的指针 [[protoType]]
(在许多浏览器实现中, 可以使用 __proto__
非标准属性来访问)。
TIP
通过 Object.getPrototypeOf(instance)
可以获取到原型对象 [[prototype]]
。
以原型链的方式实现继承
javascript
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
this.subproperty = false;
}
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
SubType.prototype = new SuperType();
var instance = new SubType();
console.log(instance.getSuperValue()); // true
instance
实例由 SubType
构造函数创建,它的原型对象指向了 SuperType
原型对象, instance
实例通过原型链可以访问父类的方法和属性。
值得注意的是,SubType
构造函数中的 this.property = true;
语句执行后, property
属性被添加到了 SubType
原型对象中,而不是 instance
对象本身中。
由于 constructor
属性存在于原型对象上, 而 SubType
的原型对象被重写为 SuperType
的实例, 且 instance
又是 SubType
的实例, 因此 instance
对象的 [[prototype]]
中的 constructor
指向了 SuperType
小结
- 子类的原型对象
prototype
被重写为父类构造函数的实例 - 子类实例
[[prototype]]
中的constructor
指向父类的constructor
- 父类原型方法仍然存在父类的原型对象中,但是其构造函数中为
this
赋值的属性却存在于子类的原型对象中。
练习
javascript
Object.getPrototypeOf(Object.prototype) === null; // true
// Object 也是一个函数
// Object 函数签名: Object([value])
// Object.length === 1, 函数的 length 属性始终为函数签名中的参数个数
Object.getPrototypeOf(Object) === Function.prototype; // true
默认原型
在默认情况下,任何函数的原型对象都是一个 Object 的实例。
确定原型和实例的关系
通过 instanceof 关键字
如果 instance
的原型链中包含 SomeFn
构造函数的原型,则返回 true。
语法
javascript
instance instanceof Somefn;
代码示例
javascript
instance instanceof Object; // true
instance instanceof SuperType; // true
instance instanceof SubType; // true
代码实现
javascript
/**
* 将实例的 [[prototype]] 和构造函数的 prototype 进行比较,直至原型链的末尾。
* 可以看到不需要访问原型对象中的 constructor 属性。
* @param left 实例对象
* @param right 构造函数
*/
function MyInstanceOf(left, right) {
let prototype = right.prototype;
while (left !== null) {
if (left === prototype) return true;
left = Object.getPrototypeOf(left);
}
return false;
}
通过 isPrototypeOf 方法
测试一个对象是否存在于某个构造函数的原型链上。
语法
javascript
SomeFn.prototype.isPrototypeOf(instance);
代码示例
javascript
Object.prototype.isPrototypeOf(instance); // true
SuperType.prototype.isPrototypeOf(instance); // true
SubType.prototype.isPrototypeOf(instance); // true
代码实现
javascript
function isPrototypeOf(proto, object) {
while (object !== null) {
if (object === proto) return true;
object = Object.getPrototypeOf(object);
}
return false;
}
// 测试一下
function Person() {}
const person = new Person();
isPrototypeOf(Object.prototype, person); // true
isPrototypeOf(Person.prototype, person); // true
isPrototypeOf(Array.prototype, person); // false
重写父类方法
在 SubType.prototype = new SuperType()
之后,添加对应的同名方法以覆盖方法, 原理是基于 JavaScript 的委托机制。
使用字面量赋值给原型对象
如果使用一个字面量对象赋值给原型对象,那么原有的原型赋值会无效。
javascript
SubType.prototype = new SuperType();
// 使用字面量会导致上面的原型赋值无效,可以理解为[[prototype]]指针指向了另一个对象。
SubType.prototype = {
// ...
};
原型链继承附带字面量对象
javascript
/**
* 一个简易的实现,保持继承父类,并且添加对象字面量中的属性。
* 对象字面量中的属性会覆盖父类原型对象中的同名属性
* @param subtype 子类构造函数
* @param superConstrucotr 父类构造函数
* @param object 对象字面量
*/
function MyExtends(subtype, superConstrucotr, object) {
subtype.prototype = new superConstrucotr();
for (key in object) {
subtype.prototype[key] = object[key];
}
}
原型链继承的缺点
- 原型链继承会导致原型链中的引用类型的值会被所有实例共享
- 子类在实例化时无法为父类构造函数传入参数,相当于只能使用无参构造
共享引用类型
在原型链中存在的引用类型的值会被所有实例共享,使用上面的原型链继承时,子类的原型实际上变成了父类构造函数的一个实例。
javascript
function SuperType() {
this.colors = ['red', 'green', 'pink', 'black'];
}
function SubType() {}
SubType.prototype = new SuperType();
const instance1 = new SubType();
instance1.colors.push('blue');
console.log(instance1.colors); // "red,green,pink,black,blue"
const instance2 = new SubType();
console.log(instance2.colors); // "red,green,pink,black,blue"
构造函数传参
我们可以发现,在基于原型链继承时,子类在实例化时无法为父类的构造函数传入参数。
经典继承
又叫盗用构造函数、对象伪装,在子类构造函数中调用父类的构造函数,通过 call
或者 apply
。
javascript
function SuperType() {
this.colors = ['red', 'green', 'pink', 'black'];
}
function SubType() {
SuperType.call(this);
}
const instance1 = new SubType();
instance1.colors.push('blue');
console.log(instance1.colors.toString()); // "red,green,pink,black,blue"
const instance2 = new SubType();
console.log(instance2.colors.toString()); // "red,green,pink,black"
优缺点
优点:
- 子类实例化时可以为父类构造函数传递参数
- 父类引用类型的属性不再被所有实例共享
- 可以实现多继承,使用
call
去调用多个父类构造函数
缺点:
- 子类实例无法访问父类原型中定义的方法
- 生成的实例仅仅是子类的实例,而不是父类的实例,
instance1 instanceof SuperType
结果为false
- 父类如果想要定义子类能够访问的方法,只能在父类构造函数的内部去定义方法,因此每个实例都有一个方法的副本,导致函数不能被复用,造成了额外的开销
组合继承
又叫伪经典继承。使用原型链继承原型上的属性和方法,使用盗用构造函数继承实例属性。
javascript
function SuperType(name) {
// 值类型的属性
this.name = name;
// 引用类型的属性
this.colors = ['red', 'blue'];
}
// 在父类原型对象上定义方法
SuperType.prototype.getName = function () {
console.log(this.name);
};
function SubType(name, age) {
// 调用一次父类构造函数,并传递了参数
SuperType.call(this, name);
this.age = age;
}
// 再次调用了父类构造函数
SubType.prototype = new SuperType();
优缺点
优点:
- 子类实例的属性是独立的,引用类型的属性不在所有实例间共享
- 子类实例可以访问父类原型上定义的方法
- 子类实例可以正确的被
instanceof
和isPrototypeOf
识别 - 可以为父类构造函数传递参数
缺点:
- 父类构造函数被调用了两次
- 父类构造函数中为
this
添加的属性不仅存在于新实例中,还存在于子类原型对象上
原型式继承
来源于 Douglas Crockford 于 2006 年写的文章《Prototypal Inheritance in JavaScript》,文中最终给出了一个函数:
javascript
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
示例
javascript
// 没有创建和使用构造函数,但是在多个实例之间共享了信息
const person = {
name: 'Joh',
friends: ['Bob', 'Alice'],
};
const anotherPerson = Object.create(person);
anotherPerson.name = 'Grace';
anotherPerson.friends.push('Alex');
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Van');
// 引用类型会被所有实例所共享
console.log(person.friends.toString()); // "Bob,Alice,Alex,Van"
使用场景
基于一个已有对象创建一个新对象。将已有对象传递给 object
函数,然后基于返回的对象进行修改。
原型式继承适合不需要单独创建构造函数,但是需要在对象间共享信息的场合使用,引用类型会被所有实例共享。
TIP
在 ES6 中,使用 Object.create
传递单个参数和这里的 object
函数效果是一样的。
寄生式继承
创建一个实现继承的函数,在函数内部增强对象,并且返回这个对象
javascript
// 一个可以生成对象的函数
function createAnother(original) {
// 基于 original 创建新对象
const clone = Object.create(original);
// 增强新对象
clone.sayHello = function () {
console.log('hello');
};
// 返回新对象
return clone;
}
使用场景
寄生式继承适合只关注对象,而不在乎类型和构造函数的场景。
寄生组合式继承
上文中的组合继承算是比较好的继承方式,但有着父类构造函数被调用两次的缺点,寄生组合式继承算是对其的完善。
基本思路是,不通过父类构造函数给子类原型赋值,而是取得父类原型的一个副本。即使用寄生式继承来继承父类原型,然后将其返回的新对象赋值给子类原型。
javascript
// 寄生式继承来继承父类原型
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype); // 创建对象
// 重写 constructor 来进行修正,一个对象的原型对象上的 constructor 应该指向其构造函数
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
javascript
// 完整的实例代码
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(SuperType, SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
};
解决的问题
- 只调用一次父类构造函数
- 子类原型对象不会添加额外用不上的属性
- 原型链仍然可以使用,即定义在原型链上的方法可以被实例对象所共享
instanceof
和isPrototypeOf
可以获得正确的结果- 解决了原型链继承时,
[[prototype]]
中的constrctor
指向父类构造函数的问题
参考资料
- JavaScript 高级程序设计
- MDN