Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

继承

1.继承的 6 种方式

原型链继承

  • 父子类型定义

    【父类型 Person】
    function Person(){
        this.sleep = function(){
            console.log("Person Sleep")
        }
    }
    
    Person.prototype.getName = function(){
        console.log("Person Name")
    }
    
    【子类型 Student】
    function Student(){
    }
    
  • 继承写法(覆盖原型对象,并生成新原型对象)

    关键是,子类型的原型为父类型的一个实例对象,即 私有 变为 公有

    Student.prototype = new Person();
    
  • 再写子类型自己的原型方法:

    可覆盖同名的父类型的实例对象上的实例方法

    Student.prototype.getScore = function () {
      console.log("Student Score");
    };
    
    var s1 = new Student();
    
  • 这样子操作

    • 不仅有自己的原型方法s1.getScore()

    • 也能向上一层,继承到父类型的实例对象上的实例方法s1.sleep()

    • 也能向上两层,继承到父类型的原型方法,s1.getName()

  • 相当于沿着原型链一路往上找方法

    • 通过s1.__proto__ 可以访问到原型,即 Person 的实例对象,调用到实例方法

    • 通过s1.__proto__.__proto__ 可以访问到原型的原型,即 Person 的原型,调用到原型方法

    • 注意:

      • 父与子中,引用类型会联动变化
      • 在子类中添加新方法或重写父类方法时,一定要在“继承写法”之后去写
  • 我们

    • s1.__proto__为隐式原型,每个实例都有,是私有属性
    • Student.prototype为显式原型,每个类都有
    • 二者是===全等的,指向对应的是同一个
  • 所以,

    • 当实例调用属性或方法时:
      • 先在自身里面寻找,
      • 找不到再从隐式原型__proto__中寻找,也就是对应类的显式原型中寻找
  • 缺点:

    • 多份子类型实例会共享同一份父类型中的实例属性(致命缺陷)
    • 创建子类型实例时,无法向父类构造函数传参(只能在改变原型时传一次)
    • 无法实现多继承
  • 相当于

    • 夺取了父类构造函数中定义的实例方法到其原型中
    • 夺取了父类的原型方法到其原型的原型中

借用构造函数继承

  • 父子类型定义同上

    父类型 Person
    
    子类型 Student
    
  • 继承写法

    关键是,子类型构造函数中,通过 call 调用父类型构造函数

    function Student() {
      Person.call(this, 给父类型填的参数);
    }
    
    var s1 = new Student();
    
  • 优势: 解决了原型链继承的三大痛点

    • 不用共享一份父实例属性了
    • 可以向父类构造函数传参数了
    • 可以实现多继承了(call 多个父类对象)
  • 出现新的缺点:

    • 只能继承父类中的实例方法和属性(即私有的),而原型方法和属性(即公有的)无法得到
    • 每次 new 子类型都要创建一个父类型实例函数的副本,无法复用函数,有些浪费
  • 相当于

    • 夺取了父类构造函数中的实例方法到子类中
    • 无法夺取原型方法

上面两种方式组合继承

  • 关键是三点

    • 模仿原型链继承,new 父类型()
    • 模仿构造函数继承,父类型.call()
    • 还要修复构造函数指向(修复语句如下)
    Student.prototype.constructor = Student;
    
  • 优势: 解决了构造函数继承的两大痛点

    • 两者结合,能得到实例和原型的所有属性方法
    • 函数可复用,只要写成原型函数即可
  • 又出现新的缺点:

    • 会调用两次父类型构造函数
      • 一次是 new 父类型()时
      • 一次是父类型.call()时,生成两份实例

组合继承的优化 1-简单版

  • 关键是:父类型.call()的基础上,不用实例化(new)父类型构造函数,直接把 new 父类型()换成父类型原型

    • 指向语句
      Student.prototype = Person.prototype;
      
    • 修复语句
      Student.prototype.constructor = Student;
      
  • 优势: 解决了生成两份实例的痛点

  • 又出现新的缺点

    • instanceof 没法辨别实例到底是父类还是子类创造的,因为原型链上两者都有
  • 相当于

    • 夺取了父类构造函数中的实例方法到子类中
    • 夺取了父类的原型方法到原型中

组合继承的优化 2-正式版,寄生组合继承(较完美的方法)

  • 关键是:父类型.call()的基础上,不用实例化(new)父类型构造函数,直接把 new 父类型()换成“借用对象创建语句得到的原型实例”,再加上修复构造函数指向语句

    这样就避免了 new 在调用构造函数时产生的多一份副本

    • 对象创建语句得到原型实例
      Student.prototype = Object.create(Person.prototype);
      
    • 修复语句
      Student.prototype.constructor = Student;
      
  • 注意

    • 跟方法 1 的差别是,Person 上的原型方法,不再跟 Student 上的原型方法混合在同一层级,而是 Person 更加往上一级(详情可以看下面测试图最后的输出)
    • 同样无法辨别实例是父类还是子类创造的

以上是 ES5 继承

实质是先创造子类的实例对象 this,然后再将父类的实例方法添加到 this 上面,再把原型方法加入到子类的原型链上

2. ES6 继承

更先进,利用 class 和 extends 关键字

实质是先将父类实例对象的方法属性加到 this 上面(通过 super 方法),然后再用子类构造函数继承和修改 this

class Student extends Person{
    constructor(name,age,score){
        //先调用父类的构造方法
        super(name,age)
        //再补充自己的属性
        this.score = score
    }
}

非常好,简单易懂操作方便,但不是所有浏览器都支持 ES6

注意,class 只是语法规范,由 ECMA 委员会发布,并不规定具体实现,上面所说的都是 V8 引擎的实现方式,当然这是主流的