Prototype和_proto_的区别
- Prototype属性(原型对象)。是函数(Function)独有的属性,非函数类对象没有这个属性。作用是,作为一个“公共仓库”,用来放置希望所有实例共享的属性和方法。
- _proto_属性(隐式原型指针)。JavaScript 中的每一个对象(包含函数、数组、实例等)都有。作用是,指向创建该对象的构造函数的 prototype。它就是对象用来“向上查找”的连接线。
一个函数对象拆解后的实际样子
要彻底搞懂一个函数对象在内存和浏览器控制台里的完整拆解结构,我们可以将它划分为 4 大核心板块。
在 Chrome 控制台中运行 console.dir(Foo) 并展开后,你能看到它的全部内部组成:
// 1. 打印函数对象(查看它的 prototype 和 [[Prototype]])
function Foo() {}
Foo.prototype.sayHi = function() {};
console.dir(Foo);
函数对象的 4 大结构板块
第一块:函数自身的基础元数据(自身属性)
length: 0- 含义: 函数的形参个数。因为
Foo()没有写入参,所以是 0。
- 含义: 函数的形参个数。因为
name: "Foo"- 含义: 函数的名字字符串。
第二块:prototype(给 future 实例准备的共享仓库)
展开 ▼ prototype: 这一段,就是将来使用 new Foo() 生产出的实例对象能共享的所有内容:
sayHi: ƒ ()- 含义: 挂载在
Foo.prototype上的方法。
- 含义: 挂载在
constructor: ƒ Foo()- 含义: 原型对象上的“反向指针”,表明这个原型仓库是属于
Foo函数的。
- 含义: 原型对象上的“反向指针”,表明这个原型仓库是属于
▼ [[Prototype]]: Object- 含义:
Foo.prototype本身也是一个普通的 JavaScript 对象,所以它的原型指向了最高顶层的Object.prototype。
- 含义:
第三块:已废弃的旧语法属性与引擎标记
arguments: null/caller: null- 含义: 传统 JS 中用来获取“传入参数”和“调用当前函数的父级函数”的旧属性。在现代 ES6 严格模式下已被禁用,所以显示为
null。
- 含义: 传统 JS 中用来获取“传入参数”和“调用当前函数的父级函数”的旧属性。在现代 ES6 严格模式下已被禁用,所以显示为
[[FunctionLocation]]: prototype.html:10- 含义: 浏览器引擎的标记,提示这个函数是在代码文件
prototype.html的第 10 行被定义的。
- 含义: 浏览器引擎的标记,提示这个函数是在代码文件
第四块:▼ [[Prototype]]: ƒ ()(Foo 函数自己继承的能力)
这是最关键的隐式原型指针(即 Foo.__proto__)。因为函数本身也是一个对象,所以它也需要继承“作为函数”的能力,这里指向的是 Function.prototype:
apply: ƒ apply()/bind: ƒ bind()/call: ƒ call()- 含义: 函数独有的能力。你平时能写
Foo.call()或Foo.apply(),就是因为在这个原型层级里找到了这些内置方法。
- 含义: 函数独有的能力。你平时能写
constructor: ƒ Function()- 含义: 说明函数
Foo是由 JavaScript 内置的Function构造函数创建出来的。
- 含义: 说明函数
toString: ƒ toString()- 含义: 将函数源码转为字符串的方法(如
Foo.toString())。
- 含义: 将函数源码转为字符串的方法(如
▼ [[Prototype]]: Object- 含义:
Function.prototype本身也是个对象,它的原型同样指向顶级Object.prototype。
- 含义:
▼ [[Scopes]]: Scopes[0]- 含义:
Function.prototype的作用域链。
- 含义:
第五块:▼ [[Scopes]]: Scopes[1](Foo 函数的作用域链)
0: Global {window: ...}- 含义: 引擎记录的当前函数生效的上下文环境。这里显示它能访问全局的
window对象。如果是闭包函数,这里还会看到Closure作用域。
- 含义: 引擎记录的当前函数生效的上下文环境。这里显示它能访问全局的
在控制台里看函数,记住两行关键展开项即可:
- 寻找
prototype, 看实例能调用的属性/方法(包含sayHi)。 - 寻找
[[Prototype]], 看函数自己能调用的属性/方法(包含call/apply/bind)。
prototype、[[Prototype]]与_proto_的关系
用最简单的话描述它们关系:
[[Prototype]]是真正的内存槽,__proto__是访问它的后门通道,而prototype只是函数手里拿着的预留仓库。1、
[[Prototype]]的正式定义
[[Prototype]]的正式名称是“对象的内部槽”(Internal Slot),更具体的称呼是“内部原型”。在 ECMAScript 官方规范中:
- 特征: 双方括号
[[...]]表示这是 规范内部的私有属性,在传统的 JS 代码中是无法直接像obj.[[Prototype]]这样书写和访问的。- 正式名称:
[[Prototype]]内部槽(Internal Slot)。- 含义: 它是存在于每个 JavaScript 对象内部、由引擎维护的一个私有槽位,用来保存该对象的原型引用(即指向上一级原型对象或
null)。2、
__proto__与[[Prototype]]的关系简而言之:
__proto__只是访问和修改内部槽[[Prototype]]的一种“历史遗留通道/访问器”。映射关系:
[[Prototype]]是真正的原型存储槽(存在于内存深处)。__proto__是挂在Object.prototype上的一个 getter / setter 访问器属性(属性拦截器)。当读取obj.__proto__时,底层实际上是在执行Object.getPrototypeOf(obj),去获取真正的[[Prototype]]。历史与规范地位:
- 历史:
__proto__最初只是浏览器厂商(主要是 Firefox/Chrome)私自添加的非标准扩展功能。- 现实: 到了 ES6,为了兼容网络上大量已经使用了
__proto__的老旧代码,官方规范不得不把它收录进标准,但明确将它放在了 附录 B(Annex B – 仅为了 Web 兼容性保留的特性) 中。由于
__proto__性能较差(修改它会破坏引擎优化)且属于废弃边缘的附录规范,现代 JS 严格禁止在生产环境中使用__proto__。规范推荐使用以下 官方标准 API 来操作
[[Prototype]]:
操作需求 不推荐(历史遗留) 官方标准 API(推荐 ) 获取 [[Prototype]]obj.__proto__Object.getPrototypeOf(obj)修改 [[Prototype]]obj.__proto__ = newProtoObject.setPrototypeOf(obj, newProto)创建时指定 [[Prototype]]– Object.create(proto)3.三者核心属性速查表
假设我们定义一个构造函数,并用它创建一个实例:
function Person() {} const p = new Person();它们三者在内存中的关系可以通过这个联动公式串联起来:
实例 p 的内部槽 实例 p 的访问后门 构造函数的预留仓库 p.[[Prototype]] === p.__proto__ === Person.prototype完整的逻辑接力过程:
Person.prototype预备资源: 函数Person诞生时,引擎会自动给它配一个prototype对象。你可以在上面挂载方法(如Person.prototype.sayHi = ...)。new操作做连接: 当你执行new Person()时,引擎创建了实例p,并把p内部的[[Prototype]]槽位直接指向了Person.prototype。__proto__负责读取: 由于[[Prototype]]是私有槽位,在代码里写p.[[Prototype]]会报错,所以你可以通过p.__proto__这个后门去读取或修改底层指向的Person.prototype。
属性名称 谁拥有它? 它是什么? 它的真实作用 [[Prototype]]所有对象(普通对象、函数、数组等) 内部私有槽位(存储在引擎深处) 真正的原型指针。原型链查找时,引擎就是沿着它一路向上找的。 __proto__所有对象(通过继承 Object.prototype获得)访问器属性(getter/setter 后门) 读取或修改 [[Prototype]]的历史通道。只是一个指向[[Prototype]]的快捷接口。prototype仅函数对象(普通对象没有) 普通属性(挂在函数上的对象) 给未来实例准备的模板仓库。当你 new Foo()时,新对象的[[Prototype]]就会连到这里。
函数的双重身份与两条原型链

二、 两种查找路径的具体对比
为了彻底理清,我们看看这两条链在查找时到底有什么区别:
1. 当函数扮演“普通对象/方法本身”时(寻找 Foo.xxx)
此时,Foo 和一个 {} 没有任何本质区别,引擎直接从 Foo 自身的 [[Prototype]] 开始向上找:
- 查找路线:
Foo$\rightarrow$Foo.[[Prototype]](指向Bar或Function.prototype) $\rightarrow$Object.prototype$\rightarrow$null - 触发场景:
Foo.call()、Foo.bind()、Foo.staticMethod()
2. 当函数扮演“构造工厂”时(给实例 f1 找 f1.xxx)
此时,Foo 只是一个“借用身份的工具人”,真正参与原型链查找的是它手里拿着的那个 Foo.prototype(一个纯普通对象):
- 查找路线:
f1$\rightarrow$Foo.prototype$\rightarrow$Foo.prototype.[[Prototype]](指向Bar.prototype) $\rightarrow$Object.prototype$\rightarrow$null - 触发场景:
const f1 = new Foo(); f1.sayHi()
三、 总结:底层原理相同,只是“发起点”不同
所以,说“函数的原型链查找方式和普通对象根本不同”:
- 底层机制上: 不同(完全相同)。 无论是函数还是普通对象,引擎在找属性时,永远只认“当前对象”的
[[Prototype]]这一根线,没有任何特权或特殊的查找算法。 - 应用逻辑上: 对(确实要区分)。 普通对象只有一条单向链;而函数因为既有
[[Prototype]]又有prototype属性,它同时身处两条互相平行的原型链中。