# 0.前言

继承的使用场景

  • 不要仅仅为了使用而使用它们,这只是在浪费时间而已。
  • 当需要创建 一系列拥有相似特性的对象 时,那么创建一个包含所有共有功能的通用对象,然后在更特殊的对象类型中继承这些特性。
  • 应避免多继承,造成混乱。
  • 注: 考虑到JavaScript的工作方式,由于原型链等特性的存在,在不同对象之间功能的共享通常被叫做委托 - 特殊的对象将功能委托给通用的对象类型完成。这也许比将其称之为继承更为贴切,因为“被继承”了的功能并没有被拷贝到正在“进行继承”的对象中,相反它仍存在于通用的对象中。

# 一. 原型链继承

  1. 特点:利用原型,让一个引用类型继承另一个引用类型的属性及方法

  2. 优点:继承了父类的模板,又继承了父类的原型对象

  3. 缺点:

  • 创建子类实例时,不能向父类的构造函数中传递参数
  • 可以在子类构造函数中,为子类实例增加实例属性。如果要新增原型属性和方法,则必须放在 SubType.prototype = new SuperType('SubType'); 这样的语句之后执行。
  • 无法实现多继承
  • 来自原型对象的所有属性被所有实例共享

例1

function Parent() {
  this.name = "parent";
  this.hobby = ["sing", "rap"];
}
function Child() {
  this.type = "child";
}

Child.prototype = new Parent();

let child1 = new Child();
let child2 = new Child();
child1.hobby.push("basketball");
console.log(child1.hobby); // [ 'sing', 'rap', 'basketball' ]
console.log(child2.hobby); // [ 'sing', 'rap', 'basketball' ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

例2

// 父类
function SuperType () {
  this.name = 'SuperType'; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
  return this.name;
};

// 子类
function SubType () {
  this.subName = "SubType"; // 子类属性
};

SubType.prototype = new SuperType(); // 重写原型对象,代之以一个新类型的实例
// 这里实例化一个 SuperType 时, 实际上执行了两步
// 1,新创建的对象复制了父类构造函数内的所有属性及方法
// 2,并将原型 __proto__ 指向了父类的原型对象

SubType.prototype.saySubName = function () { // 子类原型方法
  return this.subName;
}

// 子类实例
let instance = new SubType();

// instanceof 通过判断对象的 prototype 链来确定对象是否是某个类的实例
instance instanceof SubType; // true
instance instanceof SuperType; // true

// 注意
SubType instanceof SuperType; // false
SubType.prototype instanceof SuperType ; // true
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
30
31
32

例3

// 父类
function SuperType () {
  this.colors = ["red", "blue", "green"];
  this.name = "SuperType";
}
// 子类
function SubType () {}

// 原型链继承
SubType.prototype = new SuperType();

// 实例1
var instance1 = new SubType();
instance1.colors.push("blcak");
instance1.name = "change-super-type-name";
console.log(instance1.colors); // ["red", "blue", "green", "blcak"]
console.log(instance1.name); // change-super-type-name
// 实例2
var instance2 = new SubType();
console.log(instance2.colors); // ["red", "blue", "green", "blcak"]
console.log(instance2.name); // SuperType
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2. 借用构造函数继承(经典继承)

  1. 基本思想:在子类型的构造函数内部调用父类型构造函数。

注意:函数只不过是在特定环境中执行代码的对象,所以这里使用 apply/call 来实现。

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

  1. 优点:
  • 在子类构造函数中可以向父类的构造函数中传递参数
  • 避免了父类中的引用类型属性在子类中共享的问题
  1. 缺点:
  • 父类原型对象上的方法子类继承不到
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
  • 实例并不是父类的实例,只是子类的实例

例1

function Parent(age) {
  this.name = "parent";
  this.age = age;
  this.hobby = ["sing", "rap"];
}
Parent.prototype.sayHi = function() {
  console.log("Hi");
};
function Child(age) {
  Parent.call(this, age);
  this.type = "child";
}
let child1 = new Child(15);
let child2 = new Child(15);

child1.hobby.push("basketball");
console.log(child1.name); // parent
console.log(child1.age); // 15
console.log(child1.type); // child
console.log(child1.hobby); // [ 'sing', 'rap', 'basketball' ]
console.log(child2.hobby); // [ 'sing', 'rap' ]
child1.sayHi(); // 报错,child1.sayHi is not a function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

例2

// 父类
function SuperType (name) {
  this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
  return this.name;
};

// 子类
function SubType () {
  // 调用 SuperType 构造函数
  SuperType.call(this, 'SuperType'); // 在子类构造函数中,向父类构造函数传参
  // 为了保证子父类的构造函数不会重写子类的属性,需要在调用父类构造函数后,定义子类的属性
  this.subName = "SubType"; // 子类属性
};
// 子类实例
let instance = new SubType(); // 运行子类构造函数,并在子类构造函数中运行父类构造函数,this绑定到子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 三. 组合继承

