js 实现继承的几种方式
讲继承之前,还是要搞懂 原型和原型链 理解一下 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
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__
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
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']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
出现这个问题,是因为 skill
是引用数据类型,内存地址是一样的
而 wancai
和 dahuan
都是从 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
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
作用是什么?
- call 用于改变 this 指向的
- 在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 组合继承 优缺点
# 优点
集成了 类式继承
和 构造函数继承
的优点。
- 现在原型链的方法可以找到了
- 对应的属性也已经隔离 (挂载在当前实例的 this 上而不是公用 Animal 的属性)
# 缺点
这么完美的方式,居然还有缺点?!
- 它调用了两次父类的构造函数。(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')
// 示范栗子就不写了,自己打印看下,都是一样的道理了
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
2
3
4
5
6
在这个 demo 中,并不会造成什么大的问题。但他存在一个隐患:
虽然现在用不上 constructor
但并不代表这个没作用
假如我们想给 Dog 方法创建一个 create
方法,生成对象
Dog.prototype.create = function() {
return new this.constructor() // 效果等同于 new Dog()
}
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')
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
。所以如果我们自己写继承的时候,一定要注意这个问题
# 最后总结一下
类式继承
- 第一个是引用类型的缺陷
- 我们无法为不同的实例初始化继承来的属性
构造函数继承
- 那就是我们无法获取到父类的共有方法
组合继承
- 结合了
类式继承
和构造函数继承
的功能 - 但是它调用了两次父类的构造函数
- 结合了
寄生组合式继承
- 结合了上面 3 个的优点,并且排除了他们的缺点
es6 的 class 语法和 extends
- es6 的语法才能用,低版本浏览器还得做兼容处理/es6 转 es5