深入探究Prototype与原型链

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。
  • [[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 作用域。

在控制台里看函数,记住两行关键展开项即可:

  1. 寻找 prototype, 看实例能调用的属性/方法(包含 sayHi)。
  2. 寻找 [[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

完整的逻辑接力过程:

  1. Person.prototype 预备资源: 函数 Person 诞生时,引擎会自动给它配一个 prototype 对象。你可以在上面挂载方法(如 Person.prototype.sayHi = ...)。
  2. new 操作做连接: 当你执行 new Person() 时,引擎创建了实例 p,并把 p 内部的 [[Prototype]] 槽位直接指向了 Person.prototype。
  3. __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 属性,它同时身处两条互相平行的原型链中。

彻底理解JavaScript中的对象

JS对象迷惑性:理解本质

对于C++、Java、C#面向对象的语言而言,对象这个词都是很明确的:“对象”必须以“类(Class)”为模板实例化而来。

而JS中的对象概念,则宽泛得多。例如,以下代码中的staff 、p都是对象,甚至函数Person也是对象:

//普通对象
const staff = {
    name: "Tom",
    age: 18
};

//Promise对象
const p = new Promise(...);

//函数也是一个对象
function Person(name) {
  this.name = name;
}

这不禁让人困惑,JavaScript的对象到底指的是什么?

因为中文把 Object 翻译成“对象”,听起来特别抽象,实际上:

JS 里的“对象”并不是指某一种长相,而是指一种“可以被当成一个整体来管理,并且可以通过属性名找到内部东西”的值。

“对象”本质上只是一个包含键值对(Key-Value)的动态哈希表。

很粗浅的理解,对象就是一个可以装很多“属性”的东西。

认识了这个本质,我们很快就能理解了,staff,p,Person 长相完全不同,但它们在 JavaScript 的底层分类上都属于 Object。以下都是JS中的对象,只是创建它们的“模板”不同而已:

JavaScript Object
│
├── 普通对象
├── Promise对象
├── Array对象
├── Date对象
├── RegExp对象
├── Map对象
└── …

创建对象有很多种方式

例如,普通对象:

 const a = {};

其它类型对象:

 //Promise 对象
 const b = new Promise(...);
 //Array 对象。
 const c = [];
 //Date 对象
 const d = new Date();
 //Map 对象
 const e = new Map();
 //RegExp 对象
 const f = /abc/;
  //甚至得到的是函数对象
 const g = function() {};

不要再把对象单一理解成:

 { name: “Tom”, age: 18 }

而应该理解成:

对象 = 一个可以拥有属性和方法、可以被独立操作的实体。

于是:

                  Object
                    │
        ┌───────────┼────────────┐
        ↓           ↓           ↓
      普通对象     Array       Promise
        │           │           │
    name/age     push/length   then/catch

它们虽然功能完全不同,但都属于“对象”这个大类别。

HTTP方法的深度探源与RESTful API的设计思想

HTTP认知溯源

HTTP方法本质上就是一个普通的 ASCII 编码字符串,我们可以自定义HTTP方法。从这一点看每个http方法没有什么区别。但是,在应用层,考虑到缓存与网络效能、容错与重试机制、安全隔离与攻击防御等,浏览器、路由器、防火墙、CDN节点、NGINX等应用对各方法做了区别对待。

HTTP方法是在安全和幂等的理念下设计的,语义化的命名有助于互联网生态的协作,在缓存、爬虫、搜索、CDN分发、前后端数据交互等方面能更低成本协作。而REST则是一种设计API的理念,即使用 URI 来标识资源,使用标准的 HTTP 方法 来表示操作。RESTful则是符合这种理念的API。

即使写程序很多年的老鸟,能真正说清楚道明白HTTP方法的也不多,多数都只会使用而不知道所以然。每个人都知道使用GET、POST方法,使用也很熟练,但是底层设计思想并不一定了解。这里,来认真梳理一下HTTP方法背后设计的逻辑。

一. HTTP方法常见10种

HTTP 标准里常见的10 个:

方法主要作用
GET获取资源
POST提交数据 / 创建资源
PUT完整替换资源
PATCH部分修改资源
DELETE删除资源
HEAD获取响应头,不返回正文
OPTIONS查询服务器支持哪些方法/能力
CONNECT建立隧道
TRACE请求回显,用于诊断
QUERY用于查询搜索(2026年RFC10008新增)

(实际上HTTP方法数量多达几十种,这里只是列举常见的方法,详见文末)

二.HTTP方法两个核心设计理念:安全(Safe) 与 幂等(Idempotent)


注意:以上的标准是2014年颁布的RFC 7231,QUETY标准出自2026颁布的RFC 10008