顾名思义,组合继承就是将原型链继承与构造函数继承组合在一起,从而发挥两者之长的一种继承模式。

  1. 基本思想:使用原型链继承使用对原型属性和方法的继承,通过构造函数继承来实现对实例属性的继承。这样既能通过在原型上定义方法实现函数复用,又能保证每个实例都有自己的属性。

通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

  1. 优点:
  • 在子类构造函数中可以向父类的构造函数中传递参数
  • 避免了父类中的引用类型属性在子类中共享的问题
  • 父类原型对象上的方法子类也可以继承到
  1. 缺点:
  • 父类构造函数被调用了两次

例1

function Parent(age) {
  this.name = "parent";
  this.age = age;
  this.hobby = ["sing", "rap"];
}
Parent.prototype.sayHi = function() {
  console.log("Hi");
};
function Child(age) {
  Parent.call(this, age); // 第二次调用Parent
  this.type = "child";
}

Child.prototype = new Parent(); // 第一次调用Parent
Child.prototype.constructor = Child;

let child1 = new Child(15);
let child2 = new Child(15);

child1.hobby.push("basketball");
console.log(child1.name); // parent
console.log(child1.age); // 15
console.log(child1.type); // child
console.log(child1.hobby); // [ 'sing', 'rap', 'basketball' ]
console.log(child2.hobby); // [ 'sing', 'rap' ]
child1.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

例2

// 父类
function SuperType (name) {
  this.colors = ["red", "blue", "green"];
  this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
  return this.name;
};

// 子类
function SubType (name, subName) {
  // 调用 SuperType 构造函数
  SuperType.call(this, name); // ----第二次调用 SuperType----
  this.subName = subName;
};

// ----第一次调用 SuperType----
SubType.prototype = new SuperType(); // 重写原型对象,代之以一个新类型的实例

SubType.prototype.constructor = SubType; // 组合继承需要修复构造函数指向
SubType.prototype.saySubName = function () { // 子类原型方法
  return this.subName;
}

// 子类实例
let instance = new SubType('An', 'sisterAn')
instance.colors.push('black')
console.log(instance.colors) // ["red", "blue", "green", "black"]
instance.sayName() // An
instance.saySubName() // sisterAn

let instance1 = new SubType('An1', 'sisterAn1')
console.log(instance1.colors) //  ["red", "blue", "green"]
instance1.sayName() // An1
instance1.saySubName() // sisterAn1
// instanceof:instance 的原型链是针对 SuperType.prototype 进行检查的
instance instanceof SuperType // true
instance instanceof SubType // true

// isPrototypeOf:instance 的原型链是针对 SuperType 本身进行检查的
SuperType.prototype.isPrototypeOf(instance) // true
SubType.prototype.isPrototypeOf(instance) // true
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
30
31
32
33
34
35
36
37
38
39
40
41
42

# 四. 组合式继承优化 1

  1. 优点:
  • 在子类构造函数中可以向父类的构造函数中传递参数
  • 避免了父类中的引用类型属性在子类中共享的问题
  • 父类原型对象上的方法子类也可以继承到
  • 父类构造函数也只调用了一次
  1. 缺点:
  • 向子类原型上增加属性或方法时会影响到父类原型
function Parent(age) {
  this.name = "parent";
  this.age = age;
  this.hobby = ["sing", "rap"];
}
Parent.prototype.sayHi = function() {
  console.log("Hi");
};
function Child(age) {
  Parent.call(this, age);
  this.type = "child";
}

Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;
// 向子类原型上增加testProp属性,同时会被添加到父类原型上
Child.prototype.testProp = 1;
console.log(Parent.prototype); // 1

let child1 = new Child(15);
let child2 = new Child(15);

child1.hobby.push("basketball");
console.log(child1.name); // parent
console.log(child1.age); // 15
console.log(child1.type); // child
console.log(child1.hobby); // [ 'sing', 'rap', 'basketball' ]
console.log(child2.hobby); // [ 'sing', 'rap' ]
child1.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

# 五. 组合式继承优化 2

优点:

  • 实现继承最有效的方式!

缺点:

