重拾JS——继承

继承是面相对象编程语言的一个特色,一般分为两类:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。在 JS 中,没有函数签名,因此,JS 只支持实现继承,主要是通过原型链实现的。

构造函数,原型对象和实例之间的关系

先来回顾下构造函数,原型对象,实例,三者之间的关系:

每创建一个函数,就会为函数创建一个 prototype 属性,指向原型对象;

原型对象,默认情况下,会自动获得一个 constructor 属性,指回构造函数;

通过构造函数创建的实例,会有一个内部属性 [[prototype]](也叫__prototype__),指向原型对象。

关系如下图:

原型链继承

让子类的原型对象 等于 父类的实例,Child.prototype = new Parent();

function Parent() {
  this.parentAge = 56;
}

Parent.prototype.getParentAge = function() {
  return this.parentAge;
};

function Child() {
  this.childAge = 18;
}

Child.prototype = new Parent();

Child.prototype.getChildAge = function () {
  return this.childAge;
};

var xiaoming = new Child();
console.log(xiaoming.getParentAge()); // 56

关系图如下:

注意 上图中 Child() 的原型对象 new Parent()constructor 并不指向 Child()(此时它指向 Parent())。后续我们会手动让它指向 Child()

通过 Child.prototype.constructor 可以查看

默认原型

事实上,上图少了一环,默认的原型。我们知道所有的引用类型都默认继承了 Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此,默认原型都会包含一个内部指针,指向 Object.prototype。这也是所有自定义类型都会继承 toString(),valueOf等方法的原因所在。

从上图中,可以清晰的看到原型链逐级向上查找的路径,原型链的关键是 实例的 [[prototype]] 属性。

原型和实例关系

xiaoming 同时是 Child,Parent,Object 的实例;

xiaoming instanceof Child; // true
xiaoming instanceof Parent; // true
xiaoming instanceof Object; // true

Child.Prototype,Child.Prototype, Child.Prototype 都是 xiaoming的原型

Child.prototype.isPrototypeOf(xiaoming); // true
Parent.prototype.isPrototypeOf(xiaoming); // true
Object.prototype.isPrototypeOf(xiaoming); // true

谨慎定义方法

给原型添加方法,一定要放在替换原型语句后。

问题

纯粹的原型链继承,虽然强大,实现起来也简单,但是它也有些问题。

第一个问题是,包含引用类型的原型属性。

重拾JS——创建对象 中说过,包含引用类型的原型属性,会被共享(事实上,是所有原型属性都会被共享,只是 引用类型的共享通常不是我们所希望的)。而这也正是为什么要在构造函数中,而不是原型对象中定义属性的原因。

在通过原型继承时,原型会变成另一个类型的实例,于是,原先的实例属性也就顺理成章的成为了原型属性。如下:

function Parent() {
  this.friend = ['aa', 'bb'];
}

function Child() {}
Child.prototype = new Parent();

var xiaoming = new Child();
xiaoming.friend.push('cc');
console.log(xiaoming.friend) // ["aa", "bb", "cc"]

var xiaohong = new Child();
console.log(xiaohong.friend) // ["aa", "bb", "cc"]

第二个问题是,在创建子类的实例时,无法向父类传递参数。

因此,实践中很少单独使用原型链。

借用构造函数

思想:在子类构造函数内部,调用父类构造函数。结合 call()apply() 方法,可以在实例上执行父类构造函数,并传参:

function Parent() {
  this.friend = ['aa', 'bb'];
}

function Child() {
  // 继承了 父类属性
  Parent.call(this);
}

var xiaoming = new Child();
xiaoming.friend.push('cc');
console.log(xiaoming.friend) // ["aa", "bb", "cc"]

var xiaohong = new Child();
console.log(xiaohong.friend) // ["aa", "bb"]

传参

相对于原型链继承而言,借用构造函数模式有个很大的优势,就是可以向父类传递参数:

function Parent(skill) {
  this.skill = skill;
}

function Child() {
  // 继承了 父类属性。可以实现多继承,call多个父类对象
  Parent.call(this, 'code');
  // 实例属性
  this.age = 18;
}

var xiaoming = new Child();
console.log(xiaoming.skill) // code

问题

既然是借用构造函数,那么也无法避免构造函数模式存在的问题:方法都在构造函数中定义,因此函数复用就无从谈起了。

组合继承

组合继承就是同时采用 原型链继承 和 构造函数继承,发挥二者之长,解决各自不足。

