理解JS中的继承

面向对象之继承理解
较为常用的继承有原型链继承、借用构造函数、组合式继承、寄生组合式继承,以及ES6继承,其他的还有原型式继承、寄生式继承

原型链继承

  • js以原型链作为实现继承的主要方法
  • 基本思想是利用原型链让一个引用类型继承另一个引用类型的属性和方法
  • 构造函数、原型、实例的关系
    • 每个构造函数都有一个原型对象
    • 原型对象都有一个指向构造函数的方法
    • 实例都包含一个指向原型对象的一个指针
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
/**
* 父类
* @param {any} name
*/
function Parent(name) {
this.name = name;
this.age = 10;
}
Parent.prototype.sayName = function() {
console.log('parent name is ' + this.name + '!');
};
/**
* 子类
* @param {any} name
*/
function Child(name) {
this.name = name;
}
// 使用原型链继承Parent构造函数
Child.prototype = new Parent('father');
// constructor属性,指向当前的Child构造函数。
Child.prototype.constructor = Child;
Child.prototype.sayName = function() {
console.log('child name is ' + this.name + '!');
};
// 调用父类
var parent = new Parent('father');
parent.sayName(); // parent name is father!
// 调用子类
var child = new Child('son');
child.sayName(); // child name is son!
child.age = 18;
console.log(child.age); // 18

只要是原型链中出现过的原型,都可以说是该原型链派生的实例的原型。

缺点:

  • 子类型无法给超类型传递参数,在面向对象的继承中,我们总希望通过 var child = new Child(‘son’, ‘father’); 让子类去调用父类的构造器来完成继承。而不是通过像这样 new Parent(‘father’) 去调用父类。
  • Child.prototype.sayName 必须写在 Child.prototype = new Parent(‘father’); 之后,不然就会被覆盖掉。

借用构造函数 (类式继承)

  • 在子类型构造函数的内部调用超类型构造函数,通过call,apply 改变对象的this指向
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
/**
* 父类
* @param {any} name
*/
function Parent(name) {
this.name = name;
this.age = 10;
}
Parent.prototype.sayName = function() {
console.log('parent name is ' + this.name + '!');
};
Parent.prototype.doSomthing = function() {
console.log('parent do something!');
};
/**
* 子类
* @param {any} name
*/
function Child(name) {
// 通过call,apply 改变对象的this指向来继承
Parent.call(this, name);
// Parent.apply(this, arguments);
}
Child.prototype.sayName = function() {
console.log('child name is ' + this.name + '!');
};
var child = new Child('son');
child.sayName(); // child name: son
child.doSomthing(); // TypeError: child.doSomthing is not a function

相当于 Parent 这个函数在 Child 函数中执行了一遍,并且将所有与 this 绑定的变量都切换到了 Child 上,这样就克服了第一种方式带来的问题。

缺点:

  • 没有原型,每次创建一个 Child 实例对象时候都需要执行一遍 Parent 函数,无法复用一些公用函数。

组合式继承(原型+构造函数继承)

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
/**
* 父类
* @param {any} name
*/
function Parent(name) {
this.name = name;
this.age = 10;
}
Parent.prototype.sayName = function() {
console.log('parent name is ' + this.name + '!');
};
Parent.prototype.doSomthing = function() {
console.log('parent do something!');
};
/**
* 子类
* @param {any} name
*/
function Child(name) {
// 通过call,apply 改变对象的this指向来继承
Parent.call(this, name); // 第二次调用
// Parent.apply(this, arguments);
}
Child.prototype = new Parent('father'); // 第一次调用
Child.prototype.constructor = Child;
Child.prototype.sayName = function() {
console.log('child name is ' + this.name + '!');
};
var child = new Child('son');
child.sayName(); // child name: son
child.doSomthing(); // parent do something!

缺点:

  • 组合式继承是 JS 最常用的继承模式,但组合继承使用过程中会被调用两次:一次是创建子类型的时候,另一次是在子类型构造函数的内部。

原型式继承

  • 基于一个对象上,这个对象相当于作为原型,再根据需求对得到的对象加以修改
  • 在没有必要兴师动众创建构造函数,而只想让一个对象与另一个对象保持类似的情况下使用
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
var ob = { name: '李达康', friends: ['沙瑞金', '季昌明'] };
/**
* 原型式继承 参数o,引用类型值,实质就是一个内存地址
* @param {any} obj
* @returns
*/
function object(obj) {
/**
* 创建一个构造函数F
*/
function F() {
// 空构造函数F
}
F.prototype = obj;
return new F();
}
var ob1 = object(ob);
ob1.name = '侯亮明';
ob1.friends.push('陈海');
console.log(ob1.name); // 侯亮明
console.log(ob1.friends); // ["沙瑞金", "季昌明", "陈海"]
var ob2 = object(ob);
console.log(ob2.name); // 李达康
console.log(ob2.friends); // ["沙瑞金", "季昌明", "陈海"]

