原型和原型链
原型和原型链是 JS 中非常基础但又非常重要的东西。本篇文章争取简单直接的说清楚这两个东西究竟是什么,以及 prototype 和 __proto__,又代表着什么?
顶层构造函数
Object 是 JS 中的顶层构造函数,以下两种写法是等价的:
// 写法1
const obj = { name: '小明' };
// 写法2
const obj2 = new Object({ name: 'obj2' })
其实明白了 Object 是如何创建对象的,也就了明白了什么是原型。
在 JS 中,每个函数都有一个属性 prototype,这个属性指向一个对象。这个对象中有一个属性 constructor,这个属性又指回了函数本身。我们以 Object 这个顶层构造函数为例,他们的关系如下图所示。
以下代码的输出结果可以验证:
console.log(Object.prototype.constructor === Object); // true
那这个关系到底又有什么用呢?
当我们执行const obj2 = new Object({ name: 'obj2' })这行代码时,JS 引擎做了下面这些事:
- JS 底层(通常是浏览器或者 Node 引擎)创建一个空对象。
- 将这个空对象的
__proto__属性赋值为Object.prototype - 对这个对象执行 constructor 里面的代码,这个例子中也就是设置
name: 'obj2'。
这几件事儿做完之后,三者的关系如下:
Object.prototype.__proto__ = null,也就是说 Object.prototype 如果继续往上找圆原型就是 null 了。这个图比较重要,里面 prototype, constructor, __proto__,到底是什么意思其实不太重要,归根结底,它们只是对象身上的属性。但是它们各自的起点和终点大家是要了然于胸的。
prototype:构造函数的属性,指向其创建的原型对象,该原型对象将被实例共享。constructor:原型对象的属性,默认指回构造函数本身,用于标识实例的创建者。__proto__:实例对象的属性,指向构造函数的原型对象(即prototype)。
自定义构造函数
再来一个案例,这个案例中我们自定义一个构造函数,通过这个构造函数再来分析一遍。
function Human(name) {
this.name = name;
}
const xh = new Human('小红');
Human.prototype.eat = function(food) {
console.log("I'm eating " + food);
}
console.log(xh);
- 创建函数 Human。
- 创建 xh,具体流程图中体现。这个时候原型链就已经体现出来了,xh 的原型是 Human.prototype,Human.prototype 的原型是 Object.prototype。
- 挂载 eat 方法到原型上。
这个时候如果调用 xh.eat(), JS 引擎就会发现 xm 这个对象实例本身没有 eat 方法,但是往上找它的原型(__proto__),就能找到 eat 方法,接着就会调用这个 eat 方法。所以这也是为什么公用方法应该挂在到原型上。
xh 的 __proto__ 是通过 new Human 创建对象是绑定的,绑定的值就是 Human.prototype,而 Human.prototype 的 __proto__ 是因为在创建 Human.prototype 时调用了 new Object(),所以绑定的就是 Object.prototype。这就是原型链的由来。
总结一下
prototype就是构造函数的一个属性,这个属性决定了该构造函数 new 出来的对象实例的__proto__到底是谁;__proto__则是对象身上的一个属性,指向该对象的原型到底是谁(虽然这个属性严格来说其实并不存在,但在大多数浏览器环境是支持这样访问的。)- 而通过构造函数的层层连接,原型链就形成了。