js的五种继承方式

98 阅读6分钟

### 原型链继承

#### 代码实现

```js

function Parent() {

this.name = 'I am Parent'

}

Parent.prototype.run = function () {

console.log(this.name)

}

function Child() {}

Child.prototype = new Parent()

Child.prototype.constructor = Child

const c1 = new Child()

c1.run() // I am Parent

```

#### 问题分析

上面的代码运行的大致过程为:Child类实例化一个c1对象,接着c1访问Child类的属性run,然而Child类上并没有这个属性,接着会去Child的原型上面去找,如果Child.prototype上也没有,则会去Parent.prototype上面去找,从而实现了继承。

#### 代码分析

1. 声明一个Parent类,设置Parent中name属性,并赋值"I am Parent"

2. 在Parent.prototype上设置一个run方法,方法体中打印name属性

3. 声明一个子类Child

4. 将Child.prototype重新赋值为Parent类的实例对象(new Parent()相当于实例化一个新对象,该对象拥有Parent类上的属性以及Parent原型上的属性)。

+ 如果Child.prototype = Parent.prototype 这种赋值的话,会导致只能找到Parent原型上面的属性,而Parent上面的属性找不到,比如name属性,运行c1.run会打印出undefined。

5. 经过上一步`Child.prototype = new Parent()`, 导致了Child的constructor指向了Parent(c1.__proto__ === Child.prototype, Child.prototype.__proto__ === Parent.prototype),

但是有时候需要根据constructor来进行类型判断,为了防止判断出错,需要将原型上的属性constructor重新指向Child,因此才有了这一步操作`Child.prototype.constructor = Child`

#### 原型链继承的优缺点

优点:

1. 原型上面的方法是共享的,都是指向同一内存地址。

缺点:

1. 创建实例的时候,不能传参。

2. 如果类中有的属性为引用类型,那么,一旦其中某个实例改变了这个属性,那么其他实例中的该属性也会跟着改变。看下面这个例子

```js

function Parent() {

this.name = 'I am Parent'

this.colors = ['red', 'black']

}

// ... 省略部分代码

const c1 = new Child()

const c2 = new Child()

console.log(c1.colors) // ["red", "black"]

c2.colors.push('yellow')

console.log(c1.colors) // ["red", "black", "yellow"]

```

### 构造函数继承

为了解决上面继承方式的缺点,我们来看一下构造函数继承。

#### 代码实现

```js

function Parent(name, colors) {

this.name = name

this.colors = colors

}

function Child(id, name, colors) {

Parent.apply(this, Array.from(arguments).slice(1))

this.id = id

}

const c1 = new Child('c1', 'child1', ['red'])

const c2 = new Child('c2', 'child2', ['red'])

c2.colors.push('yellow')

console.log(c1.colors) // ["red"]

console.log(c2.colors) // ["red", "yellow"]

```

#### 问题分析

原型链继承中,如果类中有引用类型的属性,那么不同实例之间有可能会互相影响,为了解决该问题,可以把Parent类上面的属性方法都放在Child上面,不放到原型对象上面,防止被所有实例所共享,同时可以使用call、apply等方法改变this指向,同时还能够传参,因此,可以解决原型链继承方式的缺点。

#### 代码分析

1. 声明一个Parent类,接收两个参数,name、colors

2. 声明一个Child类,接收三个参数,id、name、colors

+ 这里使用apply方法改变了this指向,并且传参,复制了一遍Parent上的操作

3. 实例化两个变量 c1和c2,打印引用类型的属性colors,两个实例上的属性colors互相不影响了

#### 构造函数继承优缺点

优点:

1. 可以传参

2. 引用类型的属性在各个实例上面不会互相影响

缺点:

1. 如果类中定义了方法的话,那么实例在创建的时候,就会创建一遍方法,导致多开辟了一块内存空间,造成了内存浪费,来看下面的代码

```js

function Parent(name, colors) {

this.name = name

this.colors = colors

this.run = function() {

console.log(this.name)

}

}

// ... 省略部分代码

const c1 = new Child('c1', 'child1', ['red'])

const c2 = new Child('c2', 'child2', ['red'])

cosnole.log(c1.run === c1.run) // false

```

因为run定义在了Parent类中,为了检验实例c1和实例c2中的run是否指向同一块内存地址,通过`c1.run === c1.run`判断是否为true, 结果返回的false,说明每次实例化的时候都会创建一遍run方法。

### 组合继承

组合继承是谁和谁的组合?

在解答这个问题之前,先回顾一下上面的两种继承方式

1. 原型链继承方式,实现了基本继承,方法放在了prototype上面,子类可以直接调用,但是引用类型的属性会被所有实例所共享,而且不能传参