寄生式继承

  • 创建一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var ob = { name: '李达康', friends: ['沙瑞金', '季昌明'] };
// 上面再ECMAScript5 有了一新的规范写法,Object.create(ob) 效果是一样的
/**
* 创建一个对象
* @param {any} o
* @returns
*/
function createOb(o) {
var newob = Object.create(o); // 创建对象
newob.sayname = function() { // 增强对象
console.log(this.name);
};
return newob; // 指定对象
}
var ob1 = createOb(ob);
ob1.sayname(); // 李达康

寄生组合式继承

  • 组合继承最大的问题是无论什么情况下都会调用两次超类型构造函数
    • 一次是创建子类型原型时,一次是子函数的内部构造函数
  • 寄生组合继承通过借用构造函数来继承属性,通过原型链混成形式来继承方法。
  • 思路是不必为了指定的子类型原型而调用超类型的构造函数。
  • 使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型
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
45
46
47
48
49
50
51
/**
* 实现继承
* @param {any} Parent 父类
* @param {any} Child 子类
*/
function inheritPrototype(Parent, Child) {
Child.prototype = Object.create(Parent.prototype); // 修改
Child.prototype.construtor = Child;
}
/**
* 父类
* @param {any} name
*/
function Parent(name) {
this.name = name;
this.friends = ['达康', '瑞金'];
}
Parent.prototype.sayName = function() {
console.log('parent name is ' + this.name + '!');
};
/**
* 子类
* @param {any} name
* @param {any} parentName
*/
function Child(name, parentName) {
Parent.call(this, parentName);
this.name = name;
}
// 实现继承
inheritPrototype(Parent, Child);
Child.prototype.sayName = function() {
console.log('child name is ' + this.name + '!');
};
var parent = new Parent('father');
parent.sayName(); // parent name: father
var child1 = new Child('son', 'father');
child1.friends.push('猴子'); // ["达康", "瑞金", "猴子"]
console.log(child1.friends);
child1.sayName(); // child name: son
var child2 = new Child('son2', 'father');
console.log(child2.friends); // ["达康", "瑞金"]
child1.sayName(); // child name: son2
  • 寄生组合式继承方式,跟组合式继承的区别在于,不需要在一次实例中调用两次父类的构造函数
  • 假如说父类的构造器代码很多,还需要调用两次的话对系统肯定会有影响,寄生组合式继承的思想在于:用一个 F 空的构造函数去取代执行了 Parent 这个构造函数。

ES 6 继承

  • ES6提供了更接近传统语言”类”的写法,引入了Class(类)这个概念,作为对象的模板。
  • 通过class关键字,可以定义类。
  • 基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到。
  • 新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。下面我们用ES6的语法实现类的继承。
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* 父类
* @class Parent
*/
class Parent {
/**
* Creates an instance of Parent.
* @param {any} name
* @memberOf Parent
*/
constructor(name) {
this.name = name;
}
/**
* 做点东西
* @memberOf Parent
*/
doSomething() {
console.log('parent do something!');
}
/**
* 打印名字
* @memberOf Parent
*/
sayName() {
console.log(`parent name is ${this.name}!`);
}
}
/**
* 子类 继承 父类
* @class Child
* @extends {Parent}
*/
class Child extends Parent {
/**
* Creates an instance of Child.
* @param {any} name
* @param {any} parentName
* @memberOf Child
*/
constructor(name, parentName) {
// 调用基类的构造方法
super(parentName);
this.name = name;
}
/**
* 打印名字 覆盖父类的sayName方法
* @memberOf Child
*/
sayName() {
console.log(`child name is ${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语法开发,推荐使用ES6继承

参考文献

30 分钟学会 JS 继承
读书笔记–对象、实例、原型、继承
javascript高级程序设计

推荐文章

vue2.x Cnode社区
基于vue1.0开发的移动端H5积分商城项目
使用gulp前端自动化构建工具搭建前端项目

觉得有用的话,记得在GitHub中给个star哦!

vincentSea wechat
一颗稻草的价值,到底是多少呢?想知道的话,就订阅吧!