简述原型与原型链

在 JavaScript 中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型(prototype)。

通常,我们会通过构造函数对原型进行操作:

1
2
3
function Foo() {}

console.log(Foo.prototype) // {constructor: ƒ}

打印出来的对象就是Foo函数的原型,将其展开,发现里面有两个属性constructor__proto__constructor指向Foo函数本身,__proto__是一个对象。

可以在函数的原型上添加一些其他属性,比如:

1
2
Foo.prototype.name = 'nick'
Foo.prototype.age = 18

再次查看Foo的原型:

1
console.log(Foo.prototype)

发现刚刚添加的nameage已经在原型中了。

现在创建一个Foo的实例:

1
var foo = new Foo()

将其打印,发现里面有一个__proto__属性,而这个属性里面包含了之前在构造函数Foo的原型中创建的nameage属性。也就是说,构造函数的实例都可以通过__proto__属性找到创建它的构造函数的原型对象。

由此可以得到下面这张关系图:
实例与构造函数

在之前打印Foo的原型时,发现里面的__proto__是一个对象,将其展开,发现有一个constructor属性是指向Object构造函数的。那么Foo原型上的__proto__会不会就是构造函数Object的原型呢?

带着疑问,试着打印对比一下:

1
console.log(Foo.prototype.__proto__ === Object.prototype) // true

结果为true。并且我们知道在JavaScript中,函数都有一个特殊的属性prototype,那么构造函数Object的内部结构与之前创建的构造函数Foo的内部结构是差不多的。

现在再来更新一下刚刚的关系图:
Object与其他构造函数

可以看出,函数的__proto__属性就像一根线条一样一直往上衔接,那么问题又来了?Object原型上的__proto__又是个啥?会不会是其他构造函数的原型?

带着疑问,再次打印看看结果:

1
console.log(Object.prototype.__proto__) // null

结果是一个特殊值null,这就表明,原型链的最顶层就是到构造函数Object原型的__proto__,也就是null

更新一下刚才的关系图:

原型链顶层

到这里,基本上把原型与原型链的关系理清楚了。

但是,还可以延伸一点点内容。

我们知道,在JavaScript中,函数实际上是一个Function对象。这是不是就表明构造函数FooObjectFunction的实例?

可以通过instanceof来测试一下:

1
2
Foo instanceof Function // true
Object instanceof Function // true

发现都为true,按照之前的结论,实例的__proto__属性都指向创建它的构造函数的prototype对象,
这就说明,构造函数的__proto__都是指向Functionprototype对象。

1
2
console.log(Foo.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true

Function__proto__应该也是指向自身的prototype对象的:

1
console.log(Function.__proto__ === Function.prototype) // true

那么Functionprototype对象中的__proto__又指向哪里呢?

当然是Object的原型呐:

1
console.log(Function.prototype.__proto__ === Object.prototype) // true

这样一来,再更新一下刚刚的关系图:

完整的原型与原型链

总结

  • 任何函数都有一个prototype属性,它是一个对象,直观的说,这个对象就是当前函数的原型。
  • prototype中的constructor通常指向函数自身。
  • 实例对象的__proto__指向创建它的构造函数的原型。
  • 构造函数的__proto__指向Function的原型。
  • Function__proto__指向自身的原型(和上一条类似)。