好的,这是一个在 JavaScript 中非常经典且容易混淆的问题。我们来用清晰的类比和代码,彻底讲清楚 __proto__、prototype 和 [[Prototype]] 三者的区别和联系。
一句话总结
prototype:是 函数 独有的属性,它是一个对象。它的作用是为这个函数通过new创建出来的实例 提供共享的属性和方法。可以把它想象成一个“蓝图”或“模具”。__proto__:是 实例对象 独有的属性,它也是一个对象。它指向创建该实例的构造函数的prototype。它就是实例与“蓝图”之间的 连接。[[Prototype]]:是 ECMAScript 规范中定义的 内部属性。它才是对象原型链的实际链接,对外不可见。__proto__属性是浏览器为了方便访问[[Prototype]]而实现的一个非标准(但后来被标准化)的访问器。
详细解析
我们用一个“造车”的例子来贯穿整个解释。
1. prototype (原型对象)
- 拥有者:构造函数 (Constructor Function)。
- 是什么:一个普通的对象。
- 作用:作为该构造函数创建的所有实例的“共享仓库”。所有实例都可以访问到这个
prototype对象上的属性和方法。
代码示例:
// 1. 定义一个“汽车”构造函数 (可以看作是“汽车设计图纸”的工厂)function Car(brand) { this.brand = brand;}
// 2. 在“汽车设计图纸”(Car.prototype)上添加一个所有汽车都具备的功能Car.prototype.honk = function() { console.log('Beep beep!');};
// 3. Car.prototype 是一个对象,它有 honk 方法和一个默认的 constructor 属性console.log(Car.prototype);// 输出: { honk: [Function (anonymous)], constructor: [Function: Car] }
// 注意:普通对象和箭头函数没有 prototype 属性let obj = {};const arrowFunc = () => {};console.log(obj.prototype); // undefinedconsole.log(arrowFunc.prototype); // undefined类比:
Car 是一个“汽车工厂”。Car.prototype 就是一张设计蓝图。我们在这张蓝图上规定了,所有根据这张蓝图造出来的车,都应该有按喇叭(honk)的功能。
2. [[Prototype]] 与 __proto__ (原型链接)
- 拥有者:每一个 JavaScript 对象实例。
- 是什么:一个指向其“原型对象”的内部链接。
- 作用:当试图访问一个对象的某个属性时,如果该对象本身没有这个属性,JavaScript 引擎就会通过
[[Prototype]]链接,去它的原型对象上查找,如果还没有,就继续沿着原型对象的[[Prototype]]向上查找,直到找到或者到达原型链的终点 (null)。这个过程就是 原型链。
__proto__ 是 [[Prototype]] 的一个非标准但广泛实现的 访问器。你可以通过它来读取或设置一个对象的 [[Prototype]]。
代码示例(接上例):
// 4. 使用 new Car() “生产”两辆具体的汽车实例let myCar1 = new Car('Toyota');let myCar2 = new Car('Honda');
// myCar1 本身没有 honk 方法console.log(myCar1.hasOwnProperty('honk')); // false
// 5. 但 myCar1 可以调用 honk 方法,因为它通过 __proto__ 找到了 Car.prototypemyCar1.honk(); // Beep beep!
// 6. 核心关系:实例的 __proto__ 指向其构造函数的 prototypeconsole.log(myCar1.__proto__ === Car.prototype); // trueconsole.log(myCar2.__proto__ === Car.prototype); // true
// 现代、标准的访问方式是使用 Object.getPrototypeOf()console.log(Object.getPrototypeOf(myCar1) === Car.prototype); // true
myCar1.__proto__ === myCar1.constructor.prototype;类比:
myCar1 是一辆具体的丰田车。这辆车出厂时,车内有一个隐藏的标签([[Prototype]],可以通过 __proto__ 查看),上面写着:“我是根据‘汽车设计蓝图’(Car.prototype)制造的”。
当你让 myCar1 按喇叭(myCar1.honk()),它先在自己身上找“喇叭按钮”,发现没有。于是它查看那个隐藏标签,找到了“设计蓝图” Car.prototype,发现蓝图上有 honk 的说明,于是成功按响了喇叭。
三者关系图
这张图能帮你把所有关系串联起来:
+-------------------+ +----------------------+| 构造函数 | | 实例对象 || function Car | | let myCar1 |+-------------------+ +----------------------+| prototype |------->| |+-------------------+ | brand: "Toyota" | | | | __proto__ / | | [[Prototype]] | +--------|-------------+ | | +------------------------|-----------------+ | 原型对象 | | Car.prototype | +------------------------------------------+ | constructor: (指向 Car 构造函数) | | honk: function() { ... } | | | | __proto__ / [[Prototype]] (指向 Object.prototype) | +------------------------------------------+Car这个函数有一个prototype属性,指向它的原型对象。myCar1这个实例对象有一个__proto__内部属性,也指向Car的原型对象。- 因此,
myCar1.__proto__和Car.prototype指向的是同一个对象。 Car.prototype本身也是一个对象,所以它也有自己的__proto__,指向Object.prototype。这就是原型链的延续。
总结与最佳实践
| 特性 | prototype | __proto__ / [[Prototype]] |
|---|---|---|
| 谁拥有? | 构造函数 (e.g., Function, Array, Car) | 几乎所有对象实例 (e.g., {}, [], new Car()) |
| 是什么? | 一个对象,作为共享属性的“蓝图”或“模板”。 | 一个指向原型对象的内部链接。 |
| 作用 | 定义所有实例共享的方法和属性。 | 构建原型链,实现继承。 |
| 如何访问 | 直接通过 Constructor.prototype 访问。 | 推荐: Object.getPrototypeOf(obj)不推荐: obj.__proto__ |
graph TD A[Function] -->|__proto__| B[Function.prototype] B -->|__proto__| C[Object.prototype] C -->|__proto__| D[null]
E[Object] -->|__proto__| B F[Array] -->|__proto__| B G[所有其他函数] -->|__proto__| B所有函数的 proto 都是Function.prototype
普通对象(通过 {} 或 new Object() 创建的) proto 是Object.prototype
Object.prototype的 proto 是 null
总结表格
| 操作 | 属性位置 | 行为 | 结果 |
| 读取 (Get) | 自身 | 直接返回自身属性值 | 自身的属性值 |
| 读取 (Get) | 原型链 | 沿原型链向上查找并返回第一个找到的值 | 原型链上的属性值 |
| 读取 (Get) | 自身 & 原型链 | 自身属性遮蔽原型属性,返回自身属性值 | 自身的属性值 |
| 读取 (Get) | 不存在 | 查找到原型链末端(null) | undefined |
| 写入 (Set) | 自身 | 直接修改自身属性值 | 自身属性被更新 |
| 写入 (Set) | 不在自身,原型上有(可写) | 在自身创建新属性(遮蔽) | 实例上创建了一个同名的新属性 |
| 写入 (Set) | 不在自身,原型上有(只读) | 写入操作失败 | 静默失败或(严格模式下)抛出TypeError |
| 写入 (Set) | 不在自身,原型上有(Setter) | 调用原型上的 setter,this 指向实例 | setter 函数被执行,通常会在实例上创建/修改属性 |