# ES5继承

ECMAScript只支持实现继承,主要依靠原型链来实现。基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。继承核心思想是能够继承父类方法的同时,保证自己的私有属性和方法

# 原型链继承

如果让原型对象等于另一个类型的实例,结果会怎样呢?

此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。

function APro() {

}
APro.prototype.a = 5;
function BPro() {

}
BPro.prototype.b = 10;
APro.prototype = new BPro(); //APro继承了BPro
console.log(APro.prototype.b) // 10
a1 = new APro();

console.log(a1.b) //10
console.log(BPro.prototype.hasOwnProperty('b')) // true
console.log(a1.prototype.hasOwnProperty('b')) // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

所有的引用类型默认继承Object,而这个继承也是通过原型链实现的

在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也是所有自定义类型都会继承toString(),valueOf()等默认方法的原因。

默认方法

let newObj = new Object()
console.log(Object.getPrototypeOf(newObj))
1
2

img

# 确定原型和实例的关系

  1. instanceof操作符。
  2. isPorototypeOf()方法。只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。因此该方法会返回true

# 谨慎定义方法

子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型不存在的某个方法。但不管怎么样,给原型添加方法的代码一定要放在替换原型语句之后。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function () {
    return this.property
}

function SubType() {
    this.subproperty = false;
}

//继承了SuperType 替换原型语句
SubType.prototype = new SuperType();

//添加新方法
SubType.prototype.getSubValue = function () {
    return this.subproperty;
}
//重写超类型方法
SubType.prototype.getSuperValue = function () {
    return false;
}
var instance = new SubType();
console.log(instance.getSuperValue()) // false
console.log(instance.getSubValue()) // false

var test = new SuperType()
console.log(test.getSuperValue()) //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

如果把SubType的新方法getSubValue语句添加在替换原型语句之前,那么重写的超类型方法语句将会覆盖掉这个新方法,导致第二个console会打印出instance.getSubValue is not a function 添加新方法的时候不能使用对象字面量创建原型方法,这样就会重写原型链。

# 原型链继承的问题

  1. 包含引用类型值的原型属性会被所有实例共享;这也是为什么要在构造函数中,而不是在原型对象中定义属性的原因。
  2. 在创建子类型的实例时,不能向超类型的构造函数传递参数。
function SuperType () {
    this.colors = ['red', 'blue', 'green'];
}
function SubType() {

}
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]

var instance2 = new SubType()
console.log(instance2.colors)// ["red", "blue", "green", "black"]
1
2
3
4
5
6
7
8
9
10
11
12
13

修改后就能反映出实例共享引用类型值的原型属性这一种现象

function SuperType () {
    this.colors = 4;
}
function SubType() {

}
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors = 5;
console.log(instance1.colors); // 5

var instance2 = new SubType()
console.log(instance2.colors)// 4
1
2
3
4
5
6
7
8
9
10
11
12
13

因为数值类型不是引用类型,所以就不会共享。

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

在子类型构造函数内部调用超类型构造函数。函数只不过是特殊环境下执行代码的对象,所以可以通过使用apply()call()方法在新创建的对象上执行构造函数

function SuperType () {
    this.colors = ['red', 'blue', 'green'];
}
function SubType() {
    // 继承了SuperType
    SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]

var instance2 = new SubType()
console.log(instance2.colors)// ["red", "blue", "green"]
console.log(instance2.hasOwnProperty('colors')) // true 和原型链的不同了
1
2
3
4
5
6
7
8
9
10
11
12
13
14

TIP

实际是在未来新创建的SubType实例的环境下调用超类型构造函数,这样每个SubType实例都会具有自己的引用类型属性。

  1. 子类型构造函数中可以向超类型构造函数传递参数。
function SuperType(name) {
    this.name = name;
}
function SubType() {
    //继承了SuperType,同时还传递了参数
    SuperType.call(this, "linjiaheng");

    //实例属性 尽量跟在继承语句后面
    this.age = 29; 
}
var instance = new SubType();
console.log(instance.name); //linjiaheng
console.log(instance.age); //29
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 借用构造函数的问题:。方法都在构造函数中定义,那么函数复用就无从谈起。而且,在父类型的原型中定义方法,对于子类型来说也是不可见的。
function SuperType(name) {
    this.name = name;
}
SuperType.prototype.sayName = function() {
    console.log(this.name)
}
function SubType() {
    
    //继承了SuperType,同时还传递了参数
    SuperType.call(this, "linjiaheng");
    
}
var instance = new SubType();
instance.sayName() // Uncaught TypeError: instance.sayName is not a function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 这种继承方式的优点:
  • 实现实例化对象的独立性
  • 可以给实例化对象添加参数
  1. 这种继承方式的缺点:
  • 方法都在构造函数中定义,每次实例化对象都得创建一遍方法,基本无法实现函数复用
  • call方法仅仅调用了父级构造函数的属性及方法,没有办法调用父级构造函数原型对象的方法

# 组合继承(伪经典继承)最常用

TIP

将原型链和借用构造函数的技术组合一块

背后思路是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。共享父类方法不共享父类引用属性(两次调用超类型构造函数成为最大的问题

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);  // 第二次调用
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType(); // 第一次调用
SubType.prototype.constructor = SubType; // 修复子类的constructor指向
SubType.prototype.sayAge = function() {
    console.log(this.age);
}
var instance1 = new SubType('linjiaheng', 29);
instance1.colors.push('black');
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // linjiaheng
instance1.sayAge(); // 29

var instance2 = new SubType('xielin', 28); 
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // xielin
instance2.sayAge(); // 28

