【For Interview】es5实现继承

108 阅读5分钟

前言:面试被锤一定要会的茴香豆的六种写法, 整理自红宝书
这里循序渐进展开,中心思想是: 利用原型让一个引用类型继承另一个引用类型的属性和方法

原型链

function ParentType() {
  this.property = true;
}

Parent.prototype.getParentValue = function() {
  return this.property;
}

function SonType() {
  this.sonProperty = false;
}

// son继承parent
SonType.prototype = new ParentType();
SonType.prototype.getSonValue = function() {
  return this.sonVaule;
}

var instance = new SonType();
alert(instance.getParentValue()); // true

重写son的原型对象,代之以一个Parent的实例,这样原来存在于parent的属性和方法也存在于Son.prototype中了

判断 实例和原型的关系

  • 方法一: instanceof
alert(instance instanceof Object); // true
alert(instance instanceof ParentType); // true
alert(instance instanceof SonType); // true
  • 方法二: isPrototypeOf()
alert(Object.prototype.isPrototypeOf(instance)); // true
alert(ParnetType.prototype.isPrototypeOf(instance)); // true
alert(SonType.prototype.isPrototypeOf(instance)); // true

纯原型链的 问题

  1. 引用类型带来的问题,其实在创建类那一章节就讲到了
function ParentType() {
  this.colors = ['red', 'green'];
}

function SonType() {
}

SonType.prototype = new ParentType();

var instance1 = new SonType();
instance1.colors.push('blue');
alert(instance1.colors); // ['red', 'green', 'blue']

var instance12 = new SonType();
alert(instance2.colors); // ['red', 'green', 'blue']

相当于SonType的prototype,用了同一个数组,这个数组是ParentType.colors的一个实例。然后就是SonType的所有实例都会共享这个引用属性,所以才会造成变更一个Son实例,另外一个引用属性也会变更
2. 创建子类型实例时,不能像超类型的构造函数传递参数。

借用构造函数

或者叫做伪造对象经典继承

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

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

var instance1 = new SonType();
instance1.colors.push('blue');
alert(instance1.colors); // ['red', 'green', 'blue']

var instance12 = new SonType();
alert(instance2.colors); // ['red', 'green']

新增的那一行实际上是在调用了Parent的构造函数,这样就在SonType对象执行ParentType()函数中定义的所有对象初始化代码。结果就是SonType的每个实例都会有自己的colors副本了。
这仅仅是解决了上面第一个问题。 第二个问题,子类向超类传递参数

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

function SonType() {
  // 继承了ParentType
  ParentType.call(this, 'nichol');
  
  // 实例属性
  this.age = 21;
}

var instance = new SonType();
alert(instance.name); // 'nichol'
alert(instance.age); // 29

组合继承

还叫做伪经典继承
组合继承究竟组合了什么东西在里面呢?
原型链借用构造函数技术。 思路就是使用 原型链实现对原型属性和方法的 继承,又通过 借用构造函数来实现对实例属性 的继承

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

Parent.prototype.getParentValue = function() {
  return alert(this.name);
}


function SonType(name, age) {
  // 继承了ParentType属性
  ParentType.call(this, name);
  
  // 实例属性
  this.age = 21;
}

// 继承方法
SonType.prototype = new ParentType();

SonType.prototype.sayAge = function() {
  alert(this.age);
}

var instance1 = new SonType('jack', 21);
instance1.colors.push('black');
alert(instance1.colors); // ['red', 'green', 'black']
instance1.sayName(); // 'jack'
instance1.sayAge(); // 21

var instance2 = new SonType('Tom', 23);
alert(instance2.colors); // ['red', 'green']
instance2.sayName(); // 'Tom'
instance2.sayAge(); // 23

这种方式是js中最常用的继承模式。
缺点:会在下面寄生组合式继承提到。

原型继承

大致如下

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

先创造临时性的构造函数,然后把传入的对象作为这个临时构造函数的原型,然后返回这个临时类型的新实例。

var person = {
  name: 'Nichol',
  friends: ['Jack', 'Tom'],
}

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

alert(person.friends); // ['Jack', 'Tom', 'Rob', 'Barbie']

发现创建的两个新的实例,这个应用类型的friends不仅被person所有,还被anotherPersonyetAnotherPerson所有。
ES5新增了Object.create()规范化了原型继承,接收两个参数:1. 用作新对象原型的对象 2. 为新对象定义额外属性的对象。

var person = {
  name: 'Nichol',
  friends: ['Jack', 'Tom'],
}

var anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

alert(person.friends); // ['Jack', 'Tom', 'Rob', 'Barbie']

可以看到,如果只塞入一个变量,Object.create()object()一样,第二个参数使用方法也很简单,就省的继承以后重新再变更name

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

alert(anotherPerson.name); // 'Greg'

原型模式就是想让一个实例对象与另一个实例对象保持类似,不需要创建构造函数了。
缺点还是很明显:引用类型的属性始终都会共享。

寄生式继承

原型式机密相关的思路,创建一个仅用于封装继承过程的函数,函数在内部以某种方式来增强对象,再像真地是它做了所有工作一样返回对象。(说的弯弯绕绕)

function createAnother(origin) {
  var clone = object(origin);
  clone.sayHi = function() {
    alert('Hi');
  }
  return clone;
}

// 可以这么用
var person = {
  name: 'Nichol',
  friends: ['Jack', 'Tom'],
}

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'Hi'

新对象不仅有person所有属性和方法,而且还有自己的sayHi(), 缺点: 为对象添加函数,不能做到函数复用而降低效率。

寄生组合式继承

组合继承有自己的缺点:
调用两次超类型构造函数: 一次在创建子类原型的时候;一次式在子类型构造函数内部。

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

Parent.prototype.getParentValue = function() {
  return alert(this.name);
}


function SonType(name, age) {
  ParentType.call(this, name); // 第二次调用 ParentType()
  
  this.age = 21;
}

SonType.prototype = new ParentType(); // 第一次调用 ParentType()

SonType.prototype.sayAge = function() {
  alert(this.age);
}

第一次调用的时候: SonType.prototype会得到两个属性 namecolors;都是ParentType的实例属性,只不过位于SonType的原型中。
第二次调用的时候: SonType构造函数被调用,ParentType又被调用了一次,就导致了又一次在新对象上创建了实例属性namecolors。 于是乎,这两个属性就屏蔽了原型中的两个同名属性。
有两组name和colors,一组在实例上,一组在SonType原型中。然后,天降猛男出场了寄生组合

function inheritPrototype(sonType, parenType) {
  var prototype = object(ParentType.prototype); // 创建对象
  prototype.constructor = subType; // 增强对象
  subType.prototype = prototype;  // 指定对象
}

讲真看不太明白,这里留坑以后填

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

Parent.prototype.getParentValue = function() {
  return alert(this.name);
}

function SonType(name, age) {
  ParentType.call(this, name);
  this.age = 21;
}

// SonType.prototype = new ParentType() 用下面的替换了
inheritPrototype(SonType, ParentType);

SonType.prototype.sayAge = function() {
  alert(this.age);
}

只调用了一次ParetType构造函数,避免在SubType.prototype上创建不必要的、多余的属性。 这是最理想的继承范式~