继承

1 原型链继承#

1.1 原理及实现#

将父类的实例作为子类的原型

function Animal() { }
function Cat() { }
Cat.prototype = new Animal(); // 子类的原型对象指向父类的实例对象

  上述代码中,将子类 Cat 默认提供的原型替换为父类 Animal 的实例。于是,Cat 的新原型不仅具有作为一个 Animal 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了父类 Animal 的原型。

1.2 原型关系图#

note

TODO:图画的不对,待修正

console.log(Cat.prototype.__proto__ === Animal.prototype); // true
const cat = new Cat();
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true
console.log(cat instanceof Object); // true

1.3 优缺点#

优点:

  • 父类方法可以被复用

缺点:

  • 父类的包含引用类型值的原型属性会被所有实例共享
  • 子类构建实例时不能向父类传递参数

2 构造函数继承#

2.1 原理及实现#

子类构造函数的内部调用父类构造函数

function Animal() { }
function Cat() {
// 继承了 Animal
Animal.call(this);
}

  上述代码中,通过使用 call 方法,在(未来将要)新创建的 Cat 实例的环境下调用 Animal 构造函数。因此,会在新 Cat 对象上执行 Animal() 函数中定义的所有对象初始化代码。最终,Cat 的每个实例都会具有 Animal 相关属性的副本(避免了引用类型属性的共享问题)。

2.2 传递参数#

相比原型链继承,借用构造函数可以子类构造函数中向父类构造函数传递参数。

function Animal() {
this.name = 'Animal';
}
function Cat(name) {
// 继承了 Animal
Animal.call(this);
this.name = name || 'Tom';
}
const a = new Animal(); // Animal {name: "Animal"}
const c1 = new Cat(); // Cat {name: "Tom"}
const c2 = new Cat('Garfield'); // {name: "Garfield"}

2.3 优缺点#

构造函数继承的优缺点与原型链继承的优缺点完全相反。

优点:

  • 父类的包含引用类型值的原型属性不会被所有实例共享
  • 子类构建实例时能向父类传递参数

缺点:

  • 父类方法不可以被复用(只能继承父类实例的属性和方法,不能继承父类原型的属性和方法)

3 组合继承#

3.1 原理及实现#

原型链继承与构造函数继承的组合,兼具二者优点。

function Animal() { }
function Cat() {
// 继承 Animal 属性
Animal.call(this);
}
// 继承 Animal 方法
Cat.prototype = new Animal();

  上述代码中,利用构造函数继承父类的相关属性,并利用原型链继承父类的相关方法,从而避免了原型链继承和构造函数继承的缺点,融合了它们的优点。

3.2 举个例子#

function Animal() {
this.name = 'Animal';
this.actions = ['eat'];
}
Animal.prototype.getDescription = function () {
return this.name + ': ' +this.actions;
}
function Cat(name) {
// 继承 Animal 属性
Animal.call(this);
this.name = name || 'Cat';
}
// 继承 Animal 方法
Cat.prototype = new Animal();
const a = new Animal(); // Animal {name: "Animal"}
const c1 = new Cat(); // Cat {name: "Tom"}
const c2 = new Cat('Garfield'); // {name: "Garfield"}
c1.actions.push('run');
a.getDescription(); // "Animal: eat"
c1.getDescription(); // "Cat: eat,run"
c2.getDescription(); // "Garfield: eat"

3.3 优缺点#

优点:

  • 父类的方法可以被复用
  • 父类的包含引用类型值的原型属性不会被所有实例共享
  • 子类构建实例时可以向父类传递参数

缺点:

  • 调用了两次父类构造函数,造成性能上的浪费

4 原型式继承#

4.1 原理及实现#

对传入的参数对象进行一次浅复制。

function object(o){
function F(){}
F.prototype = o;
return new F();
}

  上述代码中,在 object 函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); // "Shelby,Court,Van,Rob,Barbie"

  原型式继承要求必须有一个对象可以作为另一个对象的基础。如果有此对象,则将其传递给 object 函数,然后再根据具体需求对得到的对象加以修改即可。例子中,对象 person 作为 anotherPersonyetAnotherPerson 的原型,而其属性 person.friends 是一个引用类型值属性,故其不仅属于 person 所有,而且也会被 anotherPersonyetAnotherPerson 所共享。

  ECMAScript 5 通过新增 Object.create() 方法规范了原型式继承。该方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。

4.2 优缺点#

优点:

  • 父类的方法可以被复用

缺点:

  • 父类的包含引用类型值的原型属性会被所有实例共享
  • 子类构建实例时不能向父类传递参数

5 寄生式继承#

使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制对象的能力。

function createAnother(original) {
var clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式来增强这个对象
alert("hi");
};
return clone; // 返回这个对象
}

  上述代码中,createAnother() 函数接收了一个新参数,作为新对象基础的对象。然后,通过 obejct() 函数对其进行浅复制,再对浅复制对象(clone)添加一个新方法 sayHi(),最后返回增强后的 clone 对象。

var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
note

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类型。

6 寄生组合式继承#

6.1 原理与实现#

使用寄生式继承来继承父类的原型,然后再将结果指定给子类的原型。

function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); // 创建了父类原型的浅复制
prototype.constructor = subType; // 修正原型的构造函数
subType.prototype = prototype; // 将子类的原型替换为这个原型
}

  上述代码中,实现了寄生组合式继承的最简单形式。在函数 inheritPrototype() 内部,第一步创建父类原型的一个副本,第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性,第三步是将新创建的对象(即副本)赋值给子类的原型。

function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
}

  上例中,只调用了一次 SuperType 构造函数,因此避免了在 SubType.prototype 上创建不必要的、多余的属性。与此同时,原型链还能保持不变,因此还能够正常使用 instanceofisPrototypeOf()

7 ES6 Class extends#

7.1 原理与实现#

ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

7.1.1 使用方法#

class A {}
class B extends A {
constructor() {
super();
}
}

7.1.2 实现原理#

class A {
}
class B {
}
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
note

本质上 ES6 继承是 ES5 继承的语法糖。

总结#

  JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构建函数的原型实现的。原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。

  解决这个问题的技术是借用构造函数,即在子类构造函数的内部调用父类构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。

  使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

  此外,还存在下列可供选择的继承模式:

  • 原型式继承:可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。

  • 寄生式继承:与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用父类构造函数而导致的低效率问题,可以将这个模式与组合模式一起使用。

  • 寄生组合式继承:集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。