js 实现继承的几种方式

6/27/2020 Javascript

讲继承之前,还是要搞懂 原型和原型链 理解一下 new 的原理:new 一个对象的时候发生了什么

原型链图镇楼,也方便后面理解

实现继承的几种方式:

# 类式继承




 







 



function Animal(color) {
  this.color = color
  this.home = '宠物店'
  this.skill = ['running', 'eat', 'sleep'] // 留意这一行,后续会说
}
Animal.prototype.run = function() {
  console.log(this.name + ' is running')
}
function Dog(name) {
  this.name = name
}
Dog.prototype = new Animal('white') // 留意*2 后面会说
var wancai = new Dog('旺财')
wancai.run() // 旺财 is running
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 为什么 B.prototype = new A() 就可以实现继承?

结合上面的代码和根据原型链查找的规则。我们的 wancai 对象如果希望继承到 Animal。那就必须原型链查找的时候可以找到Animal
wancai 的原型链:

wancai.__proto__ === Dog.prototype // true 这是最基础的原型链
// 根据原型链查找规则 得出 👇
wancai.__proto__.__proto__ === Dog.prototype.__proto__
1
2
3

如果需要从Animal继承,那就是 Dog.prototype.__proto__ 需要指向 Animal.prototype 如何找到 Animal.prototype ?


 


 



 

var a = new Animal()
a.__proto__ === Animal.prototype // true

// 于是
Dog.prototype = new Animal()
// 可以得出
Dog.prototype.__proto__ === Animal.prototype // true
// Dog.prototype === wancat.__proto__
wancat.__proto__.__proto__ === Animal.prototype // true
1
2
3
4
5
6
7
8
9

这样推导,是不是结合起来就可以得出:wancai.__proto__.proto__ === Animal.prototype
原型链已经形成,自然 wancai 就可以继承 Animal 的属性和方法,那继承就已经初步实现了~

# 类的继承的缺点

1. 第一个是引用缺陷

首先得知道哪些是引用类型 引用数据类型
Array 就是一个引用数据类型、包括我们常用的 Object 也是引用类型

引用类型有什么问题?看一个例子








 

 



 

 

// function Animal ... 忽略不写
// function Dog ...
var wancai = new Dog('旺财')
var dahuan = new Dog('大黄')
console.log(wancai.skill) // ['running', 'eat', 'sleep']
console.log(dahuan.skill) // ['running', 'eat', 'sleep']

wancai.skill.push('play ball') // 为旺财添加一个技能
console.log(wancai.skill) // ['running', 'eat', 'sleep','play ball']
// 大黄的技能也新增了
console.log(dahuan.skill) // ['running', 'eat', 'sleep','play ball']

// 再来一个问题。假如大黄不会跑步,只能走路
dahuan.skill[0] = 'walk'
// wancai 的 1技能也被改成走路了?
console.log(wancai.skill) // ['walk', 'eat', 'sleep','play ball']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

出现这个问题,是因为 skill 是引用数据类型,内存地址是一样的 而 wancaidahuan 都是从 Animal 继承而来,用的同一个 skill。所以无论是那个实例修改,都会影响另外的实例

怎么避免这个问题?使用函数返回一个新的对象,而不是用引用类型的数据
引用类型缺陷在 vue 上体验的是最淋漓尽致的。具体可以看 vue 组件的 data 为什么必须是函数

# 2. 第二个是我们无法为不同的实例初始化继承来的属性

在上面例子的 Animal中,我们可以接收一个 color 类型(标识动物的颜色)。 可是我们 new 出来所有的 dog 对象,都只能是white并不能为旺财指定黑色的毛,而大黄执行黄色的毛

# 构造函数继承

构造函数继承方式可以避免类式继承的缺陷:

function Animal(color) {
  this.color = color
  this.skill = ['running', 'eat', 'sleep']
}

Animal.prototype.home = '宠物店'
Animal.prototype.skill_1 = function() {
  console.log(this.name + ' is ' + this.skill[0])
}

function Dog(name, color) {
  this.name = name
  Animal.call(this, color)
}

var wancai = new Dog('旺财', 'black')
var dahuan = new Dog('大黄', 'yellow')

wancai.skill.push('play ball')
dahuan.skill[0] = 'walk'

console.log(wancai.skill) // ['running', 'eat', 'sleep','play ball']
console.log(dahuan.skill) //['walk', 'eat', 'sleep']

wancai.home // undefined
dahuan.home // undefined

wancai.skill_1() // Uncaught TypeError: dahuan.skill is not a function
dahuan.skill_1() // Uncaught TypeError: dahuan.skill is not a function
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

#Dog 中的 Animal.call作用是什么?

  1. call 用于改变 this 指向的
  2. 在 new 的过程中,实际上是把 Dog 函数执行了一次,在 Dog 中调用 Animal 的方法,然后 Animal 方法内部把各种对应的属性(color,skill 之类的)挂载到了 this中,注意现在 Animal 里面的 this 实际上是对象的实例的 this。不了解的可以看下 new 相关的介绍,在新建实例的时候,Dog 对象的 this 指向已经改变,然后在 Dog 内部在用 call 改变了 Animal 指向,所以属性都挂载到了对应实例的 this 对象上

