JavaScript 继承


JavaScript 继承

原文链接


有些知识当时实在看不懂的话,可以先暂且放下,留在以后再看也许就能看懂了。

几个月前,抱着《JavaScript 高级程序设计(第三版)》,啃完创建对象,就开始啃起了继承,然而啃完原型链就实在是看不下去了,脑子越来越乱,然后就把它扔一边了,继续看后面的。现在利用这个暑假搞懂了这个继承,就把笔记整理一下啦。

原型链(Prototype Chaining)

先看一篇文章,文章作者讲的非常不错,并且还配高清套图哦。lol…

从原文中小摘几句

  • 构造函数通过 prototype 属性访问原型对象

  • 实例对象通过 [[prototype]] 内部属性访问原型对象,浏览器实现了 proto 属性用于实例对象访问原型对象

  • 一切对象都是Object的实例,一切函数都是Function的实例

  • Object 是构造函数,既然是函数,那么就是Function的实例对象;Function是构造函数,但Function.prototype是对象,既然是对象,那么就是Object的实例对象

确定原型与实例的关系

有两种方法来检测原型与实例的关系:

  • instanceof:判断该对象是否为另一个对象的实例

instanceof 内部运算机理如下:

function instance_of(L, R) {  //L 表示左表达式,R 表示右表达式
  var O = R.prototype;    // 取 R 的显示原型
  L = L.__proto__;        // 取 L 的隐式原型
  while (true) { 
    if (L === null) 
      return false; 
    if (O === L)   // 这里重点:当 O 严格等于 L 时,返回 true 
      return true; 
    L = L.__proto__; 
  } 
}

上面代码摘自:JavaScript instanceof 运算符深入剖析

  • isPrototypeOf():测试一个对象是否存在于另一个对象的原型链上

这两个方法的不同点请参看:JavaScript isPrototypeOf vs instanceof usage

只利用原型链实现继承

缺点:1. 引用类型值的原型属性会被实例共享; 2. 在创建子类型的实例时,不能向超类型的构造函数中传递参数

function Father(){
  this.name = "father";
  this.friends = ['aaa','bbb'];
}
function Son(){
}
Son.prototype = new Father();
Son.prototype.constructor = Son;

var s1 = new Son();
var s2 = new Son();

console.log(s1.name);  // father
console.log(s2.name);  // father
s1.name = "son";
console.log(s1.name);  // son
console.log(s2.name);  // father

console.log(s1.friends);  // ["aaa", "bbb"]
console.log(s2.friends);  // ["aaa", "bbb"]
s1.friends.push('ccc','ddd');
console.log(s1.friends);  // ["aaa", "bbb", "ccc", "ddd"]
console.log(s2.friends);  // ["aaa", "bbb", "ccc", "ddd"]

只利用构造函数实现继承

实现方法:在子类型构造函数的内部调用超类型构造函数(使用 apply() 和 call() 方法)

  • 优点:解决了原型中引用类型属性的问题,并且子类可以向超类中传参

  • 缺点:子类实例无法访问父类(超类)原型中定义的方法,所以函数复用就无从谈起了。

function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.getName = function(){
  return this.name;
};

function Son(name){
  // 注意: 为了确保 Father 构造函数不会重写 Son 构造函数的属性,请将调用 Father 构造函数的代码放在 Son 中定义的属性的前面。
  Father.call(this,name,['aaa','bbb']);
  
  this.age = 22;
}

var s1 = new Son('son1');
var s2 = new Son('son2');

console.log(s1.name);  // son1
console.log(s2.name);  // son2

s1.friends.push('ccc','ddd');
console.log(s1.friends);  // ["aaa", "bbb", "ccc", "ddd"]
console.log(s2.friends);  // ["aaa", "bbb"]

// 子类实例无法访问父类原型中的方法
s1.getName(); // TypeError: s1.getName is not a function
s2.getName(); // TypeError: s2.getName is not a function

组合继承(Combination Inheritance)

实现方法:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
  console.log(this.name);
};

function Son(name,age){
  // 继承父类的属性
  Father.call(this,name,['aaa','bbb']);
  
  this.age = age;
}

// 继承父类原型中的属性和方法
Son.prototype = new Father();
Son.prototype.constructor = Son;

Son.prototype.getAge = function(){
  console.log(this.age);
};

var s1 = new Son('son1',12);
s1.friends.push('ccc');
console.log(s1.friends);  // ["aaa", "bbb", "ccc"]
console.log(s1.money);    // 100k $
s1.getName();             // son1
s1.getAge();              // 12

var s2 = new Son('son2',24);
console.log(s2.friends);  // ["aaa", "bbb"]
console.log(s2.money);    // 100k $
s2.getName();             // son2
s2.getAge();              // 24