console.log(instance1 instanceof SubType) // true
console.log(instance1 instanceof SuperType) //true
console.log(instance1 instanceof Object) //true
console.log(SubType.prototype.isPrototypeOf(instance1)) // true
console.log(SuperType.prototype.isPrototypeOf(instance1)) // 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
function Parent(name){
    this.name = name;
    this.type = ['JS','HTML','CSS'];
}
Parent.prototype.Say=function(){
    console.log(this.name);
}
function Son(name){
    Parent.call(this,name);
}
Son.prototype = new Parent();
son1 = new Son('张三');
son2 = new Son('李四');
son1.type.push('VUE');
son2.type.push('PHP');
console.log(son1.type);//['JS','HTML','CSS','VUE']
console.log(son2.type);//['JS','HTML','CSS','PHP']
son1.Say();//张三
son2.Say();//李四
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIP

  • 优点:

    • 利用原型链继承,实现原型对象方法的继承
    • 利用构造函数继承,实现属性的继承,而且可以参数
  • 缺点:

    • 调用了两次父构造函数

# 原型式继承(优缺点和原型链继承类似)

function object(o) {
    function F(){}
    F.prototype = o;
    return new F()
}
var obj1 = {name: 'linjiaheng', age: 23} //实例
console.log(object(obj1)) 
1
2
3
4
5
6
7

img

每个实例都存在个指针_proto_指向原型

function object(o) {
    function F(){}
    F.prototype = o;
    return new F()
}
var person = {
    name: 'linjiaheng',
    friends: ['laihengcong','penghaohao']
};
var anotherPerson = object(person); //实例
console.log(anotherPerson) // 1

anotherPerson.name = 'xielin';
anotherPerson.friends.push('mayun');
console.log(anotherPerson)  //2

console.log(anotherPerson.friends) // ["laihengcong", "penghaohao", "mayun"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

打印1的结果

img

打印2的结果

img

TIP

修改非引用类型属性时,实例属性会跟着变化,原型对象person不受影响。修改引用类型属性时原型对象会跟着变化。这个时候打印引用类型属性时也变化了。

ES5通过新增Object.create()方法规范化原型式继承。这个方法接受两个参数:一个用作新对象的原型对象;一个为新对象定义额外属性的对象(可选的)。在传入一个参数的情况下,Object.create()object()行为相同。

第二个参数如果没有指定则为 undefined,则是要添加到新创建对象的不可枚举(默认)属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。

var person = {
    name: 'linjiaheng',
    friends: ['laihengcong','penghaohao']
};
var anotherPerson = Object.create(person);
console.log(anotherPerson)
1
2
3
4
5
6

img

var person = {
    name: 'linjiaheng',
    friends: ['laihengcong','penghaohao']
};
var secondPerson = Object.create(person, {
    age: {
        value: 27
    }
})
console.log(secondPerson)
var anotherPerson = Object.create(person);
console.log(anotherPerson)
1
2
3
4
5
6
7
8
9
10
11
12

img

WARNING

原型继承会使得引用类型值的属性始终都会被实例共享

# 寄生式继承(优缺点和原型式继承类似)

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫做寄生式继承。相对于原型式继承,还是在父类基础上添加更多的方法。

function object(o) {
    function F(){}
    F.prototype = o;
    return new F()
}
function createAnother(original) {
    var clone = object(original); // 通过调用函数创建一个新对象
    clone.sayHi= function() { //增强对象
        console.log('hi') 
    }
    return clone; // 返回对象
}

var person = {
    name: 'linjiaheng',
    friends: ['shelby', 'court', 'van']
}
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

TIP

新对象不仅具有person的所有属性和方法,还具有自己的sayHi()方法。object()函数不是必需的,任何能够返回新对象的函数都适用此模式。

例如:

function createAnother(original, age) {
    var clone = Object.create(original, {age: {
        value: age
    }}); // 通过调用函数创建一个新对象
    clone.sayHi= function() { //增强对象
        console.log('hi') 
    }
    return clone; // 返回对象
}

var person = {
    name: 'linjiaheng',
    friends: ['shelby', 'court', 'van']
}
var anotherPerson = createAnother(person, 22);
anotherPerson.sayHi() //hi
console.log(anotherPerson.age) // 22
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

WARNING

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

# 寄生组合式继承 (最常用)

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了指定子类型的原型而调用超类型构造函数。使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。回到之前的组合式继承,那时候我们将类式继承和构造函数继承组合使用,但是存在的问题就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承

基本模式:

function inheritPrototype(subType, superType) {
    subType.prototype = Object.create(superType.prototype)
    subType.prototype.constructor = subType
}

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) // 核心
    this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
    console.log(this.age)
}
var instance1 = new SubType('linjiaheng', 22)
console.log(instance1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

img

寄生组合继承另一个例子:

// 父类
function Father(name) {
    this.name = name;
    this.colors = ['red','blue','green']
}

//方法定义在原型对象上(共享)
Father.prototype.sayName = function() {
    alert(this.name)
}
function Son(name,age) {
    Father.call(this, name) // 核心
    this.age = age;
}
Son.prototype = Object.create(Father.prototype) // 核心
Son.prototype.constructor = Son // 修复子类constructor的指向,不加这句的话,这个结果就变成父类型的构造函数了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

TIP

  1. 我们需要继承的仅仅是父类的原型,不用去调用父类的构造函数。换句话说,在构造函数继承中,我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们可以通过原型继承拿到,但是这么直接赋值给子类会有问题,因为对父类原型对象复制得到的复制对象p中的constructor属性指向的不是subClass子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复起constructor属性指向性不正确的问题,最后将得到的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。

这种方式继承其中最大的改变就是子类原型中的处理,被赋予父类原型中的一个引用,这是一个对象,因此有一点你需要注意,就是子类在想添加原型方法必须通过prototype.来添加,否则直接赋予对象就会覆盖从父类原型继承的对象了.