ES5 继承、ES6 class 继承

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType(); // 原型链继承
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true

缺点:

  1. 实例无法向父类构造函数传参
  2. 继承单一
  3. 所有新实例都会共享父类实例的属性(原型上的属性共享,一个是你修改了原型属性,另一个实例的原型属性也会被修改

借用构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {
this.name = name;
this.sum = function () {
alert(this.name);
};
}
function Con() {
// 利用call apply在函数内部将父类构造函数引入子类函数
Person.call(this, "name");
this.age = 12;
}
var con1 = new Con();
console.log(con1.name); // name
console.log(con1.age); // 12
console.log(con1 instanceof Person); // false

特点:

  1. 可以传参
  2. 可以继承多个构造函数属性

缺点:

  1. 只能继承父类构造函数的属性
  2. 无法实现构造函数的复用
  3. 每个新实例都有父类构造函数的副本

组合继承

结合了原型链和盗用构造函数的方法,将两者的优点集中了起来
基本思路是使用原型链继承原型上的属性和方法,使用盗用构造函数继承实例属性
这样既可以把方法定义在原型上以实现重用,又可以让每个实例有自己的属性

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 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); // 第一次调用SuperType
this.age = age;
}
// 继承方法
SubType.prototype = new SuperTuper("Nich", 29); // 第二次调用SuperType
SubType.prototype.sayAge = function () {
console.log(this.age);
};

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red, blue, green, black"
instance1.sayName(); // 'Nich'
instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red, blue, green"
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 27

组合继承弥补了原型链和盗用构造函数的不足,是 JS 中使用最多的集成模式
组合继承也保留了 instanceof 操作符和 isPrototypeOf()的识别合成对象的能力
组合继承的效率问题:父类构造函数始终会被调用两次,一次是在创建子类原型时调用,另一次时在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就可以了


原型式继承与 Object.create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 当Object.create()只传一个参数时,与以下效果相同
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {
name: "Nich",
friends: ["Van", "Court"],
};
let anotherPerson = object(person);
// 等同于let anotherPerson = Object.create(person)
console.log(anotherPerson.name); // Nich (通过原型链向上查找)
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // Court, Van, Rob, Barbie

Object.create()的第二个参数与 Object.defineProperties 第二个参数一样: 每个新增属性都通过各自的描述符来描述,以这种方式添加的属性会遮蔽原型上的同名属性

1
2
3
4
5
6
7
8
9
10
let person = {
name: "Nich",
friends: ["Van", "Court"],
};
let anotherPerson = Object.create(person, {
name: {
value: "Greg",
},
});
console.log(anotherPerson.name); // 'Greg'

原型式继承非常适合不需要单独创建构造函数,担任需要在对象间共享信息的场合,但是属性中包含的引用值始终会在相关对象间共享,和使用原型模式式一样的


寄生式继承

寄生式继承背后的思路类似于寄生构造函数和工厂模式:
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
此函数接收一个参数,就是新对象的基准对象
function createAnother(original) {
let clone = object(original)
clone.sayHi = function() {
console.log('hi')
}
return clone
}
let person = {
name: 'Nick',
friends: ['Bob', 'Van']
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // hi

通过寄生式继承给对象添加函数会导致函数难以重用, 与构造函数模式类似


寄生式组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
console.log(this.age);
};

寄生式组合继承避免了多次调用 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此效率更高。而且原型键保持不变,instanceof 和 isPrototypeOf()依然有效
寄生式组合继承可以算是引用类型继承的最佳模式


ES6 class 继承

基本用法:class 之间通过使用 extends 关键字完成继承,这比通过修改原型链实现继承方便得多

1
2
3
4
5
6
7
8
9
10
11
class Vehicle {}
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
// 继承普通的构造函数
function Person() {}
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true

在类构造函数中使用 super 可以调用父类的构造函数

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
class Vehicle {
constructor() {
this.hasEngune = true
}
}
class Bus extends Vehicle {
constructor() {
不要在super之前引用this,否则会抛出ReferenceError
super() // 相当于super.constructor
console.log(this instanceof Vehicle) // true
console.log(this) // Bus { hasEngine: true }
}
}
// 静态方法
class Vehicle {
static identify() {
console.log('vehicle')
}
}
class Bus extends Vehicle{
static identify() {
super.identify()
}
}
Bus.identify() // vehicle

