JS: 继承

继承

记录一下 javascript 的各种继承方式,个人用得比较多的还是原型链继承和 ES6 的 extends。

原型链继承


// 原型模式
function Parents() {
  this.name = 'Parents';
}
Parents.prototype.getName = function() {
  return this.name;
}

// 原型链继承
function Child() {}
Child.prototype = new Parents();
Child.prototype.constructor = Child;

let son = new Child();
son.getName(); // Parents;

缺点:

  • 在创建 Child 的实例时,无法向 Parents 传参
  • 父类里面的引用类型被共享,个例修改导致所有实例都被修改
function Parents() {
  this.name = ['dad', 'mom'];
}

function Child() {}
Child.prototype = new Parents();
Child.prototype.constructor = Child;

// 无法向 Parents 传参
let son = new Child();
let daughter = new Child();
// 父类里面的引用类型被共享
son.name == daughter.name; // true
// 个例修改导致所有实例都被修改
son.name[0] = 'father';
daughter.name[0]; // "father"

借用构造函数


为了解决上面的问题 ,经典继承方式被设计出来:

// 构造函数模式
function Parents(childName) {
  this.childName = childName;
  this.name = ['dad', 'mom'];
  this.getName = function() {
    return this.name;
  }
}

// 借用构造函数继承
function Child(name) {
  Parents.call(this, name);
}

// 可以向 Parents 传参
let son = new Child('son');
son.childName; // "son"

// 父类里面的引用类型不会被共享
let daughter = new Child('daughter');
son.name[0] = 'father';
daughter.name[0]; // "dad"

缺点:方法都在构造函数里定义,每次创建 Child 实例,都要重新创建一次方法,函数无法得到复用。

组合继承


为了弥补上面的缺点,于是乎组合继承诞生了。

// 组合模式
function Parents(childName) {
  this.childName = childName;
  this.name = ['dad', 'mom'];
}
Parents.prototype.getName = function() {
  return this.name;
}

// 组合继承
function Child(name) {
  Parents.call(this, name);
}
Child.prototype = new Parents();
Child.prototype.constructor = Child;

let son = new Child('son');
son.name[0] = 'father';

let daughter = new Child('daughter');
daufhter.name[0]; // "dad"

因为原型链能保持不变,所以instanceOfisProtypeOf()也能识别基于组合继承创建的对象。

原型式继承


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

这种方式就是在 obj 函数内部,创建一个临时性的构造函数 F(),然后将传入的对象作为这个构造函数的原型,最后返回这个临时类的实例。

其实就是对传入的对象进行一次浅复制,类似于 ES5 的 Object.create() 方法。

Object.create(proto, [propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

proto: 新创建对象的原型对象。

propertiesObject: 可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不 是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties() 的第二个参数。

寄生式继承


function createObj (o) {
    var clone = Object.create(o);
    clone.sayHi = function () {
        console.log('hi');
    }
    return clone;
}

这种方式就是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回增强过的对象。本质上还是对传入的对象进行一次浅复制。

寄生组合式继承


这种方式得先创建一个用于封装继承过程的函数。

function inheritPrototype(child, Parents) {
  let prototype = Object.create(Parents.prototype);
  prototype.constructor = child;
  child.prototype = prototype
}

然后用上面的函数实现继承:

function Parents(childName) {
  this.childName = childName;
  this.name = ['dad', 'mom'];
}
Parents.prototype.getName = function() {
  return this.name;
}

function Child(name) {
  Parents.call(this, name);
}

inheritPrototype(Child, Parents);

其实和组合继承差不多,只是封装了一个 inheritPrototype 函数使得在继承的时候只在 Child 里调用一次 Parents 构造函数。

ES6 的 extends


ES6 引入了 class,可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。但需要注意的是,ES6 的 class 只是语法糖,本质上还是 ES5 的通过修改原型链实现继承的方法,具体可以去babel 试用来看看 babel 对于 class 的转换。

class Parents {
  // 构造函数
  constructor(name) {
    this.name = name;
  }
  // 静态方法
  static getName() {
    return this.name;
  }
  // 方法
  printName() {
    console.log(this.name);
  }
}

class Child extends Parents {
  constructor(name, childName) {
    // 调用父类的constructor(name)
    super(name);
    this.childName = childName;
  }
  printName() {
    super.printName();
    console.log(this.childName);
  }
}

let c = new Child('mom', 'son');
c.printName();// mom son

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

参考


阮一峰_Class 的继承

《javascript高级程序设计》(三)

posted @ 2019-03-02 16:42  郭佬  阅读(322)  评论(0编辑  收藏  举报
我终究成长为一个不特别的人