原稿:语雀 · JS继承 · 原目录:JavaScript › JS继承

红宝书第八章写的真的很不错,第四版更新了ES6的继承,这里总结下✍

ES5

在ES5中没有正式的class,所有的类都是用于创建特定类型对象的构造函数,同时有以下事实 2. JS中每个函数都具有一个prototype属性,该属性是一个指向函数原型对象的引用 4. 原型对象会获得constructor属性,该属性是指回函数的引用 6. 每个被构造函数创建的实例有一个属性__proto__,该属性是指向对应原型对象的引用

上面的构造函数和原型构成了JS继承的基础

原型链继承

基本思想:创建父类实例作为子类的原型对象,该原型对象的引用变量和方法可被子类实例共享,从而实现继承

代码说明

// 父类声明
function Vehicle(){
    this.superName = "Vehicle";
    this.colors = ["red","blue"];
}
Vehicle.prototype.getSuperName = function(){
    return this.superName;
}

// 子类声明
function Bus(){
    this.subName = "Bus";
}

// 继承
Bus.prototype = new Vehicle();
// 新建方法
Bus.prototype.getSubName = function(){
    return this.subName;
}

// 子类实例
let bus1 = new Bus();
console.log(bus1.getSuperName() + '-' + bus1.getSubName()); // Vehicle-Bus

优点:父类原型上的方法可以被子类实例共享,做到函数复用

局限: 2. 共享原型对象使得对不同引用值的修改也被共享:因为JS是值传递,对于原始值,每个实例拷贝一份,各自独立;但对于引用值,每个实例拷贝了不同的引用,但都指向内存中一个对象,对象上的更改通过每个引用都能看到(见下方举例) 4. 子类型实例化时不能给父类型构造函数传参

let bus1 = new Bus();
let bus2 = new Bus();

console.log(bus1.superName, bus2.getSuperName());// Vehicle Vehicle
console.log(bus1.colors);// ['red', 'blue']

bus2.superName = "changed";
bus2.colors.push("green");

console.log(bus1.superName, bus2.getSuperName());// Vehicle changed
console.log(bus1.colors);// ["red", "blue", "green"]

盗用构造函数

基本思想:在子类构造函数中调用父类构造函数,使父类构造函数this被替换为子类实例,以此解决引用值共享与向父类传参的问题

代码说明

// 父类声明
function Vehicle(newColor){
    this.superName = "Vehicle";
    this.colors = ["red","blue"];
    this.colors.push(newColor);
}
Vehicle.prototype.getSuperName = function(){
    return this.superName;
}

// 子类声明
function Bus(newColor){
    // 继承
    Vehicle.call(this, newColor)
    this.subName = "Bus";

}

Bus.prototype.getSubName = function(){
    return this.subName;
}

// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");

console.log(bus1);
console.log(bus1.colors);// ["red", "blue", "bus1's color"]
console.log(bus2.colors);// ["red", "blue", "bus2's color"]

// 无法访问父类原型
console.log(bus1.getSuperName());// Uncaught TypeError

优点:可以向父类构造函数传参,且子类实例持有独立的引用值

局限:子类实例无法访问父类原型,因此必须在父类构造函数中通过this定义要继承的方法,再通过子类实例调用,不能重用函数

组合继承

基本思想:使用原型链继承父类上的可共享的属性和方法,通过盗用构造函数继承不希望共享的父类属性

代码说明

// 父类声明
function Vehicle(newColor){
    this.superName = "Vehicle";
    this.colors = ["red","blue"];
    this.colors.push(newColor);
}
Vehicle.prototype.getSuperName = function(){
    return this.superName;
}

// 子类声明
function Bus(newColor){
    // 继承
    Vehicle.call(this, newColor)
    this.subName = "Bus";
}

// 继承
Bus.prototype = new Vehicle();
// 新建方法
Bus.prototype.getSubName = function(){
    return this.subName;
}

// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");
console.log(bus1);
console.log(bus1.colors);// ["red", "blue", "bus1's color"]
console.log(bus2.colors);// ["red", "blue", "bus2's color"]
console.log(bus1.getSuperName());// Vehicle

优点:继承父类的同时既能够做到函数复用,又能实现引用值的独立性

局限:父类构造函数总是会被调用两次,一次子类原型赋值时调用,一次在子类构造函数中调用,因此会在实例和子类原型上各创建一组父类实例属性,产生冗余

原型式继承

基本思想:原型式继承无需自定义类型,可以根据已有的实例创建子类实例,但子类实例间共享信息。其中引入了一个临时构造函数object(),它接受一个对象并返回以此为原型的实例

代码说明

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

// 父类实例
let bus = {
    name: "bus",
    colors: ["red","blue"]
};

// 子类实例
let bus1 = object(bus);
bus1.colors.push("green");
// 子类实例
let bus2 = object(bus);
bus2.colors.push("black");

console.log(bus1.colors); //["red", "blue", "green", "black"]
console.log(bus2.colors); //["red", "blue", "green", "black"]

优点:无需创建自定义类型

局限:子类实例之间会共享引用值,且如果要为子类实例添加函数,无法复用

寄生式继承

基本思想:与原型式继承类似,利用一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象,最后再返回对象

代码说明