通过原型链实现原型属性和方法的继承,通过构造函数实现对实例属性的继承。

function Parent(skill) {
  this.skill = skill;
  this.parentAge = 56;
  this.friend = ['aa', 'bb']
}
Parent.prototype.getParentAge = function() {
  return this.parentAge;
};

function Child(skill, age) {
  // 继承了 父类属性(也可以叫原型属性)
  Parent.call(this, skill); // 第二次调用父类
  // 实例属性
  this.childAge = age;
}

// 继承方法
Child.prototype = new Parent(); // 第一次调用父类

Child.prototype.constructor = Child; // 重新指向子类

// 实例方法
Child.prototype.getChildAge = function() {
  return this.childAge;
};

var xiaoming = new Child('code', 18);
var xiaohong = new Child('sing', 16);

这种继承方法是 JS 中最常用的继承模式。

注意上面 Child.prototype.constructor = Child 重新指向了 Child,因此 前面那个图中浅色的 constructor 连线就恢复了。

问题

无论在什么情况下,都会调用两次父类构造函数。第一次调用父类构造函数时,子类的原型会得到 parentAge friend 等属性;第二次调用父类构造函数时,会在新对象上创建了 parentAge friend 等属性。当我们访问这两个属性时,实例中的属性会屏蔽掉子类原型中的属性。

要解决这个问题,可以使用 寄生组合式继承,而这个模式又依赖 寄生式继承,而 寄生式继承 又依赖 原型式继承,因此,先来看看 原型式继承 和。

原型式继承

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

本质上来讲,object() 方法对传入的对象执行了一次浅复制。

es5 对这种模式进行了规范化,新增 Object.create() 方法。

Object.create() 方法接收两个参数,传一个参数的时候,跟 object() 方法行为相同;第二个参数跟 Object.defineProperties() 方法的第二个参数一样,可以通过描述符自定义每个属性的行为。


var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person, {
  name: {
    value: "Greg",
  }
});
console.log(anotherPerson.name); // "Greg"
console.log(anotherPerson.friends); // ["Shelby", "Court", "Van"]

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

不足

引用类型共享问题。

寄生式继承

function createAnother(original){
  var clone = object(original);
  clone.sayHi = function(){
    alert("hi");
  };
  return clone;
}

寄生组合式继承

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

function inheritPrototype(child, parent) {
  var prototype = object(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
};

function Parent(skill) {
  this.skill = skill;
  this.parentAge = 56;
  this.friend = ['aa', 'bb']
}
Parent.prototype.getParentAge = function() {
  return this.parentAge;
};

function Child(skill, age) {
  // 继承了 父类属性(也可以叫原型属性)
  Parent.call(this, skill);
  // 实例属性
  this.childAge = age;
}

// Child.prototype = new Parent(); 
inheritPrototype(Child, Parent);

// Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child })

// 实例方法
Child.prototype.getChildAge = function() {
  return this.childAge;
};

var xiaoming = new Child('code', 18);
var xiaohong = new Child('sing', 16);

开发人员普遍认为这种模式是最理想的继承范式。

观察代码,发现,除了将 Child.prototype = new Parent() 替换为 inheritPrototype(Child, Parent) 外,其他的都一样。

继续查看 inheritPrototype 函数,发现它首先复制了父类的原型对象,然后将其赋值给子类的原型。

怎么复制父类的原型对象呢?是通过 object() 方法实现的(也可以通过其他方式实现,比如 Object.create()方法实现)。

思考:是不是只要能将父类的原型对象包含在子类的原型对象中,应该就能达到类似的效果呢? 例如 Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child })

function Parent(skill) {
  this.skill = skill;
  this.parentAge = 56;
  this.friend = ['aa', 'bb']
}
Parent.prototype.getParentAge = function() {
  return this.parentAge;
};

function Child(skill, age) {
  // 继承了 父类属性(也可以叫原型属性)
  Parent.call(this, skill);
  // 实例属性
  this.childAge = age;
}

Child.prototype = Object.assign({}, { ...Parent.prototype }, { constructor: Child })

// 实例方法
Child.prototype.getChildAge = function() {
  return this.childAge;
};

var xiaoming = new Child('code', 18);
var xiaohong = new Child('sing', 16);

注意,上图是我个人理解的,不知道对不对,欢迎讨论,指正。

最后

相当于把书抄了一遍。书读百遍,其义自见,加油。

关系图是个人理解画出来的,不知道正确不正确,仅供参考。

阅读 453

推荐阅读