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;
}
Comments
Write a Comment