2. 构造函数继承方式,解决了原型链继承遇到的两个问题,但是也引出了另外一个问题,就是类中声明的方法在实例化的时候会被重复创建的问题,导致内存占用过多。

通过上面总结的内容,突然发现原型链继承方式可以解决方法重复创建的问题,因此,我们将这两种继承方式组合起来使用,这就叫做组合继承(原型链继承+构造函数继承)

#### 代码实现

```js

function Parent(name, colors) {

this.name = name

this.colors = colors

}

Parent.prototype.run = function() {

console.log(this.name)

}

function Child(id, name, colors) {

this.id = id

Parent.apply(this, Array.from(arguments).slice(1))

}

Child.prototype = new Parent()

Child.prototype.constructor = Child

const c1 = new Child('c1', 'child1', ['red'])

const c2 = new Child('c2', 'child2', ['red'])

c1.colors.push('yellow')

c1.run()

c2.run()

console.log(c1.run === c2.run)

console.log(c1.colors)

console.log(c2.colors)

```

#### 问题分析

组合继承就是将原型链继承和构造函数继承方式的组合

#### 代码分析

这里面就不一步一步的具体分析了,这两种组合方式很好的解决了上面遇到的问题(构造函数内部声明的方法重复创建的问题)

#### 组合继承优缺点

缺点:细心的话不难发现,代码中调用了两次构造函数,做了重复操作。

```js

Parent.apply(this, Array.from(arguments).slice(1)) // apply、call方法都是直接执行

Child.prototype = new Parent()

```

因此组合继承也不完美,还差点意思。

### 寄生组合继承

组合继承重复调用了两次构造函数,因此,可以针对这两步做一下优化

`Parent.apply(this, Array.from(arguments).slice(1))`这一步是为了复制属性,因此这块代码肯定不能动

`Child.prototype = new Parent()`这一步就是为了得到父类原型上面的方法,因此可以考虑让Child.prototype间接访问到Parent.prototype,从而减少一次构造函数的调用

#### 代码实现

```js

function Parent(name, colors) {

this.name = name

this.colors = colors

}

Parent.prototype.run = function() {

console.log(this.name)

}

function Child(id, name, colors) {

this.id = id

Parent.apply(this, Array.from(arguments).slice(1))

}

// 方法一

// function TempFunc() {}

// TempFunc.prototype = Parent.prototype

// Child.prototype = new TempFunc()

// 方法二

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

Child.prototype.constructor = Child

const c1 = new Child('c1', 'child1', ['red'])

const c2 = new Child('c2', 'child2', ['red'])

c1.colors.push('yellow')

c1.run()

c2.run()

console.log(c1.run === c2.run)

console.log(c1.colors)

console.log(c2.colors)

```

#### 问题分析

通过代码可以看出,寄生组合继承就是:通过借用构造函数来继承属性,通过原型链来继承方法

#### 代码分析

声明那部分的代码就不一一分析了,这里主要分析这句代码`Child.prototype = Object.create(Parent.prototype)`,因为其他的代码上面已经分析过了,就不赘述了

这里需要注意的是Object.create()方法是ES5中原型式继承的规范化,当只传一个参数时,内部的行为如下

```js

function object (o) {

function F() {};

F.prototype = o;

return new F();

}

```

因此和上面的代码(TempFunc)是等价的

**注意**

通过原型链继承方法时,不能Child.prototype = Parent.prototype这样直接赋值,至于为什么,看下面的代码

```js

function Parent(name, colors) {

this.name = name

this.colors = colors

}

Parent.prototype.run = function() {

console.log(this.name)

}

function Child(id, name, colors) {

this.id = id

Parent.apply(this, Array.from(arguments).slice(1))

}

Child.prototype = Parent.prototype

Child.prototype.constructor = Child

const c1 = new Child('c1', 'child1', ['red'])

const c2 = new Child('c2', 'child2', ['red'])

console.log(c1.run === c2.run)

console.log(Parent.prototype) // {run: ƒ, constructor: ƒ}

Child.prototype.sing = function(){}

console.log(Parent.prototype) // {run: ƒ, sing: ƒ, constructor: ƒ}

```

Parent类原型上本来只有一个方法run

在子类Child原型上新增加一个sing方法,这个时候打印父类原型,可以看到父类的原型上面也有了sing方法。

因为是引用类型,指向的是同一块内存地址,所以当子类在原型上面添加其它属性时,那么父类上面也会同样增加该属性,这并不是我们想要的。

#### Class继承

Class 可以通过extends关键字实现继承

#### 代码实现

```js

class Parent {}

class Child extends Parent {}

```

这里面就不过多的描述class的使用方式了,感兴趣的可以看看这个[传送门](es6.ruanyifeng.com/#docs/class…)