JavaScript实现继承的几种重要范式

一 原型链

1. 代码示例

function SuperType() {
  this.superProperty = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.superProperty;
}

function SubType() {
  this.subProperty = false;
}

SubType.prototype = new SuperType(); //将 SuperType类型的实例 作为 SubType类型的 原型对象, 这样就重写了SubType的原型对象(没有使用SubType默认的原型对象), 实现了继承。

SubType.prototype.getSubValue = function() {
  return this.subProperty;
}

const subTypeInstance = new SubType();
console.log(subTypeInstance.getSuperValue());//true

详见我的另一篇博客《原型与原型链》 的"二、实现继承的主要范式:原型链
"。

二、 借用构造函数(经典继承)

1.代码示例

function SuperType() {
  this.colors = ['red', 'blue', 'green'];
}

function SubType() {
  SuperType.call(this);//在子类型构造函数内部调用超类型构造函数,继承了SuperType
}

var subTypeInstance1 = new SubType();
subTypeInstance1.colors.push('yellow');
console.log(subTypeInstance1);//['red', 'blue', 'green','yellow']

var subTypeInstance2 = new SubType();
console.log(subTypeInstance2.colors);//['red', 'blue', 'green']

基本思想就是在子类型构造函数内部调用超类型构造函数。

2. 优点

(1)子类型每个实例都会继承一份独立的超类型属性副本

通过使用call方法(或apply),实际上是在未来新创建子类型实例时当场调用了超类型的构造函数,也就是在初始化子类型实例时才把超类型的属性添加到子类型实例上。那么子类型的每个实例都会拥有一份独立的超类型属性副本。 这样不同的子类型实例对同一个继承来的属性进行修改(例如对数组属性进行push),也不会互相影响。

(2)可以在子类型构造函数中向超类型构造函数传递参数
function SuperType(name) {
  this.name = name;
}

function SubType(name) {
  SuperType.call(this,name);
}

var subTypeInstance1 = new SubType('Bonnie');
console.log(subTypeInstance1.name);//"Bonnie"

var subTypeInstance2 = new SubType('Summer');
console.log(subTypeInstance2.name);//"Summer"

3. 缺点

(1)不能做到函数复用:无法避免构造函数模式的问题——使用构造函数模式创建的每个实例都包含着各自独有的同名函数,故函数复用无从谈起。

(2)子类型创建方式限制:而在超类型的原型中定义的方法,对子类而言是不可见的,所以所有子类型都只能通过构造函数模式创建。

三、 组合继承(伪经典继承)

1. 代码示例

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}

function SubType(name, age) {
  //继承属性
  SuperType.call(this, name); //第二次调用超类型构造函数SuperType
  //自己的属性
  this.age = age;
}

//继承方法:SubType.prototype也会得到继承的属性,不过会被上述构造函数中call方法继承的属性作为实例属性覆盖掉。
SubType.prototype = new SuperType(); //第一次调用超类型构造函数SuperType
SubType.prototype.constructor = SubType;//重写prototype会割裂子类型原型与子类型构造函数的关系,故要加上这么一句
//自己的方法
SubType.sayAge = function() {
  console.log(this.age);
}

将原型链和借用构造函数组合起来:使用原型链实现对超类型原型上的方法和属性的继承,使用借用构造函数实现对超类型实例属性和方法的继承。一般超类型原型上就只有方法,超类型实例上只有属性(即 组合使用构造函数模式和原型模式,参见3.4)。这样一来,该方式就是:用原型链实现对超类型原型上方法的继承,用借用构造函数实现对超类型实例上属性的继承。

2. 优点

组合继承用 原型链实现对 超类型原型上方法的继承,用 借用构造函数实现对 超类型实例上属性的继承。这样可以让子类型的不同实例既分别拥有独立的属性(尤其是引用类型属性,如colors数组),又可以共享相同的方法。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,是 JavaScript中最常用的继承模式

3. 缺点

无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型时,一次是在子类型构造函数内部。这样的话,虽然子类型会包含超类型的全部属性,但是对于超类型的实例属性而言,调用子类型的构造函数时会重写一遍这些实例属性。

四、 原型式继承

1. 代码示例

function object(o) {
  function F(){};//先构造一个临时性的构造函数
  F.prototype = o;//将传入的对象作为该构造函数的原型
  return new F();//返回临时类型的新实例
}

//使用
var person = {
  name:'Bonnie',
  friends: ['Summer', 'Spring']
}

var person1 = object(person);
person1.name = 'Huiyun';
person1.friends.push('Tony');

var person2 = object(person);
person2.name = 'Huiyun';
person2.friends.push('Joy');

console.log(person1.friends);//["Summer", "Spring", "Tony", "Joy"]
console.log(person2.friends);//["Summer", "Spring", "Tony", "Joy"]
console.log(person.friends);//["Summer", "Spring", "Tony", "Joy"]

