09 数组、Proxy 与 symbol:特殊对象的三个样本¶
这一章讲什么¶
对象章的收尾篇,看三个「特殊对象」。数组演示了什么叫做 「仍是对象,但引擎给你开了小灶」;Proxy 演示了对象的规则本身可以被拦截 和改写——不是给某个属性挂 getter,而是给「读、写、删、枚举」这些基本动作 全部装上闸门;symbol 属性则回到 06 章埋的伏笔,看语言怎么拿 symbol 给自己(以及各大框架)留永不撞名的扩展点。三者都不长,但各自是一类大系统的地基。
核心原理¶
3.1 数组:对象 + 三条特殊规则¶
数组就是为存有序集合而优化的对象。三条特殊规则1:
- 用数字下标当键(术语叫索引属性),而不是字符串或 symbol;
- 一个自动维护的
length属性,元素增删它自动跟着变; - 从
Array.prototype继承一批懂 length 的方法:push、pop、map、filter2。
但「仍是对象」这句话不是修辞。索引属性和普通属性一样能被继承——
书里有个让人头皮发麻的合法演示:array2 的原型被设成 array1 之后,
array2[0] 读出的是 array1[0] 的值,因为下标 0 在 array2 自己身上没有,
沿原型链到 array1 上找到了3。数 组之间搞继承在生产代码里是怪物,
但这个演示把「数组只是对象」坐实到了骨头里。
引擎层还有一条优化:数组的元素和命名属性分开存,元素尽量放在连续的内存里, 下标访问因此可以按偏移直接算,不用查表4。这也是「别往数组上乱挂命名属性、 别混着用」的性能依据。
3.2 Proxy:拦截对象的全部基本操作¶
普通对象的规则是焊死的:读就是读,删就是删。你只能给单个属性装 getter/setter, 拦不住「访问一个不存在的属性」这种操作。Proxy 换了个思路: 它包住另一个对象(叫目标对象),你在它身上做的每一个基本操作——读、写、删、 枚举——都被一个叫陷阱(trap)的函数先接到,由你决定怎么办5。
更狠的是它的透明度:语言刻意规定,没有任何办法能看出一个对象是不是 Proxy—— 调用方完全无感6。这意味着拦截可以悄无声息地藏在任何 API 后面。
主线例子:带安检的抽屉。 照原书例子,给抽屉装两个陷阱7:
const originalDrawer = { contents: 'tax documents', isLocked: false };
const dynamicDrawer = new Proxy(originalDrawer, {
get(target, property, receiver) { // 读陷阱
console.log(`Accessing: ${property}`);
if (property === 'securityLevel') { // 凭空造一个属性!
return target.contents === 'classified' ? 'high' : 'normal';
}
return target[property];
},
set(target, property, value, receiver) { // 写陷阱
if (property === 'contents' && typeof value !== 'string') {
throw new Error('Contents must be a string'); // 拦不住的写入,现在拦得住
}
target[property] = value;
return true;
}
});
console.log(dynamicDrawer.contents); // 日志:Accessing: contents → 'tax documents'
console.log(dynamicDrawer.securityLevel); // 日志:Accessing: securityLevel → 'normal'(造出来的!)
dynamicDrawer.contents = 'classified'; // 写入合法,放行
console.log(dynamicDrawer.securityLevel); // → 'high'(跟着内容变)
逐步看状态:读 contents 先进 get 陷阱打日志(记一条运行记录),再正常返回;
读 securityLevel——原对象上根本没这个属性——陷阱现场计算返回 'normal';
写入 'classified' 过了类型检查;再读安全级别,内容已变,返回 'high'。
普通对象做不出「属性不存在却能读出值」这种事,Proxy 可以8。
Proxy 不是奇技淫巧,是大系统的承重墙,书里点了三座9:
Node.js 的 process.env 本身就是一种 Proxy(07 章的谜案在这里合上最后一环);
Vue 3 的响应式:读属性时记录依赖,改属性时触发重渲染,全靠 Proxy 陷阱;
Cloudflare Workers 用更 exotic 的 Proxy 形式,让远端的东西在代码里
表现得像本地对象一样10。天天用,只是看不见。
3.3 symbol 属性落地:为什么不撞名,为什么不现身¶
06 章讲了 symbol 保证唯一;这一章看它在对象上的实际用法。
回到那个老问题:String(drawer) 输出 '[object Object]'——对象从
Object.prototype 继承的默认 toString() 只会说「我是个对象」11。
库想自定义它,直接给对象加个 toString() 方法?两个库都加,互相覆盖。
这正是 symbol 属性的用武之地:symbol 属性与字符串属性永不冲突,
哪怕描述串相同12。
symbol 属性比「加个前缀的字符串属性」(如 myLibrary_toString)好在哪?
书里技术编辑的质疑引出了三条硬理由13:
- 不出现在枚举里:
Object.keys(drawer)列得出'myLibrary_toString', 列不出 symbol 属性——实现细节自动隐身; - 无法预知撞名:给第三方对象或内置对象加功能,字符串名可能跟现在或将来的 代码撞;symbol 从机制上杜绝;
- 没有 symbol 就摸不到:不知道 symbol 本身,就访问不到这个属性; 字符串属性藏不住。
语言自己就是这个模式的最大用户。知名符号(well-known symbols)
是挂在 Symbol 上的几个全局 symbol,每个控制语言的一种行为14:
| 知名符号 | 控制什么 |
|---|---|
Symbol.toStringTag |
Object.prototype.toString() 的输出:定成 'Drawer' 后返回 '[object Drawer]'15 |
Symbol.iterator |
对象能否用于 for...of |
Symbol.asyncIterator |
能否用于 for await |
Symbol.hasInstance |
instanceof 的行为 |
Symbol.toPrimitive |
对象转原始值时的自定义逻辑(06 章转换链的定制入口) |
Symbol.toStringTag 和自定义 toString() 的分工书里点明了:
前者管「类型识别」(Object.prototype.toString.call(obj) 的结果),
后者管「转字符串时的实用输出」;两者互补,平时 toString() 用得多得多16。
这套设计的深意值得单独一句:给语言加新功能(比如 for...of)时,
如果用一个字符串属性 'iterator',天知道哪个老网站的代码已经用了这个名字——
一上线就炸。知名符号从机制上保证新钩子永远撞不到旧代码17。
「不弄坏 Web」这个工程约束,在语言设计层面就是这么兑现的。
作者的判断与证据¶
有证据的判断:
作者的主张:
- 「symbol 属性比前缀字符串好」的三条理由,是作者对编辑质疑的答辩, 属于设计层面的论证而非实测;
- 「多数应用代码不需要亲手造 symbol」(06 章)在本章得到呼应, 但「库作者应该用 symbol」是作者的设计立场。
判断(我们的,不是书里的): Proxy 的「强制透明」是语言设计里少见的强保证—— 「你不可能知道自己拿到的是不是代理」让代理可以放心替换任何对象, 这是 Vue 3 响应式这类「无感拦截」能成立的地基;代价是调试时无法在类型层面 分辨拦截层,只能靠行为差异去猜。 如果错,会错在: 透明性保证的是「代码分不出来」,但性能分析器与某些调试工具 仍能观察到代理的存在——若读者把「透明」理解成「零成本」,就会低估它在热路径上的 调用开销(本章边界一节自己也承认这一点未展开)。
边界与局限¶
- Proxy 陷阱的种类有十来个(get/set/has/deleteProperty/ownKeys/…), 原书只演示了两个最常用的;Proxy 与 Reflect 的配合、revocable Proxy (可注销代理)都没出现。
- Proxy 的性能代价(每次操作多一次函数调用(跳去那个函数里执行一遍)、阻碍部分内联优化)全书未提—— 这是 10 章优化叙事的一个真实但缺失的对照面。
- 数组一章明确说「不深讲」:中间大量空缺的数组、类数组对象、TypedArray 的关系 都不在范围(后者要等不存在的「二进制数据」章)。
Symbol.toPrimitive只列了名,定制转换优先级的机制未展开。
可带走的¶
- 数组 = 对象 + 整数下标 + 自动 length + 一套专用方法;下标属性也能被原型链继承。
- 引擎把数组元素和命名属性分开存——下标访问快的原因,也是别乱挂属性的原因。
- Proxy = 给对象的全部基本操作装陷阱;对外完全透明,无法检测。
- Proxy 是基础设施:process.env、Vue 3 响应式、Cloudflare RPC。
- symbol 属性三优势:不撞名、不进枚举、没有 symbol 摸不到。
- 知名符号是语言的扩展点:Symbol.iterator 让 for...of 成为可能而不弄坏旧站。
- Symbol.toStringTag 管类型识别,toString() 管实用输出,两者互补。
原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 数组的三条特殊规则 | 5.5 Arrays | text/20-ch05-05-5-5-arrays.txt:7(搜「ordered collections」) |
| 数组仍是对象、索引属性可继承 | 同上 | text/20-ch05-05-5-5-arrays.txt:40(搜「still just objects」) · :51(搜「setPrototypeOf(array2」) |
| 元素与命名属性分开存 | 同上 | text/20-ch05-05-5-5-arrays.txt:76(搜「separately from named properties」) |
| Array.prototype 方法 | 同上 | text/20-ch05-05-5-5-arrays.txt:79(搜「push, pop, map」) |
| Proxy 与陷阱、透明性 | 5.6 Proxy objects | text/21-ch05-06-5-6-proxy-objects.txt:19(搜「traps」) · :19(搜「impossible to know」) |
| 抽屉代理例程 | 同上 | text/21-ch05-06-5-6-proxy-objects.txt:38(搜「originalDrawer」) · :54(搜「dynamicDrawer」) · :91(搜「securityLevel」) |
| 三个真实用户 | 同上 | text/21-ch05-06-5-6-proxy-objects.txt:111(搜「process.env object」) · :117(搜「Vue.js 3 reactivity」) · :123(搜「Cloudflare」) |
| '[object Object]' 默认 toString | 5.7 Converting objects to strings and symbol properties | text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:27(搜「frustration for developers」) |
| symbol 属性不撞名 | 同上 | text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:52(搜「never collide」) |
| 三条优势(对编辑的答辩) | 同上 | text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:80(搜「do not appear in enumerations」) |
| Symbol.toStringTag | 同上 | text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:106(搜「symbol property that controls it」) · :133(搜「only affects」) |
| 知名符号清单 | 同上 | text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:165(搜「Symbol.iterator」) |
| 「不弄坏 Web」 | 同上 | text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:182(搜「break existing code」) |
-
出处:「5.5 Arrays」第 7 段(
text/20-ch05-05-5-5-arrays.txt:7,搜「numeric indices」)。 ↩ -
出处:「5.5 Arrays」第 79 段(
text/20-ch05-05-5-5-arrays.txt:79,搜「Array.prototype」)。「这些方法理解并操纵 length」在同段。 ↩ -
出处:「5.5 Arrays」第 40–56 段(
text/20-ch05-05-5-5-arrays.txt:47,搜「new Array(1)」;:51搜「Object.setPrototypeOf(array2」;:52的注释'hello'即继承自 array1)。「act similarly to properties and can even be inherited」出自第 40 段。 ↩ -
出处:「5.5 Arrays」第 76 段(
text/20-ch05-05-5-5-arrays.txt:76,搜「contiguous memory」)。 ↩ -
出处:「5.6 Proxy objects」第 19 段(
text/21-ch05-06-5-6-proxy-objects.txt:19,搜「handler functions called traps」)。「Every attempt to read, write, delete, or enumerate」在同段。 ↩ -
出处:「5.6 Proxy objects」第 19 段(
text/21-ch05-06-5-6-proxy-objects.txt:19,搜「impossible to know」)。 ↩ -
出处:「5.6 Proxy objects」第 38–77 段(
text/21-ch05-06-5-6-proxy-objects.txt:54,搜「new Proxy(originalDrawer」)。 ↩ -
出处:「5.6 Proxy objects」第 90–101 段(
text/21-ch05-06-5-6-proxy-objects.txt:91,搜「#B」注释「Returns 'normal'」;:101搜「Returns 'high'」)。 ↩ -
出处:「5.6 Proxy objects」第 111 段(
text/21-ch05-06-5-6-proxy-objects.txt:111,搜「encountered more often than you may expect」)。 ↩ -
出处:「5.6 Proxy objects」第 117 段与第 117 段(
text/21-ch05-06-5-6-proxy-objects.txt:117,搜「Vue.js 3 reactivity」;:123搜「remote procedure call」)。 ↩ -
出处:「5.7 Converting objects to strings and symbol properties」第 27 段(
text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:27,搜「default (and somewhat less than useful」)。 ↩ -
出处:「5.7 Converting objects to strings and symbol properties」第 52 段(
text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:52,搜「never collide with a string property」)。 ↩ -
出处:「5.7 Converting objects to strings and symbol properties」第 80–98 段(
text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:80,搜「prefixed property name」)。 ↩ -
出处:「5.7 Converting objects to strings and symbol properties」第 159–177 段(
text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:159,搜「well-known symbols」)。 ↩ -
出处:「5.7 Converting objects to strings and symbol properties」第 106–123 段(
text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:116,搜「get [Symbol.toStringTag]」;:121搜「[object Drawer]」)。 ↩ -
出处:「5.7 Converting objects to strings and symbol properties」第 133–147 段(
text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:133,搜「only affects」;:147搜「complementary」)。 ↩ -
出处:「5.7 Converting objects to strings and symbol properties」第 182 段(
text/22-ch05-07-5-7-converting-objects-to-strings-and-symbol-pro.txt:182,搜「would break existing code」)。 ↩ -
出处:「5.5 Arrays」第 47–56 段(
text/20-ch05-05-5-5-arrays.txt:47,搜「new Array(1)」);「5.6 Proxy objects」第 54–101 段(text/21-ch05-06-5-6-proxy-objects.txt:54,搜「new Proxy(originalDrawer」)。 ↩ -
出处:「5.6 Proxy objects」第 111–128 段(
text/21-ch05-06-5-6-proxy-objects.txt:111,搜「process.env object」)。 ↩