原型链

原型链是 JS 的一个非常重要的概念,首先从对象说起:

在 JS 中,除了基本类型之外,其他的类型都是对象,我们看看对象的结构:

const a = {
  b: 1,
  c: 2,
}

这就是一个普通对象,只不过上面显示的是对象中可枚举的属性,对象中还有一个不可枚举的属性 __proto__,在控制台输出对象可以看到这个属性:

截图未命名.jpg

在 JS 中,所有对象都存在 __proto__ 属性,那么 __proto__ 是用来干什么的呢?继续看:

__proto__ 下面有一个 constructor 属性,指向的是 Objectprototype,这里要说一下函数对象和普通对象的区别:

函数对象普通对象
prototype
__proto__

也就是说只有函数才具有 prototype 属性

既然 constructor 指向的是 Object 的构造函数,那么 constructor 下必然有 prototype 属性,prototype 属性指向的仍然是 constructor 本身,也就是说:a.__proto__ === Object.prototype:

截图未命名.jpg

我们可以得出以下结论:

  • 实例对象的 __proto__ 主动指向构造函数的 prototype,这里的构造函数是 Object

我们现在再使用构造函数创建一个对象:

function Animal() {
  // ...
}
const dog = new Animal()

Animal 就是一个构造函数,通过 new 操作符可以得到一个新的 Animal 实例,Animal 是构造函数,也就一定有 prototype,因为 prototype 是一个对象,所以一定拥有 __proto__ 属性,我们来看看:

截图未命名.jpg

结果不出所料,因为 prototype 本身是一个对象,所以它的 __proto__ 理应指向它的构造函数 Objectprototype:

Animal.prototype.__proto__ === Object.prototype

那么当我们创建构造函数的时候,是如何自动生成 __proto__prototype 的呢?

function Animal() {
  // 定义函数时自动执行的代码
  Animal.__proto__ = Function.prototype
  Animal.prototype = {
    constructor: Animal,
    __proto__: Object.prototype,
  }
}

就是这么简单!

prototype 称为显式原型对象,__proto__ 称为隐式原型对象

我们知道实例的第一层除了我们自己定义的属性之外只有一个 __proto__,那么如果我想调用实例的原生对象方法,JS 是如何找到并执行的呢?

答案就是原型链

假设有如下代码:

function Animal() {
  this.name = 'Lee'
}
Animal.prototype.getName = function () {
  return this.name
}

const dog = new Animal()
dog.toString()
dog.getName()

输出 dog,我们可以看到 dog 中是没有 toStringgetName 方法的:

截图未命名.jpg

当在第一层找不到相应属性的时候,就回去找自身的 __proto__ 属性继续向上查找,__proto__ 指向构造他函数的原型,也就是 Animal.prototype,这上面就有我们定义的 getName 方法,于是停止了寻找,而 Animal.prototype 上并没有 toString,沿着 Animal.prototype.__proto__ 继续向上寻找,终于找到了 Object.prototype,上面有 tostring 方法

这个过程可以用一张图来解释:

截图未命名.jpg

明白了原型和原型链我们就知道 new 是如何实现的了:

function new(parent, ...args) {
  let obj = {};
  obj.__proto__ = parent.prototype;
  let res = parent.call(obj, args);
  return typeof res === "object" ? res : obj;
}

instance 的实现也是基于原型链:

function instance(left, right) {
  let rightProto = right.prototype
  let leftValue = left.__proto__
  while (true) {
    if (leftValue === null) {
      return false
    }
    if (leftValue === rightProto) {
      return true
    }
    leftValue = leftValue.__proto__
  }
}

__proto__prototype 的区别

说了这么多,既然 __proto__prototype 都能访问到原型,那么它们的区别是什么呢?有以下几点:

  1. __proto__ 是每个对象都有的一个属性,而 prototype 是函数才会有的属性。
  2. __proto__ 指向的是当前对象的原型对象,而 prototype 指向的,是以当前函数作为构造函数构造出来的对象的原型对象。

按照 JavaScript 的继承机制,对于实例来说可以通过 __proto__ 来访问所构造它的函数的 prototype,因此 __proto__ 就是对象用于访问原型链的桥梁。

另外 __proto__ 实际上指的是对象的内部属性 [[Prototype]],之所以使用 [[]] 括起来是因为它是词法环境中的东西;官方不建议我们使用 __proto__ 访问对象的原型,而应该使用 Object.getPrototypeOf()/Reflect.getPrototypeOf() 访问。

修改对象的 __proto__ 是一个非常浪费性能的事情,所以尽量使用 Object.create 来实现继承。