JS中的继承与原型链

581 阅读5分钟

对于原型我们通过[[prototype]]、proto 以及 prototype 这三个概念理解其实现继承的思路。

[[prototype]]

在 ECMAScript 标准中规定每个对象都有一个内置的属性[[prototype]],它指向一个对象的原型对象。当查找一个对象的属性或方法时,如果在当前对象找不到,则在其原型对象上寻找,如果原型对象上也没有,则在原型对象的原型对象上寻找,如此继续直到一个对象的原型对象为 null(null 没有原型)。可以看到,这样一种层层向上的查找是一种链式查找,在每一层上的对象都有一个指向其原型对象的链接([[prototype]]),由这些链接组成的整个链条就叫做原型链。

如图 1 所示,原型链查找的思路大致为:

  • 当前对象 object1 在查找一个属性时,首先查找自己拥有的属性,如果没有,则在 object1 对象的__proto__([[prototype]])中查找,此时__proto__指向 object2。
  • 如果对象 object2 中没有要查找的属性,则在 object2 对象的__proto__中查找,如果没有则继续向上查找。
  • 直到 Object 对象的 prototype,此时 Object 对象的__proto__为 null,则不再查找。

链式查找示意图 图1. 原型链查找示意图

说明: 图中 builts-in 为构建内置函数比如 toString()、valueOf 等。

__proto__

前述中的[[Prototype]]是一个内置属性,我们并不能直接获取,为了操作属性的便利性很多浏览器都实现了 Object.prototype.__proto__,因此可以通过 obj.__proto__ 来访问对象的原型对象[[Prototype]],所以__proto__[[Prototype]]本质上是一个东西,都指向一个对象的原型对象。 另一方面,设置[[Prototype]]是一个缓慢的操作,影响性能,因此使用 __proto__ 是有争议的,更推荐使用 Object.getPrototypeOf 和 Object.setPrototypeOf 来访问原型对象(尽管如此,如果性能是个问题,也应尽量避免使用)。

prototype

prototype 是构造函数(一个拥有 [[Construct]] 内部方法的对象)才有的属性,比如函数(非箭头函数),实例对象是没有这个属性的。这个所谓的 prototype,其实可以认为是构造函数内部一个普通的对象(或者说指向一个普通对象),只是很不幸地也叫做 prototype(原型对象)而已,当构造函数执行时,会自动将构造函数的 prototype 赋值给 __proto__(构造函数内部没有显示返回对象的情况下),这样在新的实例上通过原型链就可以共享构造函数 prototype 及其原型链上的属性了。prototype 和前述的__proto__[[Prototype]]是完全不同的概念,我们通常的混淆,主要就来自于用原型对象一词来指代了不同的它们。

__proto__与prototype关系示意图 图2. __proto__与prototype关系示意图

来看下面的例子: 函数 Animal 通过 new 实例化的对象能够访问到函数 prototype 属性的 food 和 eat,这是如何做到的呢?

var Animal = function(name) {
  this.name = name;
};
Animal.prototype.food = 'meat';
Animal.prototype.eat = function() {
  console.log(this.name + ' eat ' + this.food);
};
var panda = new Animal('panda');
var dog = new Animal('dog');
console.log(panda.eat()); // panda eat meat
console.log(dog.eat()); // dog eat meat
console.log(panda.__proto__=== Animal.prototype); // true

如下图所示,实例对象 panda 和 dog 之所以能够访问 Animal 原型上的 food 和 eat 属性是因为在调用构造函数时 Animal 的 prototype 对象赋值给了实例对象的 __proto__ 属性,实例对象在访问自己的方法(panda.eat)时,如果没有找到,则在__proto__对象中寻找,而这个对象正好是 Animal 的 prototype 对象,它拥有 eat 方法,所以可以成功访问 eat 方法。

prototype继承示意图 图3. panda/dog实例继承示意图

来看另一个例子: 如下将函数 Fish 的 prototype 赋值为 Animal,以此,通过 fish 的实例来访问 Animal 原型 prototype 上的方法,可结果是 Uncaught TypeError: nimo.eat is not a function,为什么会这样呢?之所以会出现这样的错误,是因为我们错误的把原型对象(__proto__)当原型对象(prototype)。前述我们已经知道继承是通过原型链来实现的,而原型链又是通过 __proto__来串联的。当函数 Fish 的 prototype 赋值为 Animal 后,生成的实例对象 nimo 的 __proto__ 为 Animal,所以访问 nimo.eat 会先在 Animal 上寻找 eat 方法,如图 3,Animal 函数并没有 eat 方法,从而通过 Animal 的__proto__ 继续向上寻找,直到顶层对象 Object,结果还是没有,因此报错。

var Animal = function(name) {
  this.name = name;
};
Animal.prototype.food = 'meat';
Animal.prototype.eat = function() {
  console.log('I can eat' + this.food);
};

var Fish = function(name) {
  this.name = name;
};
Fish.prototype = Animal;

var nimo = new Fish('nimo');
console.log(nimo.eat()); // Uncaught TypeError: nimo.eat is not a function

通过不同的方法来创建对象和生成原型链

  • 语法结构创建对象

    • 对象字面量

      通过对象字面量创建的对象其原型链为 obj --> Object.prototype --> null

      var obj = { a: 1 };
      
    • 数组字面量

      通过数组字面量创建的对象其原型链为 arr --> Array.prototype --> Object.prototype --> null

      var arr = [1, 2];
      
    • 函数字面量

      通过函数字面量创建的对象其原型链为 f --> Function.prototype --> Object.prototype --> null

      function f(){ console.log('func');}
      
  • 构造器创建对象

    通过构造函数创建的对象其原型链为 instance --> func.prototype --> Object.prototype --> null

      var Animal = function(name) {
        this.name = name;
      };
      Animal.prototype.food = 'meat';
      Animal.prototype.eat = function() {
        console.log('I can eat' + this.food);
      };
      //实例对象panda的__proto__指向Animal.prototype
      var panda = new Animal('panda');
    
  • Object.create 创建对象

    在 ES5 中引入了一个新的方法来创建对象,就是 Object.create,新对象的原型就是该方法传入的第一个参数。

      var a = { x: 1 };
      // a --> Object.prototype --> null
    
      var b = Object.create(a);
      // b --> a --> Object.prototype --> null
      console.log(b.__proto__ === a); // true
      console.log(b.x); // 1
    
      var c = Object.create(b);
      // c --> b --> a --> Object.prototype --> null
      console.log(c.__proto__ === b); // true
    

总结

  • 任何对象都可以成为其他对象的原型对象(__proto__指向的对象)。
  • [[Prototype]]为一个对象的指向原型对象的内置属性,不能直接访问。
  • __proto__ 为一个非标准的,只是为了方便访问原型对象而实现的一个属性,它和[[Prototype]]本质上一样都 指向原型对象,是所有对象都有的属性。
  • prototype 为拥有 [[construct]] 内部方法的对象才有的属性,它本身只是一个普通对象,只是正好叫做原型对象,它的作用是在构造函数生成新的实例时将这个所谓的原型对象赋值给实例的 __proto__ 属性,这样新的实例就可以通过 __proto__ 来继承构造函数原型里的方法。可以看到,prototype 和 __proto__ 所指的原型对象是完全不同的概念。
  • 实例对象没有 prototype 属性,