思想: 借助原型可以基于已有的对象(而非类型)创建新对象(而非创建自定义类型)。其实,object对传入其中的对象执行了一次 浅复制

2. 优点

可以基于已有的对象创建新对象,而且还不必创建新类型。适于基于已有对象加以修改得到另一个对象。

在只想让一个对象与另一个对象保持类似,又不想兴师动众创建构造函数的情况下,原型式继承完全可以胜任。

延伸:ES6的Object.create()

该方法规范化了原型式继承,在只传入一个参数的情况下,和上述object达到的效果相同。该方法的第二个参数为新对象额外属性组成的对象,也就是简化了上述object的后续用法。

语法:
Object.create(protoObj, [newPropertiesObj])

参数:

  • protoObj: 新创建对象的原型对象
  • newPropertiesObj:可选。 要添加(或重写)到新对象上的可枚举的实例属性的属性描述符及其名称组成的对象(与Object.defineProperties()的第二个参数相同)。

newPropertyiesObj语法:

{
  prop1Name:{
    value: valueConent,
    writable: false(default)/true
    enumerable: false(default)/true
    configuragle: false(default)/true
  },
  prop2Name: {
    ...
  },
  ...
}

详见https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties

返回值: 一个带有指定的原型对象属性和自己新添加的实例属性的对象

Eg:
var person = {
  name: 'Bonnie',
  friends: ['Summer','Spring']
}

var person1 = Object.create(person, {
  name:{
   value:'Huiyun'
  }
});

console.log(person1);//{name: "Huiyun"}
console.log(person1.friends);// ["Summer", "Spring"]

3.缺点

引用类型属性共享: 该方法基于已有对象创建新对象,但是对新对象修改引用类型属性,已有的基础对象也会受到修改,基于基础对象的其他对象也会受到修改。

五、 寄生式继承

1. 代码示例


function createAnother(original) {
  var clone = object(original);//此处运用了4.4中的objec函数,也可以使用Object.create(original)
  clone.sayHi = function() {
    console.log('Hi');
  }
  return clone;
}

思想:与创建对象的寄生构造函数模式和工厂模式类似,即创建了一个仅用于封装继承过程的函数,并在该函数内部以某种方式来增强对象,最后再像真地是它自己做了所有工作一样返回对象。

2. 优点

在主要考虑基于某个对象而非考虑自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。该模式可以基于已有对象创建添加了函数的新对象。

3. 缺点

(1)不能做到函数复用:不能做到对新添加函数进行函数复用,这样会降低效率。该缺点与 借用构造函数继承类似。

(2)引用类型属性共享:该方法也是基于已有对象创建新对象,但是对新对象修改引用类型属性,已有的基础对象也会受到修改,基于基础对象的其他对象也会受到修改。该缺点 与原型式继承 一样。

六、 寄生组合式继承

1. 代码示例


//其实是寄生式继承的一种应用:以SuperType.prototype为基础对象,创建SubType.prototype对象。这个SubType.prototype对象是SuperType.prototype的浅复制,同时SubType.prototype对象上又增添了额外的属性constructor指向SubType。
function inheritPrototype(SubType, SuperType) {
  var prototype = Object.create(SuperType.prototype);
  prototype.constructor = SubType;
  Subtype.prototype = prototype;
}

//应用
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};

//子类型实例通过借用构造函数继承来获取超类型实例上的属性:
function Subtype(name, age) {
  SuperType.call(this,name);
  this.age = age;
}
//子类型的原型通过寄生式继承来获取超类型原型上的方法:
inheritPrototype(SubType,SuperType);
//给子类型原型添加自己的方法
SubType.prototype.sayAge = function() {
  console.log(this.age);
}

思想: 子类型的实例通过借用构造函数来获取超类型实例上的属性,子类型的原型通过寄生式继承来获取超类型原型上的方法(此处寄生式继承是指子类型原型对超类型原型进行浅复制)。也就是说子类型通过借用构造函数继承属性,通过寄生式继承来继承方法。 和组合式继承相比,该寄生组合式继承不必为了指定子类型的原型而调用超类型的构造函数,只是使用了超类型原型的一个副本;而只有在指定子类型的实例属性时调用了超类型的构造函数(借用构造函数继承);这样该方法就只调用了一次超类型的构造函数。

2. 优点

(1) 属性独立、方法共享:拥有组合式继承的所有优点:分别拥有独立的属性(尤其是引用类型属性,如colors数组),又可以共享相同的方法。

(2) 高效率:避免了组合式继承调用两次超类型构造函数的缺点,只调用一次超类型构造函数,具有高效率。

(3) 原型链不变:能够正常使用instanceof和isPrototypeOf()

该寄生组合式继承是最理想的继承范式。

七、Es6的Class对继承的实现*

参考资料

《JavaScirpt高级程序设计》6.3
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties

原文地址:https://www.cnblogs.com/Bonnie3449/p/9430516.html