组合继承避免了单方面使用原型链或构造函数来实现继承的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式,但是它也是有缺陷的,组合继承的缺陷会在后面专门提到。

原型式继承(Prototypal Inheritance)

实现思路:借助原型基于已有的对象创建新对象,同时不必因此而创建自定义类型。

为了达到这个目的,引入了下面的函数(obj)

function obj(o){
  function F(){}
  F.prototype = o;
  return new F();
}
var person1 = {
  name: "percy",
  friends: ['aaa','bbb']
};
var person2 = obj(person1);
person2.name = "zyj";
person2.friends.push('ccc');

console.log(person1.name);    // percy
console.log(person2.name);    // zyj
console.log(person1.friends); // ["aaa", "bbb", "ccc"]
console.log(person2.friends); // ["aaa", "bbb", "ccc"]

ECMAScript 5 通过新增 Object.create() 方法规范化了原型式继承。在传入一个参数的情况下,Object.create() 和 obj() 方法的行为相同。

var person1 = {
  name: "percy",
  friends: ['aaa','bbb']
};
var person2 = Object.create(person1);
person2.name = "zyj";
person2.friends.push('ccc');

console.log(person1.name);    // percy
console.log(person2.name);    // zyj
console.log(person1.friends); // ["aaa", "bbb", "ccc"]
console.log(person2.friends); // ["aaa", "bbb", "ccc"]

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,可以选择使用这种继承。

寄生式继承(Parasitic Inheritance)

寄生式继承是与原型式继承紧密相关的一种思路。

实现思路:创建一个仅仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。

function obj(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function createPerson(original){    // 封装继承过程
  var clone = obj(original);         // 创建对象
  clone.showSomething = function(){  // 增强对象
    console.log("Hello world!");
  };
  return clone;                      // 返回对象
}

var person = {
  name: "percy"
};
var person1 = createPerson(person);
console.log(person1.name);  // percy
person1.showSomething();    // Hello world!

寄生组合式继承(Parasitic Combination Inheritance)

先来说说我们前面的组合继承的缺陷。组合继承最大的问题就是无论什么情况下,都会调用两次父类的构造函数:一次是创建子类的原型的时候,另一次是在调用子类构造函数的时候,在子类构造函数内部又调用了父类的构造函数。

function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
  console.log(this.name);
};

function Son(name,age){
  // 继承父类的属性
  Father.call(this,name,['aaa','bbb']);    // 第二次调用 Father() , 实际是在 new Son() 时才会调用
  
  this.age = age;
}

// 继承父类原型中的属性和方法
Son.prototype = new Father();               // 第一次调用 Father()
Son.prototype.constructor = Son;

第一次调用使的子类的原型成了父类的一个实例,从而子类的原型得到了父类的实例属性;第二次调用会使得子类的实例也得到了父类的实例属性;而子类的实例属性默认会屏蔽掉子类原型中与其重名的属性。所以,经过这两次调用,子类原型中出现了多余的的属性,从而引进了寄生组合式继承来解决这个问题。

寄生组合式继承的背后思路是:不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已

本质上,就是使用寄生式继承来继承父类的原型,然后将结果返回给子类的原型。

function obj(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function inheritPrototype(son,father){
  var prototype = obj(father.prototype);  // 创建对象
  prototype.constructor = son;            // 增强对象
  son.prototype = prototype;              // 返回对象
}
function Father(name,friends){
  this.name = name;
  this.friends = friends;
}
Father.prototype.money = "100k $";
Father.prototype.getName = function(){
  console.log(this.name);
};

function Son(name,age){
  // 继承父类的属性
  Father.call(this,name,['aaa','bbb']);
  
  this.age = age;
}

// 使用寄生式继承继承父类原型中的属性和方法
inheritPrototype(Son,Father);

Son.prototype.getAge = function(){
  console.log(this.age);
};

var s1 = new Son('son1',12);
s1.friends.push('ccc');
console.log(s1.friends);  // ["aaa", "bbb", "ccc"]
console.log(s1.money);    // 100k $
s1.getName();             // son1
s1.getAge();              // 12

var s2 = new Son('son2',24);
console.log(s2.friends);  // ["aaa", "bbb"]
console.log(s2.money);    // 100k $
s2.getName();             // son2
s2.getAge();              // 24

优点:使子类原型避免了继承父类中不必要的实例属性。

开发人员普遍认为寄生组合式继承是实现基于类型继承的最理想的继承方式。

最后

最后,强烈推荐两篇很硬的文章

摘第二篇文章的一张硬图过来:

JavaScript 继承

看完之后,秒懂原型链,有木有?

参考文章


9 responses on “JavaScript 继承

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>