继承
继承是面向对象编程中讨论最多的话题。ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。重温一下构造函数、原型和实例的关系: 如下图:
原型链继承
子类想要继承父类的属性和方法时,可以将属性和方法定义在父类构造函数的原型上,子类的原型指向父类构造函数的原型。
function Super(){
this.name = 'Jack';
this.colors = ['white', 'blue', 'green']
}
Super.prototype.sayName = function(){
console.log(this.name)
}
function Sub(){
this.name = "Sub";
}
Sub.prototype = new Super();
const s1 = new Super();
const sub1 = new Sub();
const sub2 = new Sub();
console.log("super--->",s1);
// sub1 change lastName
sub1.lastName = 'Rose'; // 不能改变父类的原始值,相当于给这个实例对象上添加属性
// sub2 change colors
sub2.colors.push('yellow'); // 修改父类的引用值类型,子类的实例都会跟着改变
console.log("sub1--->",sub1);
console.log("sub2--->",sub2);
缺点:
- 子类实例篡改父类的引用值类型会影响所有子类的实例。
- 子类在实例化时不能给父类型的构造函数传参,无法在不影响所有对象实例的情况下把参数传进父类的构造函数。
盗用构造函数
为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”,基本思路很简单:在子类
构造函数中调用父类构造函数。可以使用 apply()
和 call()
方法以新创建的对象为上下文执行构造函数。
function Super(name,age){
this.name = name;
this.age = age;
this.sayHi = function(){}
}
Super.prototype.sayName = function(){
console.log(this.name)
}
function Sub(name,age,sex){
Super.call(this,name,age);
this.sex = sex;
}
const s1 = new Super('super',18);
const sub1 = new Sub('sub1',19,'male'); // 不能访问父类原型上定义的方法,只能访问父类构造函数的方法
console.log("super--->", s1);
console.log("sub1--->", sub1);
缺点:
- 必须在父类构造函数中定义方法,因此构造函数不能重用。
- 不能访问父类原型上定义的方法。
组合继承
综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。
function Super(name, age) {
this.name = name;
this.age = age;
this.colors = ["red", "blue", "green"];
}
Super.prototype.sayName = function () {
console.log(this.name)
}
function Sub(name, age, sex) {
Super.call(this, name, age);
this.sex = sex;
}
Sub.prototype = Super.prototype;
const s1 = new Super('super', 18);
const sub1 = new Sub('sub1', 19, 'male');
const sub2 = new Sub('sub2', 20, 'female');
console.log("super--->", s1);
console.log("sub1--->", sub1);
sub1.colors.push('black')
console.log("sub2--->", sub2);
在这个例子中,Super构造函数定义了name 、age和colors属性,在Sub构造函数中调用Super构造函数,又定义了sex属性,将Sub原型指向Super的原型,继承了Super原型sayName方法。这样创建两个Sub的实例,实例上都有自己的属性,包括colors属性,同时还共享sayName方法。
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。
原型式继承
在《JavaScript 中的原型式继承》这篇文章中介绍了一种不涉及严格意义上构造函数的继承方法,出发点就是即使不自定义类型也可以通过原型实现对象之间的信息共享。最终给出了一个函数:
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
这个 object() 函数会创建一个临时构造函数(F),将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上就是 object 函数对传入的对象进行了一次浅复制。
来看下下面的例子:
let person = {
name: 'Jack',
age: 18,
color: ['red', 'green']
}
let p1 = object(person);
let p2 = object(person);
p1.name = 'Rose';
p1.color.push('blue');
p2.name = 'Linda';
p2.color.push('yellow');
console.log('p1 --->',p1);
console.log('p2 --->',p2);
这种原型式继承适用于这种情况,就是有一个对象,如果想要在它的基础上再创建一个新的对象,就可以传给上面的object函数,然后再对返回的对象进行适当的修改。
这种模式非常适合不需要单独创建构造函数,但仍然需要做到共享数据的场景。但最重要的是,属性值中包含引用值会在相关对象间共享,和原型模式一样。
寄生式继承
寄生式继承和原型式继承就比较相似了,实现原理也非常简单:创建一个实现继承的函数(这里使用上面封装好的object函数),以某种方式增加对象的属性,最后返回这个对象。
function createPerson(origin) {
let obj = object(origin); // 创建一个对象
obj.sayHello = function () { // 增加对象的属性
console.log('Hello')
}
return obj; // 返回这个对象
}
let p1 = createPerson(person);
p1.sayHello();
console.log("p1---->",p1)
在这段代码中,createPerson 这个函数接收了一个参数,就是这个新对象的基准对象(origin),这个基准对象(origin)传给object函数,然后用obj接收这个返回的新对象,接着给这个obj新对象添加一个新方法,最后返回这个新对象。
寄生式继承同样适合关注对象,而不在乎类型和构造函数的场景。
注意:object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承
上面说到的组合继承的模式其实也是存在效率问题的,最主要的问题就是父类构造函数会第调用两次,一次是创建子类原型时调用,另一次是在子类构造函数中调用。简单回顾一下:
function Super(name, age) {
this.name = name;
this.age = age;
this.colors = ["red", "blue", "green"];
}
Super.prototype.sayName = function () {
console.log(this.name)
}
function Sub(name, age, sex) {
Super.call(this, name, age); // 第二次调用
this.sex = sex;
}
Sub.prototype = new Super(); // 第一次调用
const s1 = new Super('super', 18);
const sub1 = new Sub('sub1', 19, 'male');
const sub2 = new Sub('sub2', 20, 'female');
console.log("super--->", s1);
console.log("sub1--->", sub1);
sub1.colors.push('black')
console.log("sub2--->", sub2);
可以看出 有两组属性,一组在实例上,另一组在原型上,这是调用两次 Super 方法的结果。
那就是说,不通过调用父类构造函数给子类原型赋值,取得父类原型的一个副本,就可以解决这个问题。可以通过寄生式继承来继承父类的原型,然后后将返回的新对象赋值给子类原型。
function inherit(Sub, Super) {
let obj = object(Super.prototype); // 创建对象
obj.constructor = Sub; // 增强对象
Sub.prototype = prototype; // 赋值对象
}
function Super(name, age) {
this.name = name;
this.age = age;
this.colors = ["red", "blue", "green"];
}
Super.prototype.sayName = function () {
console.log(this.name)
}
function Sub(name, age, sex) {
Super.call(this, name, age);
this.sex = sex;
}
inherit(Sub, Super);
const s1 = new Super('super', 18);
const sub1 = new Sub('sub1', 19, 'male');
console.log("super--->", s1);
console.log("sub1--->", sub1);
这个inherit函数实现了寄生式组合继承的核心逻辑。接收两个参数,子类构造函数和父类构造函数。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的 obj 对象设置 constructor 属性,解决由于重写原型导致默认 constructor 丢失的问题。最后将新创建的对象赋值给子类型的原型。
这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和isPrototypeOf方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。