JavaScript中的继承方式

借用原型链

ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。它的主要思想就是利用构造函数,原型和实例之间的关系。来实现一个引用类型继承另一个引用类型的属性和方法。我们知道,每个函数都有一个prototype属性,这个属性指向该函数的原型对象。而当这个函数被当作构造函数进行实例化的时候,它的实例内部有一个[[Prototype]]属性,这个属性指向这个构造函数的原型对象。如果将一个构造函数的原型等于另一个实例,那么这个构造函数的原型就会包含一个指向另一个原型对象的指针。这种原型和实例之间的关系,正是我们借用原型链实现继承的基本思想。

举例说明:

function SuperType() {
  this.superType = 'SuperType';
}
SuperType.prototype.getSuper = function() {
  return this.superType;
}

function ChildType() {
  this.childType = ';childType'
}
// 继承了SuperType
ChildType.prototype = new SuperType(); 

let instance1 = new ChildType();

console.log(instance1.getSuper()); // 'SuperType'

上面的例子定义了两个函数SuperTypeChildType,而它们的主要区别是ChildType继承了SuperType。而这个继承的实现是通过创建SuperType的实例并将这个实例赋值给ChildType.prototype实现的

通过原型链继承的问题
原型链很强大,可以用它来实现继承,但是它也偶一些问题,在通过原型实现继承的时候,原型实际上会变成另一个构造函数的实例,那么原先的实例属性就会变成了原型的属性,从而这个属性变会称为共享的属性,下面这个例子会很好的讲述这个问题:

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

let instance1 = new ChildType();

instance1.colors.push('yellow');
console.log(instance1.colors); // ["red", "green", "blue", "yellow"]

let instance2 = new ChildType();
console.log(instance2.colors); // ["red", "green", "blue", "yellow"]

在这个例子中SuperType中定义了一个colors属性,SuperType的每个实例都会包含这个colors属性,当ChildType.prototype成为SuperType的实例的时候,ChildType.prototype中也包含这个colors属性,因此,当我们通过instance1.colors进行操作的时候,引擎会顺着原型链找到ChildType.prototype中的colors属性,因此我们对colors属性进行的操作,也会影响到ChildType的其他实例。因此实践中很少会单独使用原型链进行实现继承。

借用构造函数

借用构造函数有时也被称为伪造对象或经典继承,它的基本思想是在子类型构造函数中使用apply()call()调用超类型(父类型)构造函数。举例说明:

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

function ChildType() {
  SuperType.call(this);
}

let instance1 = new ChildType();

instance1.colors.push('yellow');
console.log(instance1.colors); // ["red", "green", "blue", "yellow"]

let instance2 = new ChildType();
console.log(instance2.colors); // ["red", "green", "blue"]

通过使用call()方法,我们实际上是在新创建的ChildType实例的环境下调用了SuperType,这样就会在新的ChildType的实例上执行SuperType中的初始化代码,这样每个实例都会有自己的colors属性副本。而借助构造函数相比于借用原型链的一大优势就是,子类型构造函数可以向超类型构造函数传递参数。看下面这个例子:

function SuperType(name) {
  this.name = name;
}

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

let instance1 = new ChildType('Nick');

console.log(instance1.name); // 'Nick'

借用构造函数的问题
如果仅仅借用构造函数,那么方法都只能在构造函数中进行定义,那么函数的复用性将不复存在,而且对于子类型构造函数来讲,定义在超类型原型上的方法对于子类型构造函数是不可见的。结果所有的类型都只能使用构造函数模式。因此借用构造函数的这个办法也很少单独使用。

组合继承

组合继承有时候也叫伪经典继承,它是组合了原型链和构造函数两种方式,从而发挥二者之长的一种继承方式。基本思想是,利用原型链实现原型属性和方法的继承,从而通过构造函数来实现实例属性的继承(保证每个实例都有单独的实例属性,而不互相影响),举例说明:

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'green', 'yellow'];
}
SuperType.prototype.getColors = function() {
  return this.colors;
}

function ChildType(name) {
  // 继承属性
  SuperType.call(this, name);
}
// 继承方法
ChildType.prototype = new SuperType();

let instance1 = new ChildType('Nick');
let instance2 = new ChildType('Cherry');
instance1.colors.push('black');

console.log(instance1.name); // 'Nick'
console.log(instance1.colors); // ' ["red", "green", "yellow", "black"]'
instance1.getColors(); // ' ["red", "green", "yellow", "black"]'
console.log(instance2.name); // 'Cherry'
console.log(instance2.colors); //  ["red", "green", "yellow"]

组合继承避免了单独使用原型链和构造函数实现继承时的缺点,时JavaScript中最常用的一种继承方式。

原型式继承

原型式继承是2006年道格拉斯.克罗克福德提出的,他的基本思想是借助原型基于已有的对象创建一个新对象,同时还不必因此创建自定义类型。他给出了如下的函数:

function object (o) {
  function F() {};
  F.prototype = o;
  return new F();
}
let person = {
  name: 'Nick',
  friends: ['cherry', 'july'],
};
let anotherPerson = object(person);
anotherPerson.name = 'lily';
anotherPerson.friends.push('Tom');

let person2 = object(person);
person2.name = 'Jone';
person2.friends.push('Linda'); 

console.log(person.friends); // ["cherry", "july", "Tom", "Linda"]

原型式继承要求你必须有一个对象作为另一个对象的基础,在这个例子中,我们把person对象作为基础,将person对象传入到object()函数中,这样它就会返回另一个新的对象,这个新对象将person作为原型,所以它的原型中就包含一个name属性和一个friends属性。这意味着,person.friends不仅是person所有,也被anotherPerson和person2所共享。

ES5通过新增的Object.create()方法规范化了原型式继承。这个方法接受两个参数:一个用于作为新对象原型的对象和(可选的)为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和上面的object()方法相同。

let person = {
  name: 'Nick',
  friends: ['cherry', 'july'],
};

let person1 = Object.create(person);
person1.name = 'Jhon';
person1.friends.push('cherry');

let person2 = Object.create(person);
person2.name = 'Lily';
person2.friends.push('Bob');

console.log(person.friends); // ["cherry", "july", "cherry", "Bob"]

Object.create()方法的第二个参数和使用Object.defineProperties()方法的第二个参数相同:每个属性都是通过属性描述符添加的

let person = {
  name: 'Nick',
  friends: ['cherry', 'july'],
};

let anotherPerson = Object.create(person, {
  name: {
    value: 'Lily',
  }
})
console.log(anotherPerson.name); // Lily
console.log(person.name); // Nick

在没有必要兴师动众的创建构造函数,而只是想让一个对象与另一个对象保持上面的这种关系,原型式继承是完全胜任的。不过,包含引用类型的属性始终是共享的,就像使用原型链继承一样

寄生式继承

寄生式继承是与原型式继承紧密相连的一种思想,它的基本思路是:创建一个仅用于封装继承过程的函数,在函数内部已某种方式来增强对象,最后在真的像它做了所有工作一样返回对象。示例:

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

function createObject(original) {
  let clone = object(original);
  clone.sayHi = function() {
    console.log('hi');
  };
  return clone;
}

let person = {
  name: 'Nick',
  friends: ['Tom', 'Jhon'],
}
let person1 = createObject(person);
person1.sayHi(); // 'hi'

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种比较有用的模式。使用寄生式继承来为对象添加函数,会由于造成函数不能被复用而降低效率

寄生组合式继承

前面说过,组合继承是JavaScript中的经典继承方式,但是它也有自己的不足。组合式继承最大的问题就是无论什么情况下都会调用两次构造函数:一次是在创建子类型原型的时候,一次是在子类型构造函数的内部。看一下下面的这个示例:

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'yellow', 'blue'];
}
SuperType.prototype.sayName = function() {
  console.lofg(this.name)
}
function ChildType(name, age) {
  SuperType.call(this, name); // 第二次调用SuperType()
  this.age = age;
}
ChildType.prototype = new SuperType(); // 第一次调用SuperType()
ChildType.prototype.constructor = ChildType;
ChildType.prototype.sayAge = function() {
  console.log(this.age);
}

在第一次调用SuperType构造函数的时候,此时ChildType.prototype会获得两个属性:namecolors,它们都是SuperType的实例属性。只不过位于ChildType的原型中,当调用ChildType进行实例化的时候,又会调用一次SuperType函数,这一次又在新对象上创建了namecolors属性。于是这两个属性就会屏蔽掉ChildType原型中的两个同名属性因此就会造成有两组name和colors属性:一组在ChildType的原型中,一组在ChildType的实例中。这就是调用两次构造函数的结果。而解决这个问题的方法就是——寄生式组合继承。

寄生式组合继承的基本模式如下:

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}
function inheritPrototype(childType, superType) {
  let prototype = object(superType.prototype);   // 创建对象
  prototype.constructor = childType;            // 增强对象
  childType.prototype = prototype;             // 指定对象
}

实例中的inheritPrototype完成了寄生组合式继承最简单的形式。它接受两个参数:子类型构造函数超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本,第二步是为创建的副本添加constructor属性,弥补重写原型后constructor属性的丢失。最后一步将新创建的原型副本赋值给子类型的原型。这样我们就可以调用inheritPrototype来替换之前例子中为子类型原型赋值的那一步,例如:

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

inheritPrototype(ChildType, SuperType);

ChildType.prototype.sayAge = function() {
  console.log(this.age);
}
let instance1 = new ChildType('cherry', 20);
instance1.sayName(); // 'cherry'
instance1.sayAge(); // 20

这个例子高效率的体现了它只调用了一次SuperType(),并且因此避免了在ChildType原型上创建不必要的属性。于此同时,原型链还能保持不变,可以正常使用instanceof和isPrototypeof()。普遍认为寄生组合式继承是最理想的继承方式。

你可能感兴趣的:(javascript)