function Parent(age) {
  this.name = "parent";
  this.age = age;
  this.hobby = ["sing", "rap"];
}
Parent.prototype.sayHi = function() {
  console.log("Hi");
};
function Child(age) {
  Parent.call(this, age);
  this.type = "child";
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

let child1 = new Child(15);
let child2 = new Child(15);

child1.hobby.push("basketball");
console.log(child1.name); // parent
console.log(child1.age); // 15
console.log(child1.type); // child
console.log(child1.hobby); // [ 'sing', 'rap', 'basketball' ]
console.log(child2.hobby); // [ 'sing', 'rap' ]
child1.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

# 六. 寄生组合继承

在组合继承中,调用了两次父类构造函数,这里通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

  1. 主要思想:借用 构造函数 继承 属性 ,通过 原型链的混成形式来继承方法

  2. 优点:

  • 只调用一次 SuperType 构造函数,只创建一份父类属性
  • 原型链保持不变
  • 能够正常使用 instanceof 与 isPrototypeOf
// 父类
function SuperType (name) {
  this.colors = ["red", "blue", "green"];
  this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
  return this.name;
};

// 子类
function SubType (name, subName) {
  // 调用 SuperType 构造函数
  SuperType.call(this, name); // ----第二次调用 SuperType,继承实例属性----
  this.subName = subName;
};

// ----第一次调用 SuperType,继承原型属性----
SubType.prototype = Object.create(SuperType.prototype)

SubType.prototype.constructor = SubType; // 注意:增强对象

let instance = new SubType('An', 'sisterAn')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 七. 原型式继承

实现思路就是将子类的原型设置为父类的原型

例子1

// 父类
function SuperType (name) {
  this.colors = ["red", "blue", "green"];
  this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
  return this.name;
};

/** 第一步 */
// 子类,通过 call 继承父类的实例属性和方法,不能继承原型属性/方法
function SubType (name, subName) {
  SuperType.call(this, name); // 调用 SuperType 的构造函数,并向其传参 
  this.subName = subName;
}

/** 第二步 */
// 解决 call 无法继承父类原型属性/方法的问题
// Object.create 方法接受传入一个作为新创建对象的原型的对象,创建一个拥有指定原型和若干个指定属性的对象
// 通过这种方法指定的任何属性都会覆盖原型对象上的同名属性
SubType.prototype = Object.create(SuperType.prototype, { 
  constructor: { // 注意指定 SubType.prototype.constructor = SubType
    value: SubType,
    enumerable: false,
    writable: true,
    configurable: true
  },
  run : { 
    value: function(){ // override
      SuperType.prototype.run.apply(this, arguments); 
      	// call super
      	// ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
}) 

/** 第三步 */
// 最后:解决 SubType.prototype.constructor === SuperType 的问题
// 这里,在上一步已经指定,这里不需要再操作
// SubType.prototype.constructor = SubType;
var instance = new SubType('An', 'sistenAn')
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

例2

如果希望能多继承 ,可使用混入的方式

// 父类 SuperType
function SuperType () {}
// 父类 OtherSuperType
function OtherSuperType () {}

// 多继承子类
function AnotherType () {
    SuperType.call(this) // 继承 SuperType 的实例属性和方法
    OtherSuperType.call(this) // 继承 OtherSuperType 的实例属性和方法
}

// 继承一个类
AnotherType.prototype = Object.create(SuperType.prototype);

// 使用 Object.assign 混合其它
Object.assign(AnotherType.prototype, OtherSuperType.prototype);
// Object.assign 会把  OtherSuperType 原型上的函数拷贝到 AnotherType 原型上,使 AnotherType 的所有实例都可用 OtherSuperType 的方法

// 重新指定 constructor
AnotherType.prototype.constructor = AnotherType;

AnotherType.prototype.myMethod = function() {
     // do a thing
};

let instance = new AnotherType()
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

最重要的部分是:

  • SuperType.call 继承实例属性方法
  • 用 Object.create() 来继承原型属性与方法
  • 修改 SubType.prototype.constructor的指向

# 八.ES6 继承

一个简单的 ES6 继承:

class People {
    constructor(name) {
        this.name = name
    }
    run() { }
}

// extends 相当于方法的继承
// 替换了上面的3行代码
class Man extends People {
    constructor(name) {
        // super 相当于属性的继承
        // 替换了 People.call(this, name)
        super(name)
        this.gender = '男'
    }
    fight() { }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

extends 继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

function _inherits(subType, superType) {
    // 创建对象,Object.create 创建父类原型的一个副本
    // 增强对象,弥补因重写原型而失去的默认的 constructor 属性
    // 指定对象,将新创建的对象赋值给子类的原型 subType.prototype
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: { // 重写 constructor
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 九.扩展:new

new 关键字创建的对象实际上是对新对象 this 的不断赋值,并将 prototype 指向类的 prototype 所指向的对象。

var SuperType = function (name) {
    var nose = 'nose' // 私有属性
    function say () {} // 私有方法
    
    // 特权方法
    this.getName = function () {} 
    this.setName = function () {}
    
    this.mouse = 'mouse' // 对象公有属性
    this.listen = function () {} // 对象公有方法
    
    // 构造器
    this.setName(name)
}

SuperType.age = 10 // 类静态公有属性(对象不能访问)
SuperType.read = function () {} // 类静态公有方法(对象无法访问)

SuperType.prototype = { // 对象赋值(也可以一一赋值)
    isMan: 'true', // 公有属性
    write: function () {} // 公有方法
}

var instance = new SuperType()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24