# 构造函数继承的缺点

前面说到 构造函数继承 就是避免了类的继承的缺点(引用类型问题,无法初始化继承来的属性)
但也不是完美的解决方案

# 1. 那就是我们无法获取到父类的共有方法

可以看到调用 skill_1 是不存在的方法。获取 Animal 原型链上的 home 属性也是不存在的。所以说这不是真正意义的继承

只是通过改变 this 指向的方法,获取Animal中的属性,挂载到自己的 this 中。

# 组合继承

组合继承 = 类式继承 + 构造函数继承
来实现一下:











 

 





function Animal(color) {
  this.color = color
  this.home = '宠物店'
  this.skill = ['running', 'eat', 'sleep']
}
Animal.prototype.run = function() {
  console.log(this.name + ' is running')
}
function Dog(name, color) {
  this.name = name
  Animal.call(this, color)
}
Dog.prototype = new Animal()
var wancai = new Dog('旺财', 'black')
var dahuan = new Dog('大黄', 'white')
wancai.run() // 旺财 is running
wancai.color // black
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 组合继承 优缺点

# 优点

集成了 类式继承构造函数继承的优点。

  • 现在原型链的方法可以找到了
  • 对应的属性也已经隔离 (挂载在当前实例的 this 上而不是公用 Animal 的属性)

# 缺点

这么完美的方式,居然还有缺点?!

  1. 它调用了两次父类的构造函数。(Animal 方法被调用了 2 次)

    一次绑定 this 的值,一次挂载原型链方法

# 寄生组合式继承

寄生组合式继承强化的部分就是在组合继承的基础上减少一次多余的调用父类的构造函数
怎么减少?回想类式继承中的一个 原型链分析
可以发现 B.prototype = new A() 这个步骤无非就是想达到:B.prototype.__proto__ === A.prototype__proto__ 也不是我们想创建就创建的,所以我们需要用到 Object.create() 可以看下 Object.create()的定义 (opens new window)

来写一个实现的栗子












 

 
 






function Animal(color) {
  this.color = color
  this.home = '宠物店'
  this.skill = ['running', 'eat', 'sleep']
}
Animal.prototype.run = function() {
  console.log(this.name + ' is running')
}

function Dog(name, color) {
  this.name = name
  Animal.call(this, color)
}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog

var wancai = new Dog('旺财', 'black')
var dahuan = new Dog('大黄', 'yellow')

// 示范栗子就不写了,自己打印看下,都是一样的道理了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 为什么要 Dog.prototype.constructor = Dog

这是前面一直都存在可是没有讲的问题!其实每次赋值都是必须进行这步的操作

观察这 2 段代码:原型上的构造函数应该是等于自身的函数的,很明显 Dog 的构造函数被改写了!那这会造成什么问题?

  • 构造函数观察
function Animal() {}
console.log(Animal.prototype.constructor === Animal) // true

function Dog() {}
Dog.prototype = Object.create(Animal.prototype)
console.log(Dog.prototype.constructor === Dog) // false
1
2
3
4
5
6

在这个 demo 中,并不会造成什么大的问题。但他存在一个隐患:
虽然现在用不上 constructor 但并不代表这个没作用
假如我们想给 Dog 方法创建一个 create方法,生成对象

Dog.prototype.create = function() {
  return new this.constructor() // 效果等同于 new Dog()
}
1
2
3

如果一开始我们就没重置 constructor 状态,上面的 create 方法就会有问题,尤其是大型项目中,很多时候设计了功能我们得把一些小尾巴收掉,不然别人来使用这个方法的时候,很有可能就会莫名掉进某一个坑中,这就不好了

# extends 继承

class 和 extends 是 es6 新增的。class 创建一个类,extends 来实现继承。来一段 demo 熟悉下














 







class Animal {
  constructor(color) {
    this.color = color
    this.skill = ['running', 'eat', 'sleep']
  }

  run() {
    console.log(this.name + ' is ' + this.skill[0])
  }
}

class Dog extends Animal {
  constructor(name, color) {
    super(color)
    this.name = name
  }
}

var wancai = new Dog('旺财', 'black')
var dahuan = new Dog('大黄', 'yellow')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

super 在继承中是必须调用的。通过 super 调用 Animal 的 constructor 方法,实现初始化的

super 方法

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。

感兴趣的还可以把 class 的 demo 复制到 ES6 转 ES5 的网页中,转换一下,看下 class 是如何工作的 babeljs (opens new window)。其中可以看到很多私有方法,都是要用到 constructor。所以如果我们自己写继承的时候,一定要注意这个问题

# 最后总结一下

  1. 类式继承

    • 第一个是引用类型的缺陷
    • 我们无法为不同的实例初始化继承来的属性
  2. 构造函数继承

    • 那就是我们无法获取到父类的共有方法
  3. 组合继承

    • 结合了 类式继承构造函数继承 的功能
    • 但是它调用了两次父类的构造函数
  4. 寄生组合式继承

    • 结合了上面 3 个的优点,并且排除了他们的缺点
  5. es6 的 class 语法和 extends

    • es6 的语法才能用,低版本浏览器还得做兼容处理/es6 转 es5
Last Updated: 5/9/2021, 11:13:04 PM