function createAnother(original) {
  // 通过调用函数创建一个新对象
  let clone = object(original);
  // 以某种方式增强这个对象
  clone.speedup = function () {
    console.log("speed up!");
  };
  return clone; // 返回这个对象
}

let bus = {
  name: "bus",
  colors: ["red", "blue"],
};

let bus1 = createAnother(bus);
bus1.speedup(); // speed up!

优点:无需创建自定义类型

局限:寄生过程中无法做到函数复用

寄生式组合继承

基本思想:组合式继承存在两次调用父类构造函数的问题。寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。混合式原型链不必为了指定子类原型而调用父类构造函数,只需要一个父类原型副本替换父类实例的角色。本质上,就是使用寄生式继承来继承父类原型,然后再将结果指定给子类型的原型

代码说明 

核心是inheritPrototype(subType, superType) 2. 通过临时构造函数创建父类原型副本 4. 让返回的副本constructor指向子类,否则会指向父类 6. 让子类的prototype指向父类原型副本

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype); // 创建对象,创建父类原型副本
  prototype.constructor = subType; // 增强对象,解决由于重写原型导致constructor失配问题
  subType.prototype = prototype; // 赋值对象,代替之前的子类原型赋值
}

// 父类声明
function Vehicle(newColor){
    this.superName = "Vehicle";
    this.colors = ["red","blue"];
    this.colors.push(newColor);
}
Vehicle.prototype.getSuperName = function(){
    return this.superName;
}

// 子类声明
function Bus(newColor){
    Vehicle.call(this, newColor)
    this.subName = "Bus";
}

inheritPrototype(Bus, Vehicle)
// 新建方法
Bus.prototype.getSubName = function(){
    return this.subName;
}

// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");
console.log(bus1);
console.log(bus1.colors);// ["red", "blue", "bus1's color"]
console.log(bus2.colors);// ["red", "blue", "bus2's color"]
console.log(bus1.getSuperName());// Vehicle

对比组合继承 2. 组合继承子类原型是父类实例;寄生式组合继承子类原型是寄生后父类原型副本,避免在子类原型上创建冗余属性 4. 组合继承中子类原型是父类实例,访问子类原型constructor会沿原型链找到父类构造函数;寄生式组合继承在inheritPrototype中直接修改子类原型constructor指向子类构造函数

小结

原型.svg

ES6

ES6原生支持了class关键字,类实际就是函数,但是类语法明确了存在于实例、原型、类上的成员

  • 实例成员:在constructor()通过this为实例添加的属性,每个实例都有唯一成员对象,所有实例不会通过this定义的属性。
  • 原型成员:为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法
  • 类成员:静态方法使用static关键字,其中的this引用类自身,通常用于执行不特定于实例的操作,不要求存在类实例

继承

  • ES6使用extends关键字继承,可以继承类或者普通的构造函数,其背后依旧基于“寄生式组合继承”

class A extends B{}过程 工具准备:Babel一直只是不断看到大家说extends过程是语法糖,背后还是原型链继承,所以其中到底封装了什么?首先祭出神器Babel,它可以把ES6+的JS代码转为向后兼容的旧版本实现准备好下面的ES6类继承代码,在babel网站上转换class B{ constructor(nam… Front-end Baby👶

  • ES6继承要求子类必须在constructor()中调用super()后才能使用this。因为本质上讲,ES5的继承是先创建子类对象,将之替换为父类的this;然而ES6的继承没有创建子类实例而是继承父类的this对象,然后对其使用this进行加工
// 父类声明
class Vehicle {
  // 类构造函数,其成员不可共享
  constructor(newColor) {
    this.superName = "Vehicle";
    this.colors = ["red", "blue"];
    this.colors.push(newColor);
  }
  // 原型方法,可共享
  getSuperName() {
    return this.superName;
  };
  static locate() {
    console.log("class Vehicle");
  };
}

// 子类声明
class Bus extends Vehicle {
  constructor(newColor) {
    super(newColor);
    this.subName = "Bus";
  }
  getSubName() {
    return this.subName;
  };
  static locate() {
    console.log("class Bus");
  };
}

// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");
console.log(bus1);
console.log(bus1.colors); // ["red", "blue", "bus1's color"]
console.log(bus2.colors); // ["red", "blue", "bus2's color"]
console.log(bus1.getSuperName() + "-" + bus1.getSubName()); // Vehicle-Bus
console.log(Bus.locate()); // class Bus

小结

ES6继承与ES5继承对比 2. ES5的继承是先创建子类对象,将之替换为父类的this

ES6继承父类的this对象,然后在子类对其使用this进行加工 2. ES5原型链中子类原型是父类实例,子类构造函数和父类构造函数无直接关系,因此需要使用call()

ES6具有双重继承关系:除了原型对象间继承,子类本身是父类构造的实例

下面对比ES6继承和ES5寄生式组合继承:

image.png
image.png
ES6.svg

参考链接:

详解ES5和ES6的继承 - Annikaa - 博客园 ES5继承 构造函数、原型和实例的关系:每一个构造函数都有一个原型对象,每一个原型对象都有一个指向构造函数的指针,而每一个实例都包含一个指向原型对象的内部指针, 原型链实现继承 基本思想:利用原型让一 博客园ES6的继承 ES5的继承的实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。另一个需要注意的地方是,在子类的构造函数中,只有调用super之后才可以使用this关键字,… SegmentFault