JS 继承的6+1种方案
6+1 就是原型链继承、构造函数继承、组合继承、原型式继承、寄生继承、寄生组合继承、再加上class的 extend 继承这么几种方法,我们逐一分析一下。
在开始之前,先明确一个共识:我们期望的继承能达到这样的效果:
- 每个子类都复用父类的方法,(方法共享)
- 每个子类都有从父类那里继承来,但是却属于自己的属性。(属性不共享)
例如对于如下的一个父类 Person ,我们希望继承这个父类而得到的子类 Student,Student 的方法 getName 是复用
// 父类 Person
function Person(name){
this.name = name
this.getName = function(){
return this.name
}
}
//子类 Student
const Student 继承自 Person
// 子类 Student 的实例 p1,p2
const p1 = new Student('李雷')
const p2 = new Student('韩梅梅')
//每个子类都复用父类的方法:
// p1.getName === p2.getName //true
// 子类实例的属性是自己的,不是继承来的
// p1.hasOwnProperty('name') //true
// p2.hasOwnProperty('name') //true
原型链继承
借助 js 原型链特性实现的继承
优点:父类的方法在多个子类间是复用的
缺点:
- 父类的引用属性被所有子类共享
- 子类构建时不能向父类传递参数
实现方式
// 父类
function Person() {}
// 子类
function Student(){}
Student.prototype = new Person()
这种一目了然的继承方式在各个方面干的都不错,但是一个我们无法容忍的缺陷:「父类的引用属性被所有子类共享」
举个例子
// 父类
//假如人类被创造时,唱和跳的技能都是 50分
function Person() {
this.skills= {
sing:50,
jump:50,
}
}
// 子类
//一个平平无奇的学生子类
function Student(){}
Student.prototype = new Person()
//此时我们有两个学员c,d, 都是学生
var c = new Student()
var d = new Student()
// c 学生 唱跳天赋极高,经过学习提升到了 100; d 则什么都没学
c.skills.sing = 100
c.skills.jump = 100
//两年半过后,需要考核一下两个人的技能
console.log(c.skills.sing) // 100
console.log(c.skills.jump) // 100
console.log(d.skills.sing) // 100
console.log(d.skills.jump) // 100
//居然发现 d 虽然什么都没做,居然也得到了 100 分的好成绩,这是我们不能容忍的
这就叫「父类的引用属性被所有子类共享」
要想让父类的引用属性不在子类间共享,就要做点优化,比如「借用构造函数」
Tips:所有涉及到原型链继承的继承方式都要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType,不过为了让行文更易读,就不每次都写该表达式了,但不代表这一点不重要。
SubType.prototype.constructor = SubType;
构造函数继承
优缺点和原型链继承完全反过来
优点:
* 父类的引用属性不会被共享
* 子类构建实例时可以向父类传递参数
缺点:父类的方法不能在多个子类间复用(方法是个函数,函数是引用属性 => 引用属性不能被共享就意味着方法不能被共享)
原理:在子类中执行了一遍父类的代码,也就是说每实例化一个子类,就复制了一遍的函数代码;有点浪费
举个例子
function Person() {
this.skills= {
sing:50,
jump:50,
}
}
function Student(){
Person.call(this)
}
// 这样一来,我们再试试上面的例子
//此时我们有两个学员c,d, 都是学生
var c = new Student()
var d = new Student()
// c 学生 唱跳天赋极高,经过学习提升到了 100; d 则什么都没学
c.skills.sing = 100
c.skills.jump = 100
//两年半过后,需要考核一下两个人的技能
console.log(c.skills.sing) // 100
console.log(c.skills.jump) // 100
console.log(d.skills.sing) // 50
console.log(d.skills.jump) // 50
达到了我们想要的效果,但是它也有一个无法容忍的缺点:「父类的方法在不能再多个子类间是复用」
举例解释一下
// 父类
// 人类有一个基础方法:打招呼;
function Person(name) {
this.sayHi = function(){
console.log('Hi')
}
}
// 子类
//一个平平无奇的学生子类1和学生子类2,我们分别用原型链和构造函数来实现对 Person 父类的继承
// Student1 原型链继承
function Student1(){
}
Student1.prototype = new Person()
// Student2 构造函数继承
function Student2(){
Person.call(this)
}
var a = new Student1('')
var b = new Student1('')
console.log( a.sayHi === b.sayHi ) // true
var c = new Student2('')
var d = new Student2('')
console.log( c.sayHi === d.sayHi ) // false
我们看到,c和d 都是「通过构造函数继承的函数」生成实例;c 和 d 虽然都有 SayHi
方法,但是却并不是同一个方法,这就叫「父类的方法在不能再多个子类间是复用」
(而 a 和 b 是原型链继承来的函数,)
所以我们要使用继承时,应该选择原型链继承还是构造函数继承呢?
小孩子才做选择,成年人当然是都要!
那怎么把原型链继承和构造函数继承的优点组合起来呢?
「组合继承!」
组合继承
优点:
- 父类的方法可以被复用
- 父类的引用属性不会被共享
- 子类构建实例时可以向父类传递参数
缺点:
调用了两次父类的构造函数,第一遍是在原型继承的时候实例化父类, 第二遍是在子类的构造函数里面借用父类的构造函数。
回顾一下:原型链继承的缺点是引用类型属性会被共享,那我们就让引用类型的属性使用构造函数继承;构造函数继承的缺点是:由于方法也是引用类型,所以方法没有被子类共享,那我们让方法使用原型链继承。
把缺点有解决掉,剩下的不就都是优点了嘛!
结论:各取长处;属性(普通类型 + 引用类型)用构造函数继承,方法用原型链继承
// 父类
function Person(){
this.skills = {
sing:50
}
this.sayHi = function(){
console.log('hi')
}
}
// 平平无奇的学生子类
function Student(){
Person.call(this) // 构造函数继承(继承属性)
}
Student.prototype = new Person()// 原型链继承(继承方法)
这就是组合继承啦,我们来试试看是不是两种缺点都解决了
var a = new Student()
var c = new Student()
console.log(a.sayHi === c.sayHi)// true 说明构造函数「父类方法不能再子类间共享」的缺点已经解决了
// c 努力提升自己技能
c.skills.sing = 100
console.log(a.skills.sing) // 50
console.log(c.skills.sing) // 100
// 看来原型链继承中,「父类引用类型的属性被所有子类共享」的缺点也解决了
但是这样又引入了新的缺点:调用了两次父类的构造函数,第一次给子类的原型添加了父类的name, arr属性,第二次又给子类的构造函数添加了父类的name, arr属性,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。
原型式继承
和原型链继承名字相似,原理相同
本质:原型式继承的object方法本质上是对参数对象的一个浅复制。
优点:父类方法可以复用
缺点:
父类的引用属性会被所有子类实例共享
子类构建实例时不能向父类传递参数
function myCreate(o){
function F(){} //创建一个空构造函数F
F.prototype = o; 修改 prototype 指向
return new F(); 返回 F 的实例
}
var Person = {
name: '?',
getName:function(){console.log(this.name)}
}
var p1 = myCreate(Person)
要说和原型链继承有什么不同,那就是 myCreate 函数接收的参数不一定要是构造函数,也可以是其他任何对象, 这样我们就相当于是浅复制了一个对象.
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。
所以上文中的 myCreate
方法我们不必自己实现,直接这么写即可
var Person = {
name: '?',
getName:function(){console.log(this.name)}
}
var p1 = Object.creat(Person)
寄生继承
寄生式继承其实就是在原型式继承的基础上,做了一些增强
本质:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
优缺点:实例对象的引用类型属性无法共享
function createOther(proto){
function F () {}
F.prototype = proto
let f = new F()
f.say = function() {
console.log('I am a person')
}
return f
}
因为是给浅靠背的对象加能力,所以这样做的缺点就类似构造函数继承一样,实力对象的引用类型都是各自的,无法共享
寄生组合继承
通常寄生组合继承被认为是引用类型最理想的继承范式
之前组合继承提到了它的缺点:父类构造函数里面的代码会执行两遍
解决这个缺点的方法就是借鉴寄生继承
下面同时列出这两种继承
//组合继承
function Person(){}// 父类
function Student(){
Person.call(this) // 构造函数继承(继承属性)
}
Student.prototype = new Person()// 原型链继承(继承方法)
//寄生继承
function Person(){}
function Student(){
Person.call(this)
}
inherit(Student, Person)
function inherit(sub, super){
let prototype = Object.create(super.prototype)
prototype.constructor = sub
sub.prototype = prototype
}
我们用 inherit
函数替换了 Student.prototype = new Person()
,从而避免了执行 new Person()。
ES6 的 class extend
ES6继承是一种语法糖,结果和寄生组合继承相似。
但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
用法:
class A {}
class B extends A {
constructor() {
super();
}
}
原理
class A{}
class B{}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
Tips
Object.setPrototypeOf()
方法是 ECMAScript 6最新草案中的方法,用于修改 __proto__
在ES5 的版本时,通常以如下方法实现
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}