前言

众所周知,JavaScript 中,没有 JAVA 等主流语言“类”的概念,更没有“父子类继承”的概念,而是通过原型对象和原型链的方式实现继承。

于是,我们这一篇讲一讲 JS 中的继承(委托)。

一、为什么要有继承?

JavaScript 是面向对象编程的语言,里面全是对象,而如果不通过继承的机制将对象联系起来,势必会造成程序代码的冗余,不方便书写。

二、为什么又是原型链继承?

好,既然是 OO 语言,那么就加继承属性吧。但是 JS 创造者并不打算引用 class,不然 JS 就是一个完整的 OOP 语言了,而创造者 JS 更容易让新手开发。

后来,JS 创造者就将 new 关键字创建对象后面不接 class,改成构造函数,又考虑到继承,于是在构造函数上加一个原型对象,最后让所有通过new 构造函数 创建出来的对象,就继承构造函函数的原型对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
function Person() {
// 构造函数
this.name = "jay";
}

Person.prototype = {
sex: "male"
};

var person1 = new Person();
console.log(person1.name); // jay
console.log(person1.sex); // male

所以,就有了 JavaScript 畸形的继承方式:原型链继承~

三、原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent() {
this.names = ["aa", "bb", "cc"];
this.age = 18;
}

function Child() {
// ...
}

Child.prototype = new Parent(); // 改变构造函数的原型对象

var child1 = new Child();

// 继承了 names 属性
console.log(child1.names); // ["aa", "bb", "cc"]
console.log(child1.age); // 18
child1.names.push("dd");
child1.age = 20;
var child2 = new Child();
console.log(child2.names); // ["aa", "bb", "cc", "dd"]
console.log(child2.age); // 18

以上例子中,暴露出原型链继承的两个问题:

  1. 包含引用类型数据的原型属性,会被所有实例共享,基本数据类型则不会。
  2. 在创建子类型实例时,无法向父类型的构造函数中传递参数。

四、call 或 apply 继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent(age) {
this.names = ["aa", "bb", "cc"];
this.age = age;
}
function Child() {
Parent.call(this, 18);
}

var child1 = new Child();

// 继承了 names 属性
console.log(child1.names); // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age); // 18

var child2 = new Child();
console.log(child2.names); // ["aa", "bb", "cc"]
console.log(child2.age); // 18

call 或 apply 的原理是在子类型的构造函数中,“借调”父类型的构造函数,最终实现子类型中拥有父类型中属性的副本了。

call 或 apply 这种继承方式在《JavaScript 高级程序设计》中叫作“借用构造函数(constructor stealing)”,解决了原型链继承中,引用数据类型被所有子实例共享的问题,也能够实现传递参数到构造函数中,但唯一的问题在于业务代码也写在了构造函数中,函数得不到复用。

五、组合继承

组合继承(combination inheritance)也叫作伪经典继承,指的是,前面两种方法:原型链继承和 call 或 apply 继承 组合起来,保证了实例都有自己的属性,同时也能够实现函数复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Parent(age) {
this.names = ["aa", "bb", "cc"];
this.age = age;
}

Parent.prototype.sayName = function() {
console.log(this.names);
};

function Child() {
Parent.call(this, 18); // 第一次调用
}

Child.prototype = new Parent(); // 第二次调用:通过原型链继承 sayName 方法
Child.prototype.constructor = Child; // 改变 constructor 为子类型构造函数

var child1 = new Child();
child1.sayName(); // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age); // 18

var child2 = new Child();
console.log(child2.names); // ["aa", "bb", "cc"]
console.log(child2.age);
child2.sayName(); // ["aa", "bb", "cc"]

组合继承将继承分为两步,一次是创建子类型关联父类型原型对象的时候,另一次是在子类型构造函数的内部。是 JS 最常用的继承方式。

六、原型式继承

原型式继承说白了,就是将父类型作为一个对象,直接变成子类型的原型对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

var parent = {
age: 18,
names: ["aa", "bb", "cc"]
};

var child1 = object(parent);

// 继承了 names 属性
console.log(child1.names); // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age); // 18

var child2 = object(parent);
console.log(child2.names); // ["aa", "bb", "cc", "dd"]
console.log(child2.age); // 18

原型式继承其实就是对原型链继承的一种封装,它要求你有一个已有的对象作为基础,但是原型式继承也有共享父类引用属性,无法传递参数的缺点。

这个方法后来有了正式的 API: Object.create({...})

所以当有一个对象,想让子实例继承的时候,可以直接用 Object.create() 方法。

七、寄生式继承

寄生式继承是把原型式 + 工厂模式结合起来,目的是为了封装创建的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createAnother(original) {
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function() {
//以某种方式来增强这个对象
console.log("hi");
};
return clone; //返回这个对象
}

var person = {
age: 18,
names: ["aa", "bb", "cc"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

八、 寄生组合式继承

刚才说到组合继承有一个会两次调用父类的构造函数造成浪费的缺点,寄生组合继承就可以解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); // 创建了父类原型的浅复制
prototype.constructor = subType; // 修正原型的构造函数
subType.prototype = prototype; // 将子类的原型替换为这个原型
}

function SuperType(age) {
this.age = age;
this.names = ["aa", "bb", "cc"];
}

SuperType.prototype.sayName = function() {
console.log(this.names);
};

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

SubType.prototype.sayAge = function() {
console.log(this.age);
};

var child1 = new SubType(22);
child1.sayAge(); // 22
child1.sayName(); // ["aa", "bb", "cc"]

九、ES6 class extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Parent {
constructor(name) {
this.name = name;
}
doSomething() {
console.log("parent do something!");
}
sayName() {
console.log("parent name:", this.name);
}
}

class Child extends Parent {
constructor(name, parentName) {
super(parentName);
this.name = name;
}
sayName() {
console.log("child name:", this.name);
}
}

const child = new Child("son", "father");
child.sayName(); // child name: son
child.doSomething(); // parent do something!

const parent = new Parent("father");
parent.sayName(); // parent name: father

ES6 的 class extends 本质上是 ES5 的语法糖。
ES6 实现继承的具体原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent {}

class Child {}

Object.setPrototypeOf = function(obj, proto) {
obj.__proto__ = proto;
return obj;
};

// B 的实例继承 A 的实例
Object.setPrototypeOf(Child.prototype, parent.prototype);

// B 继承 A 的静态属性
Object.setPrototypeOf(Child, Parent);

总结

javascript 由于历史发展原因,继承方式实际上是通过原型链属性查找的方式,但正规的叫法不叫继承而叫“委托”,ES6 的 class extends 关键字也不过是 ES5 的语法糖。所以,了解 JS 的原型和原型链非常重要,详情请翻看我之前的文章《JavaScript 原型与原型链》

参考:
《JavaScript 高级程序设计》

2019/02/10 @Starbucks