使用 super 的时候需要注意的问题

  • 不能 super 只能在派生类构造函数和静态方法中使用
  • 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法
  • super 会调用父类构造函数,并将返回的实例赋值给 this
  • super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
1
2
3
4
5
6
7
8
9
10
11
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {
constructor(lP) {
super(lP);
}
}
console.log(new Bus("XF18")); // Bus {licensePlate: "XF018"}
  • 如果没有定义类构造函数,在实例化派生类的时候会调用 super,并且会传入所有传给派生类的参数
1
2
3
4
5
6
7
8
class Vehicle {
constructor(lp) {
this.lp = lp;
}
}
class Bus extends Vehicle {}

console.log(new Bus("XF18")); // Bus {lp: "XF18"}
  • 在类构造函数中,不能在 super()之前引用 this
  • 如果在派生类中显式地定义了构造函数,则要么必须在其中调用 super,要么必须在其中返回一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
}
}
class Van extends Vehicle {
constructor() {
return {};
}
}
console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}

抽象基类

有时候可能需要这样的一种类: 它可以供其它类继承,但是当时本事不能被实例化
虽然专门支持这种类的语法,但是可以通过 new.target 关键字进行判断实现
new.target 保存通过 new 关键字调用的类,在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error("Vehicle cannot be directly instantiated");
}
}
}
class Bus extends Vehicle {}

new Bus(); // Bus{}
new Vehicle(); // Vehicle
// Error: Vehicle cannot be directly instantiated

通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法,因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来查询相应方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vehicle {
constructor() {
if (new.target === Vehicle)
throw new Error("Vehicle cannot be directly instantiated");
if (!this.foo) throw new Error("Inheriting class must define foo()");
console.log("success");
}
}
class Bus extends Vehicle {
foo() {}
}
class Van extends Vehicle {}
new Bus(); // Bus {} success
new Van(); // Error: Inheriting class must define foo()

继承内置类型

ES6 类为继承内置应用类型提供了顺畅的机制,可以方便的扩展内置类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SuperArray extends Array {
shuffle() {
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // SuperArray(5) [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // SuperArray(5) [3, 5, 4, 2, 1]

有些内置类型的方法会返回新的实例,默认情况下返回的实例类型与原始实例的类型是一致的

1
2
3
4
5
6
7
class SuperArray extends Array {}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter((x) => !!(x % 2));
console.log(a1); // SuperArray(5) [1, 2, 3, 4, 5]
console.log(a2); // SuperArray(3) [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // true

如果要覆盖这个默认行为,可以覆盖 Symbol.species 访问器

1
2
3
4
5
6
7
8
9
10
11
class SuperArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter((x) => !!(x % 2));
console.log(a1); // SuperArray(5) [1, 2, 3, 4, 5]
console.log(a2); // SuperArray(3) [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // false

类混入

把不同类的行为集中到一个类,ES6 虽然没有显式的对类混入支持,但是现有的特性可以模仿这种行为

1
2
3
Object.assign()是为了混入对象行为而设计的
只有在需要混入类的行为时才有必要自己实现混入表达式
如果混入的只是对象的属性,那么使用Object.assign()即可

如果 Person 类需要组合 A B C,则需要某种机制实现 B 继承 A,C 继承 B,然后 Person 继承 C

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
// 可嵌套的连续继承
class Vehicle {}
let FooMixin = (SuperClass) =>
class extends SuperClass {
foo() {
console.log("foo");
}
};
let BarMixin = (SuperClass) =>
class extends SuperClass {
bar() {
console.log("bar");
}
};
let BazMixin = (SuperClass) =>
class extends SuperClass {
baz() {
console.log("baz");
}
};
// 连续继承
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

以上连续继承可以通过一个辅助函数,把嵌套展开

1
2
3
4
5
6
7
8
9
10
11
12
function mix(BaseClass, ...Mixins) {
return Mixins.reduce(
(accumulator, current) => current(accumulator),
BaseClass
);
}

class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz