对象 | 继承(含图解、手写)

原型链继承

基本思想:通过原型链继承多个引用类型的属性和方法

我们知道,实例和原型是有直接联系的,实例可以通过__proto__访问原型,所以原型链继承即将父类的实例作为子类的原型,这样子类的原型就可以通过__proto__访问父类的原型了

1
2
3
4
5
6
7
8
function Father() {};

function Son() {};

// 继承Father
Son.prototype = new Father();

console.log(Son.prototype.__proto__ === Father.prototype); // true

优点

  • 父类的方法子类能够被子类复用,因为子类可以访问父类原型,原型存在着父类实例可以共享的方法和属性

缺点

  • 更改一个子类的引用,其他子类也会受到影响
1
2
3
4
5
6
7
8
9
10
11
12
function Father() {};
Father.prototype.colors = ['red', 'black', 'yellow'];

function Son() {};
Son.prototype = new Father();

let p1 = new Son();
let p2 = new Son();

p1.colors[1] = 'pink';
console.log(p1.colors); // ['red', 'pink', 'yellow']
console.log(p2.colors); // ['red', 'pink', 'yellow']
  • 子类在实例化的时候不能向父类传参

盗用构造函数继承

基本思想:在子类构造函数中调用父类构造函数,可以使用call或者apply的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Father() {
this.colors = ['red', 'black', 'yellow'];
};

function Son() {
// 继承Father
Father.call(this); // 通过此方法让子类继承父类中的方法和属性
};

let p1 = new Son();
let p2 = new Son();

console.log(p1.colors); // ['red', 'black', 'yellow']
console.log(p2.colors); // ['red', 'black', 'yellow']

优点

  • 可以在子类构造函数中向父类构造函数传参
1
2
3
4
5
6
7
8
9
10
11
function Father(name) {
this.name = name;
};

function Son() {
// 继承Father
Father.call(this, 'Katrina'); // 通过此方法让子类继承父类中的方法和属性
};

let p1 = new Son();
console.log(p1.name); // Katrina
  • 父类的引用属性不会被共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Father() {
this.colors = ['red', 'black', 'yellow'];
};

function Son() {
// 继承Father
Father.call(this); // 通过此方法让子类继承父类中的方法和属性
};

let p1 = new Son();
let p2 = new Son();

p1.colors[1] = 'pink';
console.log(p1.colors); // ['red', 'pink', 'yellow']
console.log(p2.colors); // ['red', 'black', 'yellow']

缺点

  • 必须在构造函数中定义方法,函数不能复用,每次创建都会初始化
1
2
3
4
5
6
7
function Father(name) {
this.colors = ['red', 'black', 'yellow'];
this.name = name;
this.sayHello = function() {
console.log(`${this.name} say hello!`)
};
}

每一次调用new Father,就会在实例内部定义一次这个sayHello方法,也就是new Function(console.log(${this.name} say hello!)),其实是更推荐把方法定义在Father.prototype上的,这样每个实例构造出来就自动继承这个方法,不需要在构造函数里面一次次地写

1
2
3
4
5
6
7
8
9
10
11
12
function Son(name) {
Father.call(this, name);
}

// 等价于
function Son(name) {
this.colors = ['red', 'black', 'yellow'];
this.name = name;
this.sayHello = function() {
console.log(`${this.name} say hello!`)
};
}

也就是说在构造函数继承的时候也在一次次调用sayHello这个方法

  • (上个问题的延申)子类不能访问父类原型上的方法【instanceOf会失效】,所有方法都只能定义在父类构造函数中

组合式继承

基本思想:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性

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
function Father(name, age) {
this.name = name;
this.age = age;
this.friends = ['Jack', 'Jenny', 'Lucy'];
}

Father.prototype.sayName = function() {
console.log(this.name);
}

function Son(name, age) {
// 构造函数继承
Father.call(this, name, age); // 调用两次
};

// 原型链继承
Son.prototype = new Father(); // 调用一次

let p1 = new Son('Katrina', 18);
let p2 = new Son('Kate', 20);

p1.sayName(); // 'Katrina'
p2.sayName(); // 'Kate'

p1.friends.push('Bob');
console.log(p1.friends); // ['Jack', 'Jenny', 'Lucy', 'Bob']
console.log(p2.friends); // ['Jack', 'Jenny', 'Lucy']

优点

  • 父类的方法子类能够被子类复用
  • 父类的引用属性不会被共享
  • 子类可以访问父类原型上的方法

缺点

  • 效率问题:父类构造函数始终会被调用两次
    • 创建子类原型时调用
    • 在子类构造函数中调用

原型式继承

基本思想:把现有的对象指定为构造函数的原型对象,并返回以这个对象为原型的构造函数的实例

适用于:你有一个对象,你想在这个对象的基础上再创建一个对象 以及 不需要单独创建构造函数,但仍需要在对象间共享信息的场合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function object(o) {
function F(){}; // 临时构建构造函数
F.prototype = o; // 把传入的对象指定为
return new F(); // 返回临时类型的实例
}


// 本质上,object是吧传入的o进行了一次浅复制
let person = {
name: 'Katrina',
age: 18,
gender: 'female',
friends: ['Jack', 'Jenny', 'Lucy'],
};

let anotherPerson = object(person);
anotherPerson.name; // 'Katrina';
anotherPerson.friends.push('Kate');

let yetAnotherPerson = object(person);
yetAnotherPerson.name; // 'Katrina';
yetAnotherPerson.friends.push('Bob');

console.log(person.friends); // ['Jack', 'Jenny', 'Lucy', 'Kate', 'Bob']

ES5中新增的Object.create()只传一个参数时就是运用的这种思想:手写原理 | Object.create

优点

  • 父类方法可以复用

缺点

  • 更改一个子类的引用,其他子类也会受到影响
  • 子类在实例化的时候不能向父类传参

寄生式继承

基本思想:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

适用于:主要关注对象,而不在乎类型和构造函数的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function object(o) {
function F(){}; // 临时构建构造函数
F.prototype = o; // 把传入的对象指定为
return new F(); // 返回临时类型的实例
}


function createAnotherPerson(original) {
let clone = object(original); // 创建一个新对象,它的构造函数的原型是original
clone.sayHi = function() { // 给对象添加一个sayHi的方法
console.log('Hi');
};
return clone; // 返回这个对象
}

let person = {
name: 'Katrina',
age: 18,
gender: 'female',
friends: ['Jack', 'Jenny', 'Lucy'],
};

let anotherPerson = createAnotherPerson(person);
anotherPerson.sayHi(); // Hi

object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用

寄生式组合继承

寄生式组合继承主要是为了解决组合继承的效率问题

基本思想:不通过调用父类构造函数给子类原型赋值,而是取得父类函数的一个副本,即使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型

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
function object(o) {
function F(){}; // 临时构建构造函数
F.prototype = o; // 把传入的对象指定为
return new F(); // 返回临时类型的实例
}

function inheritPrototype(Son, Father) {
let prototype = object(Father.prototype); // 创建对象
prototype.constructor = Son; // 增强对象
Son.prototype = prototype; // 赋值对象
}




function Father(name, age) {
this.name = name;
this.age = age;
this.friends = ['Jack', 'Jenny', 'Lucy'];
}

Father.prototype.sayName = function() {
console.log(this.name);
}

function Son(name, age) {
// 构造函数继承
Father.call(this, name, age); // 调用一次
};

inheritPrototype(Father, Son);

instanceof isPrototypeof方法正常有效

总结

参考

关于构造函数继承的缺点的一个疑问

一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends