RadiomM
文章22
标签13
分类1
原型继承的六种类型比较

原型继承的六种类型比较

最近在恶补基础,发现我目前虽然对原型以及原型链有一定的理解,但是真要去记住继承的这几种方式的优缺点的时候,反而不能够说的出来,加上本来就这种东西理解还是不够,故我决定是时候重新开始写一篇文章去总结这个东西,当然这些东西都是需要记的,所以说,有一定理解之后再去记这些东西是比较好的,那废话不说了,我们开始吧。

课前总结

虽然我之前写的一篇文章有粗略的说过几种继承方式,但是不够全面,而且着重于解决办法,却没有具体的总结。其实原型的继承无非就是下面几点:

  • 父原型上的方法和定义的变量能否获取以及被影响

  • 能否向父原型上的方法传递参数

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Dog(name, age) {
this.name = name
this.age = age
}
Dog.prototype.eat = function () {
console.log('吃东西')
}
Dog.prototype.drink = function () {
console.log('喝东西')
}


function dog() {}

dog.prototype = new Dog() // 记住这个方式

我们知道原型上还有一个 construtor 指向的是构造函数本身,所以很明显,这里 dog 的 construtor 指向的是 Dog 函数本身。而且这样的继承方式虽然可以获取到Dog上面定义的变量以及原型上的方法,但是假如是定义的是一个引用类型,那么,这样的继承方式就会印象到其他的实例对象,像下面一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Dog(name, age) {
this.name = name
this.age = age
+ this.c = {
+ foo: '1',
+ bar: '2'
+ }
}


const b = new dog()

const a = new dog()

b.c.foo = "3"

console.log("a", a.c.foo); // 3

借助 call、apply 函数继承

我们使用下面的代码进行测试:

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
function Dog(name, age) {
this.name = name
this.age = age
this.c = {
foo: '1',
bar: '2'
}
}
Dog.prototype.eat = function () {
console.log('吃东西')
}
Dog.prototype.drink = function () {
console.log('喝东西')
}


function dog() {
Dog.call(this,'ydw','28')
}

const b = new dog()

const a = new dog()


b.c.foo = "3"

console.log("b", b.c.foo); // 3
console.log("a", a.c.foo); // 1

通过借助call,apply函数,我们可以解决上面提到的引用类型修改影响其他实例对象的问题,但是这个也是不完美的,为什么呢,因为这种方法的使用,只能继承在Dog 函数上定义的东西,不能继承在Dog 函数原型上定义的方法或者变量。看下面代码:

1
2
console.log("a.drink()", a.drink()); // a.drink is not a function

组合继承

既然原型继承可以获取到原型上面的方法以及变量,而借助 call、apply 函数可以保证引用类型的数据不受影响,那么我们只需要将两者结合在一起,那是不是就可以完美解决这两个问题呢?,看下面代码:

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 Dog(name, age) {
this.name = name
this.age = age
this.c = {
foo: '1',
bar: '2'
}
}
Dog.prototype.eat = function () {
console.log('吃东西')
}
Dog.prototype.drink = function () {
console.log('喝东西')
}


function dog() {
Dog.call(this,'ydw','28')
}

dog.prototype = new Dog()

const a = new dog();
const b = new dog();

b.c.foo = "3";

console.log("b", b.c.foo); // 3
console.log("a", a.c.foo); // 1

console.log("dog", dog.prototype);

这种方式确实是可以做到,传递参数以及是父类定义的变量和原型上的方法,不过还是有一个问题,还是 construtor 问题,指向的不对。也就是说虽然可以用,但是调用了两次父类的构造函数方法,比如:**new Dog()、Dog.call()**在性能上是不好的。

原型式继承

简单的说,我们之前都是需要先构造一个父类(如 Dog 函数),然后再构造一个子类(如 dog 函数),然后利用原型链继承的方式,实现继承。这个原型式继承,实则就是减少了其中一步,不需要另外构造一个子类,直接再内部实现一个,直接复制一份返回给你,但是依旧有问题,还是我们之前说的一样的问题。

1
2
3
4
5
6

function copyObj(instance) {
function son() {}
son.prototype = instance
return new son()
}

因为是对属性的浅复制,所以还是避免不了所有引用类型的问题,就是共享引用类型存储地址。还有一个问题就是无法传递值的问题,我们知道之前我们可以通过 call,apply 的函数进行值传递,后面我们会逐一解决这个问题。其实还可以用 Object.create()去代替,效果也是一样的,第一层的浅复制。

寄生继承

为什么会出现寄生继承呢,其实原则上是为了对原型链式的继承进行增强,也就是说,除了能对原型链上的属性以及方法得到继承外,我们还需要扩展到其他的属性方法,那么寄生继承就出现了,看看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let Dog = {
name: "ydw",
age: "25",
gender: "男",
sayhi: function () {
console.log("hi");
},
}
function clone() {
let copy = copyObj(Dog);
copy.makeFriend = function () {
console.log("make friends");
};
copy.job = "font-end-engineer";
return copy;
}
let dog = clone(Dog)
console.log("job", dog.job);
console.log("name", dog.name);
console.log("friend", dog.makeFriend);

虽然这种方式可以增强继承,但是总的来说,浅复制还是会有,引用类型赋值地址问题,还是无法从根本解决问题。其实我们从上面的例子中知道,只有通过 apply,call 这类的函数才能做到每个实例子类独享一份数据,但是不能继承原型上的方法属性,虽然组合继承结合了原型链以及构造函数的优点,但是两次调用父类构造函数,未免会觉得性能方面颇有缺失,那么有没有可以真正意义上能完美解决问题的方法呢?其实是有的,可以看下面。

寄生组合继承

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
function clone(parent,child) {
child.prototype = Object.create(parent.prototype)
child.prototype.construtor = child
}

function Dog(name, age) {
this.name = name
this.age = age
this.c = {
foo: '1',
bar: '2'
}
}

Dog.prototype.eat = function () {
console.log('吃东西')
}

Dog.prototype.drink = function () {
console.log('喝东西')
}


function dog() {
Dog.call(this)
this.gender = '男'
}

clone(Dog,dog)



const dog1 = new dog()
const dog2 = new dog()

dog2.c.pro = '3'

dog1.c.bor = '4'


console.log('dog2',dog2.c) /* {foo: '1', bar: '2', pro: '3'} */
console.log('dog1',dog1.c) // {foo: '1', bar: '2', bor: '4'}

这是目前比较完美的实现继承的方式,也是 ES6 版本 class 关键词所使用的继承方式,在减少调用父类构造方法的同时,也能使用原型链上的属性以及方法,同时保证了实例对象数据独享的问题。

总结

在说原型,原型链的文章中,我也说过部分类似的继承,但是说的比较简单,这次写这边文章主要是最近的发现对这个东西一窍不通了,也是为了之后可以复习所用。说回到这个继承的东西,在一开始的时候,我说了两个结论,实际上这两个东西一直在贯穿着继承的问题,我们知道在这些实现继承的方法中,无非就两个东西,原型链以及是构造方法

  • 原型链:通过子类继承父类的原型上,可以实现对原型的方法以及属性的调用,但是却不能做到方法以及属性的独享。

  • 构造方法: 虽然能实现方法以及属性的独享,但是无法获取到父类原型上的方法以及属性,也就是说是不可枚举的。

实际上,对应原型链方法的有:原型链继承、原型式继承(实则是浅复制属性和方法)、寄生继承。对应构造方法的有:call、apply 函数。像组合继承、寄生组合继承,都是在结合这两个方法的基础上优化一点,得出来的,像组合继承他的唯一缺点就是多次调用了父类构造方法,像寄生组合在原型式基础上,使用 Object.create 方法优化复制,减少了父类构造方法的次数,然后结合构造方法最终实现的继承。