编程语言研究书库

09 数组、Proxy 与 symbol:特殊对象的三个样本

09 数组、Proxy 与 symbol:特殊对象的三个样本

这一章讲什么

对象章的收尾篇,看三个「特殊对象」。数组演示了什么叫做 「仍是对象,但引擎给你开了小灶」;Proxy 演示了对象的规则本身可以被拦截 和改写——不是给某个属性挂 getter,而是给「读、写、删、枚举」这些基本动作 全部装上闸门;symbol 属性则回到 06 章埋的伏笔,看语言怎么拿 symbol 给自己(以及各大框架)留永不撞名的扩展点。三者都不长,但各自是一类大系统的地基。

核心原理

3.1 数组:对象 + 三条特殊规则

数组就是为存有序集合而优化的对象。三条特殊规则1:

  1. 用数字下标当键(术语叫索引属性),而不是字符串或 symbol;
  2. 一个自动维护的 length 属性,元素增删它自动跟着变;
  3. 从 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:

  1. 不出现在枚举里:Object.keys(drawer) 列得出 'myLibrary_toString', 列不出 symbol 属性——实现细节自动隐身;
  2. 无法预知撞名:给第三方对象或内置对象加功能,字符串名可能跟现在或将来的 代码撞;symbol 从机制上杜绝;
  3. 没有 symbol 就摸不到:不知道 symbol 本身,就访问不到这个属性; 字符串属性藏不住。

语言自己就是这个模式的最大用户。知名符号(well-known symbols) 是挂在 Symbol 上的几个全局 symbol,每个控制语言的一种行为14:

知名符号控制什么
Symbol.toStringTagObject.prototype.toString() 的输出:定成 'Drawer' 后返回 '[object Drawer]'15
Symbol.iterator对象能否用于 for...of
Symbol.asyncIterator能否用于 for await
Symbol.hasInstanceinstanceof 的行为
Symbol.toPrimitive对象转原始值时的自定义逻辑(06 章转换链的定制入口)

Symbol.toStringTag 和自定义 toString() 的分工书里点明了: 前者管「类型识别」(Object.prototype.toString.call(obj) 的结果), 后者管「转字符串时的实用输出」;两者互补,平时 toString() 用得多得多16。

这套设计的深意值得单独一句:给语言加新功能(比如 for...of)时, 如果用一个字符串属性 'iterator',天知道哪个老网站的代码已经用了这个名字—— 一上线就炸。知名符号从机制上保证新钩子永远撞不到旧代码17。 「不弄坏 Web」这个工程约束,在语言设计层面就是这么兑现的。

作者的判断与证据

有证据的判断:

  • 数组继承演示、Proxy 例程,都是可直接运行的代码,状态可复现18。
  • Vue 3/Cloudflare/Node.js 对 Proxy 的使用是公开事实,书中点到为止19。

作者的主张:

  • 「symbol 属性比前缀字符串好」的三条理由,是作者对编辑质疑的答辩, 属于设计层面的论证而非实测;
  • 「多数应用代码不需要亲手造 symbol」(06 章)在本章得到呼应, 但「库作者应该用 symbol」是作者的设计立场。

判断(我们的,不是书里的): Proxy 的「强制透明」是语言设计里少见的强保证—— 「你不可能知道自己拿到的是不是代理」让代理可以放心替换任何对象, 这是 Vue 3 响应式这类「无感拦截」能成立的地基;代价是调试时无法在类型层面 分辨拦截层,只能靠行为差异去猜。 如果错,会错在: 透明性保证的是「代码分不出来」,但性能分析器与某些调试工具 仍能观察到代理的存在——若读者把「透明」理解成「零成本」,就会低估它在热路径上的 调用开销(本章边界一节自己也承认这一点未展开)。

边界与局限

  • Proxy 陷阱的种类有十来个(get/set/has/deleteProperty/ownKeys/…), 原书只演示了两个最常用的;Proxy 与 Reflect 的配合、revocable Proxy (可注销代理)都没出现。
  • Proxy 的性能代价(每次操作多一次函数调用(跳去那个函数里执行一遍)、阻碍部分内联优化)全书未提—— 这是 10 章优化叙事的一个真实但缺失的对照面。
  • 数组一章明确说「不深讲」:中间大量空缺的数组、类数组对象、TypedArray 的关系 都不在范围(后者要等不存在的「二进制数据」章)。
  • Symbol.toPrimitive 只列了名,定制转换优先级的机制未展开。

可带走的

  1. 数组 = 对象 + 整数下标 + 自动 length + 一套专用方法;下标属性也能被原型链继承。
  2. 引擎把数组元素和命名属性分开存——下标访问快的原因,也是别乱挂属性的原因。
  3. Proxy = 给对象的全部基本操作装陷阱;对外完全透明,无法检测。
  4. Proxy 是基础设施:process.env、Vue 3 响应式、Cloudflare RPC。
  5. symbol 属性三优势:不撞名、不进枚举、没有 symbol 摸不到。
  6. 知名符号是语言的扩展点:Symbol.iterator 让 for...of 成为可能而不弄坏旧站。
  7. Symbol.toStringTag 管类型识别,toString() 管实用输出,两者互补。

原文地图

主题原书章原文位置
数组的三条特殊规则5.5 Arraystext/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 objectstext/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]' 默认 toString5.7 Converting objects to strings and symbol propertiestext/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」)

Footnotes

  1. 出处:「5.5 Arrays」第 7 段(text/20-ch05-05-5-5-arrays.txt:7,搜「numeric indices」)。 ↩

  2. 出处:「5.5 Arrays」第 79 段(text/20-ch05-05-5-5-arrays.txt:79,搜「Array.prototype」)。「这些方法理解并操纵 length」在同段。 ↩

  3. 出处:「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 段。 ↩

  4. 出处:「5.5 Arrays」第 76 段(text/20-ch05-05-5-5-arrays.txt:76,搜「contiguous memory」)。 ↩

  5. 出处:「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」在同段。 ↩

  6. 出处:「5.6 Proxy objects」第 19 段(text/21-ch05-06-5-6-proxy-objects.txt:19,搜「impossible to know」)。 ↩

  7. 出处:「5.6 Proxy objects」第 38–77 段(text/21-ch05-06-5-6-proxy-objects.txt:54,搜「new Proxy(originalDrawer」)。 ↩

  8. 出处:「5.6 Proxy objects」第 90–101 段(text/21-ch05-06-5-6-proxy-objects.txt:91,搜「#B」注释「Returns 'normal'」;:101 搜「Returns 'high'」)。 ↩

  9. 出处:「5.6 Proxy objects」第 111 段(text/21-ch05-06-5-6-proxy-objects.txt:111,搜「encountered more often than you may expect」)。 ↩

  10. 出处:「5.6 Proxy objects」第 117 段与第 117 段(text/21-ch05-06-5-6-proxy-objects.txt:117,搜「Vue.js 3 reactivity」;:123 搜「remote procedure call」)。 ↩

  11. 出处:「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」)。 ↩

  12. 出处:「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」)。 ↩

  13. 出处:「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」)。 ↩

  14. 出处:「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」)。 ↩

  15. 出处:「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]」)。 ↩

  16. 出处:「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」)。 ↩

  17. 出处:「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」)。 ↩

  18. 出处:「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」)。 ↩

  19. 出处:「5.6 Proxy objects」第 111–128 段(text/21-ch05-06-5-6-proxy-objects.txt:111,搜「process.env object」)。 ↩

On this page