不像 Java,JavaScript 中并没有类的概念,因此要想实现继承,必须依靠 JavaScript 中的原型(prototype)机制。
继承的目的是使子类别具有父类别的属性和方法,子类别可以重新定义某些属性,也可以重写某些方法,子类别也可以添加新的属性和方法。
子类别可以继承父类别的属性和方法,但不是所有的属性和方法,因此我们做以下约定:
|
|
只有父类别的实例成员才可以被继承,私有成员是不能被继承的。
接下来介绍实现继承的六种方法:构造函数继承、原型链继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。
构造函数继承
实现原理: 将父对象的构造函数绑定在子对象上,父对象中的所有实例属性和方法复制到子对象,没有用到原型链。
|
|
在父对象 Animal 的构造函数中定义了 species 和 age 两个属性,和 sayAge 方法,在子对象 Dog 中调用 call() 或 apply() 方法使父对象的构造函数绑定到子对象上,子对象拥有父对象的所有实例属性和方法,Dog 对象的一个实例 teddy 也拥有其构造函数 Dog 上的所有属性和方法(包括 Dog 从 Animal 中继承过来的)。
优点:
- 子对象可以向父对象传递参数,如上述代码中的 age
- 可以实现多继承,即多次调用 call()/apply() 方法
- 父对象的属性和方法只是被复制,不被共享,避免了污染
缺点:
- 方法都是定义在构造函数中,无法被复用,继承时,每个子对象都有父对象方法的副本,造成内存的浪费
- 子对象只能继承父对象构造函数中的属性和方法,无法继承其原型对象中的属性和方法
|
|
比如在父对象 Animal 的原型对象中添加一个 sleep 方法和 head 属性,子对象 Dog 的实例 teddy 是无法继承这些属性和方法的。
|
|
但是在 Dog 的原型对象上定义的属性和方法,teddy 还是可以使用的,这主要还是因为 teddy 是 Dog 直接 new 出来的实例。
注意:借用构造函数实现的继承与原型链并没有关系。
|
|
可以看到父对象 Animal 和子对象 Dog 两者的原型对象之间并没有关联。
原型链继承
实现原理: 子类的原型对象指向父类的一个实例,实现原型链的连接。
|
|
只是单纯的使用 new 操作符创建一个父类实例:
会有一个问题:
|
|
我们发现子类原型对象的 constructor 属性指向了父类构造函数。更重要的是,每一个实例也有一个 constructor属性,默认调用 prototype 对象的 constructor 属性。
|
|
解决办法: 手动纠正,为 prototype 属性重新赋值。
|
|
优点:
- 实例可以继承子类和父类原型对象中的属性和方法
- 父类原型对象新增属性和方法,子类实例仍可以访问
缺点:
- 无法实现多继承
- 无法向父类构造函数传参
- 对子类原型的修改会反映到父类原型对象
组合继承
组合继承融合了原型链和构造函数的优点,是常用的继承模式。
实现原理: 使用原型链实现原型属性和方法(共享)的继承,使用构造函数实现对实例属性的继承。
|
|
优点:
- 子对象可以向父对象传递参数,可以实现多继承,继承自构造函数中的实例属性不会被污染
- 子对象共享继承自原型链中的属性和方法
缺点: 无论什么时候都会调用两次构造函数。
原型式继承
很多时候我们不一定会有或者说通过构造函数实现继承,可能我们只有两个对象,我们想实现这两个对象的继承。本例参考阮一峰老师的博客
|
|
json 的发明人提出一个 object() 函数:
|
|
在 object() 函数内部,先创建了一个临时的构造函数,将传入对象作为该构造函数的原型对象,最后返回该构造函数的一个实例。实际上就是父对象传入 object() 函数,返回子对象,继承关系还是通过原型链实现的。
|
|
同样的,在父对象的属性和方法的任何修改会反映到子对象上。
|
|
寄生式继承
寄生式继承创建个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
|
|
createAnother() 函数接收了一个参数,即被继承对象。
anotherPerson 是基于 person 创建的一个新对象,新对象不仅具有person的所有属性和方法,还有自己的 sayHi() 方法。但是父对象的引用属性是共享的。
寄生组合式继承
寄生组合式继承主要是对组合继承的改进,在组合继承中,最大的问题就是无论什么时候都会调用两次构造函数。
|
|
为了解决这一问题,寄生组合式继承 通过借用构造函数来继承属性,通过原型链来继承方法。