PhysChen.com
主页
物理
笔记 科普 研究
教学
IB 课程
编程
笔记 项目
随笔
所感 所思
摄影
Shenzhen Portrait Cats Others Wuhan Japan
关于
主页
物理
笔记 科普 研究
编程
笔记 项目
摄影
Shenzhen Portrait Cats Others Wuhan Japan
教学
IB 课程
随笔
所感 所思
关于
文章目录
    JavaScript 7. 现代语法、协议与元编程 陈华的个人主页

    文章信息

    • 标题: JavaScript 7. 现代语法、协议与元编程
    • 发布时间: 2026 年 7 月 19 日
    • 来源: https://physchen.com/zh-Hans/programming/notes/javascript-modern-syntax-protocols-and-metaprogramming/
    • 摘要: 系统介绍现代 JavaScript 的解构与展开语法、模板字面量、迭代协议、生成器、Iterator Helpers、异步迭代、Proxy、Reflect、动态代码执行与 Intl 国际化接口。

    目录

      JavaScript 7. 现代语法、协议与元编程

      发布于 2026 年 7 月 19 日
      • JavaScript 基础
      • JavaScript
      • ECMAScript
      • Meta-programming

      “⁠ES6+⁠”是工程实践中的非正式说法⁠,通常泛指 ECMAScript 2015 及其后的语言特性⁠。ECMAScript 2015 曾经是一次规模较大的版本更新⁠,此后 ECMAScript 改为按年度发布规范⁠,因此现代 JavaScript 并不是一组一次性加入⁠、边界固定的“⁠ES6 语法⁠”⁠。

      前面的章节已经系统介绍了若干现代特性⁠:

      • 第一章⁠:let⁠、const⁠、模板字面量基础⁠、可选链和空值合并⁠;
      • 第二章⁠:箭头函数⁠、剩余参数和函数作用域⁠;
      • 第四章⁠:Promise⁠、async 和 await⁠;
      • 第五章⁠:Symbol⁠、Map⁠、Set 与现代数组方法⁠;
      • 第六章⁠:类⁠、私有元素和静态成员⁠;
      • 第九章⁠:ES Modules 和动态导入⁠。

      本章不重复这些内容⁠,而是集中讨论现代 JavaScript 中更具组合性的语言机制⁠:

      1. 解构赋值与展开语法⁠;
      2. 标签模板⁠;
      3. 可迭代协议与迭代器协议⁠;
      4. 生成器与异步生成器⁠;
      5. ECMAScript 2025 引入的 Iterator Helpers⁠;
      6. Proxy 与 Reflect⁠;
      7. globalThis⁠、new Function() 与 eval()⁠;
      8. Intl 国际化接口⁠;
      9. 年度标准与运行环境兼容性⁠。

      这些机制的共同特点是⁠:它们并不只提供更短的语法⁠,还建立了对象与语言结构之间的协议⁠。例如⁠,只要对象实现了 [Symbol.iterator]()⁠,它就可以被 for...of⁠、数组解构和展开语法消费⁠;只要代理实现相应陷阱⁠,就可以参与属性访问⁠、定义⁠、删除和构造等对象操作⁠。

      需要继续区分语言规范与引擎实现⁠。较新的语法并不必然比旧写法更快⁠,解构不意味着“⁠底层快速寻址⁠”⁠,箭头函数也不意味着减少执行上下文成本⁠。性能取决于运行环境⁠、数据结构⁠、优化状态和具体工作负载⁠,应通过测量确认⁠。

      7.1 解构赋值

      解构赋值(⁠Destructuring Assignment⁠)使用一种结构模式⁠,从可迭代对象或普通对象中读取值并绑定到变量⁠。

      数组式解构依据迭代协议读取值⁠,对象式解构依据属性键读取属性⁠。二者的语义不同⁠。

      7.2 数组式解构

      7.2.1 基本形式

      const values = [
        10,
        20,
        30,
      ];
      
      const [
        first,
        second,
        third,
      ] = values;
      
      console.log(first);  // 10
      console.log(second); // 20
      console.log(third);  // 30

      左侧看起来像数组字面量⁠,但它是解构绑定模式⁠。

      7.2.2 数组解构依赖可迭代协议

      数组式解构并不要求右侧一定是数组⁠。只要右侧是可迭代对象⁠,就可以按迭代顺序读取⁠:

      const [
        first,
        second,
      ] = new Set([
        "A",
        "B",
        "C",
      ]);
      
      console.log(first);  // A
      console.log(second); // B

      字符串也可迭代⁠:

      const [
        firstCharacter,
        secondCharacter,
      ] = "JavaScript";
      
      console.log(firstCharacter);  // J
      console.log(secondCharacter); // a

      普通对象默认不可迭代⁠:

      const user = {
        name: "Alice",
        age: 30,
      };
      
      // TypeError
      // const [name, age] = user;

      需要按属性名读取普通对象时⁠,应使用对象式解构⁠。

      7.2.3 跳过元素

      const [
        first,
        ,
        third,
      ] = [
        10,
        20,
        30,
      ];
      
      console.log(first); // 10
      console.log(third); // 30

      空位置仍会推进迭代器⁠,只是不把对应值绑定到变量⁠。

      7.2.4 默认值

      默认值只在迭代得到 undefined 时使用⁠:

      const [
        first = 10,
        second = 20,
      ] = [
        undefined,
        null,
      ];
      
      console.log(first);  // 10
      console.log(second); // null

      null⁠、0⁠、空字符串和 false 都不会触发默认值⁠。

      默认表达式按需求值⁠:

      function createDefault() {
        console.log("default evaluated");
        return 10;
      }
      
      const [
        value = createDefault(),
      ] = [5];
      
      console.log(value); // 5

      createDefault() 不会执行⁠。

      7.2.5 剩余元素

      数组解构的剩余元素必须位于模式末尾⁠:

      const [
        first,
        second,
        ...remaining,
      ] = [
        10,
        20,
        30,
        40,
      ];
      
      console.log(remaining);
      // [30, 40]

      剩余元素始终创建普通数组⁠。

      以下语法非法⁠:

      // SyntaxError
      // const [
      //   first,
      //   ...remaining,
      //   last,
      // ] = values;

      7.2.6 嵌套解构

      const data = [
        "Alice",
        [
          90,
          95,
        ],
      ];
      
      const [
        name,
        [
          physicsScore,
          mathematicsScore,
        ],
      ] = data;
      
      console.log(name);             // Alice
      console.log(physicsScore);     // 90
      console.log(mathematicsScore); // 95

      嵌套层级过深会降低可读性⁠。数据来自网络或外部输入时⁠,还应先验证结构⁠,避免解构过程中抛出错误⁠。

      7.2.7 交换变量

      let first = 10;
      let second = 20;
      
      [
        first,
        second,
      ] = [
        second,
        first,
      ];
      
      console.log(first);  // 20
      console.log(second); // 10

      右侧先求值并形成可迭代结果⁠,随后再对左侧目标赋值⁠。

      7.2.8 读取函数返回值

      function divide(
        dividend,
        divisor,
      ) {
        return [
          Math.trunc(
            dividend / divisor,
          ),
          dividend % divisor,
        ];
      }
      
      const [
        quotient,
        remainder,
      ] = divide(17, 5);
      
      console.log(quotient);  // 3
      console.log(remainder); // 2

      如果返回值具有明确字段语义⁠,对象往往比数组更容易阅读⁠:

      function divide(
        dividend,
        divisor,
      ) {
        return {
          quotient: Math.trunc(
            dividend / divisor,
          ),
          remainder: (
            dividend % divisor
          ),
        };
      }

      7.2.9 解构会提前关闭迭代器

      数组解构只读取模式所需的值⁠。读取完成后⁠,如果迭代器尚未结束⁠,并且实现了 return()⁠,语言会通知迭代器提前关闭⁠。

      const iterable = {
        [Symbol.iterator]() {
          let value = 0;
      
          return {
            next() {
              value += 1;
      
              return {
                value,
                done: false,
              };
            },
      
            return() {
              console.log(
                "iterator closed",
              );
      
              return {
                done: true,
              };
            },
          };
        },
      };
      
      const [
        first,
        second,
      ] = iterable;
      
      console.log(first, second);
      // 1 2
      // iterator closed

      这一机制允许迭代器在消费方提前结束时清理资源⁠。

      7.3 对象式解构

      7.3.1 基本形式

      const user = {
        name: "Alice",
        age: 30,
      };
      
      const {
        name,
        age,
      } = user;
      
      console.log(name); // Alice
      console.log(age);  // 30

      对象式解构按属性键查找⁠,不依赖属性声明顺序⁠。

      7.3.2 变量别名

      const user = {
        name: "Alice",
      };
      
      const {
        name: userName,
      } = user;
      
      console.log(userName); // Alice

      name 是被读取的属性键⁠,userName 是新变量名⁠。

      7.3.3 默认值

      const user = {
        name: "Alice",
      };
      
      const {
        name,
        role = "student",
      } = user;
      
      console.log(name); // Alice
      console.log(role); // student

      对象属性存在但值为 undefined 时⁠,也会使用默认值⁠:

      const user = {
        role: undefined,
      };
      
      const {
        role = "student",
      } = user;
      
      console.log(role); // student

      属性值为 null 时不会使用默认值⁠:

      const user = {
        role: null,
      };
      
      const {
        role = "student",
      } = user;
      
      console.log(role); // null

      7.3.4 别名与默认值

      const settings = {};
      
      const {
        language: currentLanguage
          = "zh-CN",
      } = settings;
      
      console.log(
        currentLanguage,
      ); // zh-CN

      冒号右侧是目标变量⁠,等号右侧是默认值⁠。

      7.3.5 计算属性键

      const key = "score";
      
      const student = {
        score: 95,
      };
      
      const {
        [key]: result,
      } = student;
      
      console.log(result); // 95

      计算属性模式必须为读取结果指定目标⁠:

      const {
        [key]: result,
      } = student;

      不能仅写⁠:

      // SyntaxError
      // const {
      //   [key],
      // } = student;

      7.3.6 嵌套对象

      const user = {
        name: "Alice",
        profile: {
          city: "Taipei",
          language: "zh-TW",
        },
      };
      
      const {
        name,
        profile: {
          city,
          language,
        },
      } = user;
      
      console.log(name);
      console.log(city);
      console.log(language);

      这里没有创建名为 profile 的变量⁠,只创建 city 和 language⁠。

      如果还需要整个嵌套对象⁠,应单独绑定⁠:

      const {
        profile,
        profile: {
          city,
        },
      } = user;

      7.3.7 缺失嵌套对象

      下面的代码会抛出 TypeError⁠:

      const user = {};
      
      // TypeError
      // const {
      //   profile: {
      //     city,
      //   },
      // } = user;

      因为它相当于试图从 undefined 中读取属性⁠。

      可以为嵌套对象设置默认值⁠:

      const {
        profile: {
          city = "Unknown",
        } = {},
      } = user;
      
      console.log(city); // Unknown

      复杂外部数据仍应先进行结构验证⁠,而不是通过大量默认值掩盖无效输入⁠。

      7.3.8 对象剩余属性

      const user = {
        id: 1,
        name: "Alice",
        password: "secret",
        role: "student",
      };
      
      const {
        password,
        ...publicData
      } = user;
      
      console.log(publicData);
      // {
      //   id: 1,
      //   name: "Alice",
      //   role: "student",
      // }

      对象剩余属性会收集尚未被模式排除的自有可枚举属性⁠,并创建新普通对象⁠。

      它是浅层复制⁠:

      const source = {
        profile: {
          score: 95,
        },
      };
      
      const {
        ...copy
      } = source;
      
      copy.profile.score = 100;
      
      console.log(
        source.profile.score,
      ); // 100

      自有可枚举 Symbol 属性也可以进入剩余对象⁠。

      7.3.9 右侧不能为 null 或 undefined

      // TypeError
      // const { value } = null;
      
      // TypeError
      // const { value } = undefined;

      其他原始值会被对象化后读取属性⁠:

      const {
        length,
      } = "JavaScript";
      
      console.log(length); // 10

      7.3.10 已声明变量的解构赋值

      声明时可以直接写⁠:

      const {
        name,
      } = user;

      如果变量已经声明⁠,对象解构赋值在语句开头需要圆括号⁠:

      let name;
      let age;
      
      ({
        name,
        age,
      } = user);

      如果省略圆括号⁠,开头的 {} 会被解析为代码块⁠。

      数组解构没有这一歧义⁠:

      let first;
      let second;
      
      [
        first,
        second,
      ] = [
        10,
        20,
      ];

      7.4 参数解构

      参数位置可以直接使用解构模式⁠:

      function createUser({
        name,
        role = "student",
      } = {}) {
        return {
          name,
          role,
        };
      }
      
      console.log(
        createUser({
          name: "Alice",
        }),
      );

      给整个参数设置默认值 {}⁠,可以防止调用时未传参数⁠:

      createUser();

      数组参数⁠:

      function distance([
        x,
        y,
      ]) {
        return Math.hypot(
          x,
          y,
        );
      }
      
      console.log(
        distance([
          3,
          4,
        ]),
      ); // 5

      参数解构适合选项对象⁠,但过度嵌套会让函数签名难以阅读⁠。公共函数通常应保持参数模式简洁⁠,并在函数体内对外部数据进行验证⁠。

      7.5 剩余语法与展开语法

      ... 会根据所在语法位置表示不同操作⁠:

      • 函数参数中的剩余参数⁠;
      • 数组解构中的剩余元素⁠;
      • 对象解构中的剩余属性⁠;
      • 函数调用中的实参展开⁠;
      • 数组字面量中的可迭代展开⁠;
      • 对象字面量中的属性展开⁠。

      它不是一个可以独立求值的普通运算符⁠。

      7.5.1 实参展开

      function add(
        first,
        second,
        third,
      ) {
        return (
          first
          + second
          + third
        );
      }
      
      const values = [
        1,
        2,
        3,
      ];
      
      console.log(
        add(...values),
      ); // 6

      调用展开要求值可迭代⁠:

      // TypeError
      // add(...{
      //   0: 1,
      //   1: 2,
      //   2: 3,
      //   length: 3,
      // });

      类数组对象可以先通过 Array.from() 转换⁠。

      引擎对单次函数调用的实参数量通常存在实现限制⁠,因此不应对超大数组直接执行⁠:

      // 可能超出实参数量限制。
      // Math.max(...veryLargeArray);

      大规模数据应通过循环或 reduce() 处理⁠。

      7.5.2 数组展开

      const first = [
        1,
        2,
      ];
      
      const second = [
        3,
        4,
      ];
      
      const combined = [
        ...first,
        ...second,
      ];
      
      console.log(combined);
      // [1, 2, 3, 4]

      数组展开按迭代协议读取值⁠,因此可以展开 Set⁠、字符串和其他可迭代对象⁠:

      console.log([
        ...new Set([
          1,
          2,
          2,
        ]),
      ]);
      // [1, 2]
      
      console.log([
        ..."JS",
      ]);
      // ["J", "S"]

      展开是浅层复制⁠:

      const source = [
        {
          value: 1,
        },
      ];
      
      const copy = [
        ...source,
      ];
      
      copy[0].value = 2;
      
      console.log(
        source[0].value,
      ); // 2

      7.5.3 对象展开

      const defaults = {
        theme: "light",
        language: "en",
      };
      
      const preferences = {
        theme: "dark",
      };
      
      const settings = {
        ...defaults,
        ...preferences,
      };
      
      console.log(settings);
      // {
      //   theme: "dark",
      //   language: "en",
      // }

      后展开的同名属性覆盖前面的属性⁠。

      对象展开复制源值的自有可枚举属性⁠。它不使用可迭代协议⁠:

      const arrayObject = {
        ...[
          "A",
          "B",
        ],
      };
      
      console.log(arrayObject);
      // {
      //   "0": "A",
      //   "1": "B",
      // }

      字符串的索引属性可枚举⁠:

      console.log({
        ..."JS",
      });
      // {
      //   "0": "J",
      //   "1": "S",
      // }

      null 和 undefined 在对象展开中不会产生属性⁠:

      console.log({
        ...null,
        ...undefined,
      });
      // {}

      对象展开不会复制⁠:

      • 继承属性⁠;
      • 不可枚举属性⁠;
      • 属性描述符⁠;
      • 原型⁠;
      • 私有元素⁠。

      getter 会在展开时读取⁠,结果作为普通数据属性写入新对象⁠。完整差异见第五章⁠。

      7.5.4 条件属性

      const includeDebug = true;
      
      const options = {
        mode: "production",
      
        ...(
          includeDebug
            ? {
              debug: true,
            }
            : {}
        ),
      };

      也可以写成⁠:

      const options = {
        mode: "production",
        ...(includeDebug && {
          debug: true,
        }),
      };

      后者依赖假值原始值在对象展开中不产生相关属性⁠。虽然语法简洁⁠,但三元表达式往往更明确⁠。

      7.6 模板字面量

      模板字面量使用反引号⁠:

      const name = "Alice";
      
      const message = (
        `Hello, ${name}!`
      );

      基础插值和多行文本已经在第一章介绍⁠。本节重点讨论其边界与标签模板⁠。

      7.6.1 插值表达式

      ${...} 中可以放置任意表达式⁠:

      const first = 10;
      const second = 20;
      
      console.log(
        `sum = ${first + second}`,
      );

      表达式结果直接转换为字符串⁠。它与二元 + 的对象转换路径并不完全相同⁠,因此不应把模板插值简单等同于连续字符串加法⁠。

      7.6.2 多行文本与缩进

      const message = `first line
      second line`;

      源码中的换行会进入结果字符串⁠。

      代码缩进也会成为字符串内容⁠:

      function createMessage() {
        return `
          first line
          second line
        `;
      }

      结果包含首尾换行和空格⁠。需要控制输出时⁠,可以⁠:

      • 把文本靠左书写⁠;
      • 使用专门的去缩进工具⁠;
      • 对结果调用明确的格式处理函数⁠。

      7.6.3 嵌套模板

      const active = true;
      
      const className = (
        `button ${
          active
            ? "button-active"
            : "button-disabled"
        }`
      );

      嵌套条件过多时⁠,应先计算中间变量⁠,避免把业务逻辑全部放进模板字面量⁠。

      7.6.4 String.raw

      String.raw 是内置标签函数⁠,可以取得未经转义处理的原始文本部分⁠:

      const path = String.raw`C:\Users\Alice`;
      
      console.log(path);
      // C:\Users\Alice

      插值值仍会正常转换并插入⁠:

      const name = "Alice";
      
      const path = (
        String.raw`C:\Users\${name}`
      );
      
      console.log(path);
      // C:\Users\Alice

      7.7 标签模板

      标签模板把模板字面量交给函数处理⁠:

      function tag(
        strings,
        ...values
      ) {
        console.log(strings);
        console.log(values);
      
        return "result";
      }
      
      const name = "Alice";
      const result = tag`Hello, ${name}!`;
      
      console.log(result); // result

      标签函数可以返回任意值⁠,不要求返回字符串⁠。

      7.7.1 参数结构

      对于⁠:

      tag`A${first}B${second}C`;

      标签函数接收⁠:

      strings = [
        "A",
        "B",
        "C",
      ];
      
      values = [
        first,
        second,
      ];

      静态文本数组长度总是比插值数量多 1⁠。

      7.7.2 cooked 与 raw 文本

      strings 包含经过转义解释的文本⁠,strings.raw 包含源码中的原始文本⁠:

      function inspect(strings) {
        console.log(
          strings[0],
        );
      
        console.log(
          strings.raw[0],
        );
      }
      
      inspect`line 1\nline 2`;

      第一个输出包含实际换行⁠,第二个输出包含反斜杠和字符 n⁠。

      7.7.3 模板对象身份稳定

      同一个标签模板表达式每次求值时⁠,标签函数会收到同一个模板字符串数组对象⁠:

      const history = [];
      
      function record(strings) {
        history.push(strings);
      }
      
      function run() {
        record`value`;
      }
      
      run();
      run();
      
      console.log(
        history[0] === history[1],
      ); // true

      模板数组及其 raw 数组是冻结的⁠。稳定身份允许标签函数使用 WeakMap 等结构缓存编译结果⁠:

      const cache = new WeakMap();
      
      function compile(
        strings,
        ...values
      ) {
        let template = cache.get(strings);
      
        if (template === undefined) {
          template = {
            strings,
          };
      
          cache.set(
            strings,
            template,
          );
        }
      
        return {
          template,
          values,
        };
      }

      7.7.4 构建参数化结构

      标签函数不必直接拼接字符串⁠:

      function query(
        strings,
        ...values
      ) {
        return {
          text: strings.join("?"),
          parameters: values,
        };
      }
      
      const userId = 42;
      const active = true;
      
      const result = query`
        SELECT *
        FROM users
        WHERE id = ${userId}
          AND active = ${active}
      `;
      
      console.log(result.text);
      console.log(result.parameters);

      这只是教学示例⁠。真实数据库查询应使用数据库驱动提供的参数化查询接口⁠,并遵守其占位符语法⁠,不能自行假设这种字符串结构已经安全⁠。

      7.7.5 标签模板与 XSS

      标签模板本身不会自动防止跨站脚本攻击⁠。安全性取决于标签函数是否针对输出上下文进行了正确处理⁠。

      下面的简单转义函数只适合把值放入普通 HTML 文本节点⁠:

      function escapeHtmlText(value) {
        return String(value)
          .replaceAll("&", "&")
          .replaceAll("<", "&lt;")
          .replaceAll(">", "&gt;");
      }
      
      function htmlText(
        strings,
        ...values
      ) {
        let result = strings[0];
      
        for (
          let index = 0;
          index < values.length;
          index += 1
        ) {
          result += (
            escapeHtmlText(
              values[index],
            )
            + strings[index + 1]
          );
        }
      
        return result;
      }

      即使这样⁠,它也不能自动安全处理⁠:

      • HTML 属性值⁠;
      • URL⁠;
      • CSS⁠;
      • JavaScript 源码⁠;
      • 事件处理器属性⁠;
      • SVG⁠;
      • 已经包含可信标记的复杂片段⁠。

      不同上下文需要不同编码规则⁠。更可靠的浏览器代码通常使用⁠:

      element.textContent = userInput;

      或者使用 DOM API 创建元素和设置属性⁠,而不是拼接 HTML 字符串⁠。

      如果必须插入 HTML⁠,应采用经过安全审计⁠、明确支持目标上下文的模板系统和清理库⁠,并配合内容安全策略⁠。不要把一个简短的正则替换函数描述为通用 XSS 防御方案⁠。

      7.8 Symbol 协议概览

      第五章已经介绍 Symbol 作为属性键⁠。JavaScript 还使用若干内置 Symbol 定义协议钩子⁠。

      常见示例包括⁠:

      内置 Symbol作用
      Symbol.iterator定义同步迭代行为
      Symbol.asyncIterator定义异步迭代行为
      Symbol.toPrimitive定义对象到原始值的转换
      Symbol.hasInstance自定义 instanceof
      Symbol.toStringTag自定义对象标签
      Symbol.match参与字符串匹配协议
      Symbol.replace参与字符串替换协议
      Symbol.search参与字符串搜索协议
      Symbol.split参与字符串拆分协议

      这些 Symbol 允许对象参与语言和内置方法定义的操作⁠,而不需要把协议名称暴露为容易冲突的普通字符串键⁠。

      本章重点讨论迭代协议⁠。其他协议应在实际使用到相应功能时学习⁠,不需要为了“⁠现代语法⁠”而一次性重写所有内置行为⁠。

      7.9 可迭代协议与迭代器协议

      7.9.1 可迭代对象

      对象具有可调用的 [Symbol.iterator]() 方法时⁠,就是同步可迭代对象⁠。

      该方法应返回迭代器⁠:

      const iterable = {
        [Symbol.iterator]() {
          return iterator;
        },
      };

      内置可迭代对象包括⁠:

      • Array⁠;
      • String⁠;
      • Map⁠;
      • Set⁠;
      • TypedArray⁠;
      • 许多 DOM 集合⁠;
      • 生成器对象⁠。

      普通对象默认不可迭代⁠。

      7.9.2 迭代器

      迭代器是具有 next() 方法的对象⁠。

      每次调用 next()⁠,应返回迭代结果对象⁠:

      {
        value: currentValue,
        done: false,
      }

      完成后⁠:

      {
        value: finalValue,
        done: true,
      }

      示例⁠:

      function createIterator(
        start,
        end,
      ) {
        let current = start;
      
        return {
          next() {
            if (current <= end) {
              const value = current;
              current += 1;
      
              return {
                value,
                done: false,
              };
            }
      
            return {
              value: undefined,
              done: true,
            };
          },
        };
      }
      
      const iterator = createIterator(
        1,
        3,
      );
      
      console.log(iterator.next());
      // { value: 1, done: false }
      
      console.log(iterator.next());
      // { value: 2, done: false }
      
      console.log(iterator.next());
      // { value: 3, done: false }
      
      console.log(iterator.next());
      // { value: undefined, done: true }

      只有 next() 的对象是迭代器⁠,但不一定可直接用于 for...of⁠。for...of 首先要求可迭代对象⁠,并调用其 [Symbol.iterator]()⁠。

      7.9.3 可迭代迭代器

      许多迭代器同时也是可迭代对象⁠,其 [Symbol.iterator]() 返回自身⁠:

      function createIterator(
        start,
        end,
      ) {
        let current = start;
      
        return {
          next() {
            if (current <= end) {
              return {
                value: current++,
                done: false,
              };
            }
      
            return {
              value: undefined,
              done: true,
            };
          },
      
          [Symbol.iterator]() {
            return this;
          },
        };
      }

      这样就可以⁠:

      const iterator = createIterator(
        1,
        3,
      );
      
      for (const value of iterator) {
        console.log(value);
      }

      7.9.4 自定义范围对象

      更常见的设计是让范围对象每次迭代都创建独立迭代器⁠:

      class Range {
        constructor(
          start,
          end,
        ) {
          this.start = start;
          this.end = end;
        }
      
        [Symbol.iterator]() {
          let current = this.start;
          const end = this.end;
      
          return {
            next() {
              if (current <= end) {
                return {
                  value: current++,
                  done: false,
                };
              }
      
              return {
                value: undefined,
                done: true,
              };
            },
          };
        }
      }
      
      const range = new Range(
        1,
        3,
      );
      
      console.log([
        ...range,
      ]);
      // [1, 2, 3]
      
      console.log([
        ...range,
      ]);
      // [1, 2, 3]

      每次调用 [Symbol.iterator]() 都生成新状态⁠,所以同一个 Range 可以重复遍历⁠。

      7.9.5 一次性迭代器

      迭代器通常保存当前消费位置⁠,因此是一次性的⁠:

      const iterator = [
        1,
        2,
        3,
      ].values();
      
      console.log([
        ...iterator,
      ]);
      // [1, 2, 3]
      
      console.log([
        ...iterator,
      ]);
      // []

      第一次展开已经耗尽迭代器⁠。

      应区分⁠:

      • 可重复产生迭代器的数据源⁠;
      • 已经处于某个消费位置的迭代器对象⁠。

      7.9.6 哪些语法会消费可迭代对象

      常见消费者包括⁠:

      • for...of⁠;
      • 数组式解构⁠;
      • 数组展开⁠;
      • 函数实参展开⁠;
      • Array.from()⁠;
      • Map 构造器⁠;
      • Set 构造器⁠;
      • Promise.all() 等 Promise 组合方法⁠;
      • yield*⁠。

      不同消费者可能提前结束⁠、全部消费或在错误时关闭迭代器⁠。

      7.9.7 迭代器关闭

      迭代器可以实现可选的 return() 方法⁠,用于消费方提前退出时清理资源⁠:

      const iterable = {
        [Symbol.iterator]() {
          let value = 0;
      
          return {
            next() {
              value += 1;
      
              return {
                value,
                done: false,
              };
            },
      
            return() {
              console.log(
                "cleanup",
              );
      
              return {
                done: true,
              };
            },
          };
        },
      };
      
      for (const value of iterable) {
        console.log(value);
      
        if (value === 3) {
          break;
        }
      }

      输出末尾会执行 cleanup⁠。

      常见触发关闭的情况包括⁠:

      • break⁠;
      • return⁠;
      • 循环体抛出异常⁠;
      • 解构只读取部分值⁠;
      • 某些内置算法发生突然完成⁠。

      如果迭代器管理文件⁠、数据库游标或其他资源⁠,return() 可以参与清理⁠。但清理能力仍取决于具体对象和宿主环境⁠。

      7.10 生成器

      生成器函数使用 function* 定义⁠:

      function* generateValues() {
        yield 1;
        yield 2;
        yield 3;
      }

      调用生成器函数不会立即执行函数体⁠,而是返回生成器对象⁠:

      const generator = generateValues();

      生成器对象是可迭代迭代器⁠:

      console.log(
        generator[Symbol.iterator]()
        === generator,
      ); // true

      7.10.1 惰性执行

      function* generateValues() {
        console.log("start");
      
        yield 1;
      
        console.log("middle");
      
        yield 2;
      
        console.log("end");
      }
      
      const generator = generateValues();
      
      console.log("created");
      
      console.log(generator.next());
      console.log(generator.next());
      console.log(generator.next());

      可能输出⁠:

      created
      start
      { value: 1, done: false }
      middle
      { value: 2, done: false }
      end
      { value: undefined, done: true }

      生成器只在调用 next() 时推进到下一个 yield 或结束位置⁠。

      生成器的“⁠暂停⁠”是控制流暂停⁠,不等于异步或非阻塞⁠。next() 调用中的同步计算仍会占用当前线程⁠。

      7.10.2 yield 的返回与恢复

      yield expression 向外产生一个值⁠,并暂停函数⁠。

      下一次 next(value) 传入的参数⁠,会成为上一个暂停的 yield 表达式的求值结果⁠:

      function* communicate() {
        const firstInput = yield "first";
        const secondInput = yield (
          `received: ${firstInput}`
        );
      
        return (
          `finished: ${secondInput}`
        );
      }
      
      const generator = communicate();
      
      console.log(
        generator.next(),
      );
      // {
      //   value: "first",
      //   done: false,
      // }
      
      console.log(
        generator.next("A"),
      );
      // {
      //   value: "received: A",
      //   done: false,
      // }
      
      console.log(
        generator.next("B"),
      );
      // {
      //   value: "finished: B",
      //   done: true,
      // }

      第一次 next(argument) 的参数没有前一个暂停的 yield 可以接收⁠,因此通常会被忽略⁠。

      7.10.3 生成器返回值

      function* generate() {
        yield 1;
        return 2;
      }
      
      const generator = generate();
      
      console.log(generator.next());
      // { value: 1, done: false }
      
      console.log(generator.next());
      // { value: 2, done: true }

      for...of 和数组展开只收集 done: false 的产出值⁠,不包含最终返回值⁠:

      console.log([
        ...generate(),
      ]);
      // [1]

      7.10.4 yield*

      yield* 把迭代过程委托给另一个可迭代对象⁠:

      function* firstPart() {
        yield 1;
        yield 2;
      }
      
      function* complete() {
        yield* firstPart();
        yield 3;
      }
      
      console.log([
        ...complete(),
      ]);
      // [1, 2, 3]

      委托给普通数组⁠:

      function* generate() {
        yield* [
          "A",
          "B",
        ];
      }

      如果被委托生成器具有最终返回值⁠,yield* 表达式可以取得它⁠:

      function* inner() {
        yield 1;
        return 10;
      }
      
      function* outer() {
        const result = yield* inner();
      
        yield result;
      }
      
      console.log([
        ...outer(),
      ]);
      // [1, 10]

      7.10.5 generator.return()

      function* generate() {
        try {
          yield 1;
          yield 2;
        } finally {
          console.log("cleanup");
        }
      }
      
      const generator = generate();
      
      console.log(generator.next());
      
      console.log(
        generator.return("finished"),
      );

      return() 请求生成器结束⁠,并在暂停位置产生类似 return 的控制流⁠。finally 仍会执行⁠。

      典型结果⁠:

      {
        value: "finished",
        done: true,
      }

      如果 finally 中继续 yield⁠,生成器可能不会在第一次 return() 后立即彻底结束⁠,因此不要把它理解为无条件强制销毁⁠。

      7.10.6 generator.throw()

      function* generate() {
        try {
          yield 1;
        } catch (error) {
          yield (
            `caught: ${error.message}`
          );
        }
      }
      
      const generator = generate();
      
      console.log(generator.next());
      
      console.log(
        generator.throw(
          new Error("failed"),
        ),
      );

      throw() 在暂停位置产生抛出完成⁠。生成器内部可以捕获⁠,也可以让错误向调用者传播⁠。

      7.10.7 无限序列

      惰性生成器适合表示无限序列⁠:

      function* fibonacci() {
        let current = 0;
        let next = 1;
      
        while (true) {
          yield current;
      
          [
            current,
            next,
          ] = [
            next,
            current + next,
          ];
        }
      }

      不能直接全部展开⁠:

      // 永远不会完成。
      // [...fibonacci()];

      必须限制消费数量⁠:

      const iterator = fibonacci();
      const values = [];
      
      for (
        let index = 0;
        index < 10;
        index += 1
      ) {
        values.push(
          iterator.next().value,
        );
      }
      
      console.log(values);

      后文的 Iterator Helpers 可以更自然地处理这种惰性序列⁠。

      7.11 Iterator Helpers

      ECMAScript 2025 引入全局 Iterator 对象和迭代器辅助方法⁠。它们为迭代器提供类似数组高阶方法的操作⁠,但许多转换是惰性的⁠,不需要先把全部值收集到数组中⁠。

      7.11.1 Iterator.from

      Iterator.from() 可以把兼容的可迭代对象或迭代器转换为标准 Iterator 接口对象⁠:

      const iterator = Iterator.from([
        1,
        2,
        3,
      ]);
      
      console.log(
        iterator.next(),
      );
      // {
      //   value: 1,
      //   done: false,
      // }

      对于已经继承标准 Iterator 原型的对象⁠,实现可以直接返回原对象⁠;其他兼容对象会得到包装⁠。

      7.11.2 惰性转换方法

      常见惰性方法包括⁠:

      • map()⁠;
      • filter()⁠;
      • flatMap()⁠;
      • take()⁠;
      • drop()⁠。
      const iterator = Iterator
        .from([
          1,
          2,
          3,
          4,
          5,
        ])
        .filter(
          value => value % 2 !== 0,
        )
        .map(
          value => value ** 2,
        )
        .take(2);
      
      console.log(
        iterator.toArray(),
      );
      // [1, 9]

      filter()⁠、map() 和 take() 不会在链条创建时立即遍历全部数据⁠。只有消费结果迭代器时⁠,源迭代器才逐步推进⁠。

      7.11.3 无限序列

      function* naturalNumbers() {
        let value = 1;
      
        while (true) {
          yield value;
          value += 1;
        }
      }
      
      const values = naturalNumbers()
        .filter(
          value => value % 2 === 0,
        )
        .map(
          value => value ** 2,
        )
        .take(5)
        .toArray();
      
      console.log(values);
      // [4, 16, 36, 64, 100]

      如果没有 take() 或其他终止条件⁠,终端操作可能永远无法完成⁠。

      7.11.4 终端方法

      常见终端方法包括⁠:

      • toArray()⁠;
      • forEach()⁠;
      • reduce()⁠;
      • some()⁠;
      • every()⁠;
      • find()⁠。
      const total = Iterator
        .from([
          1,
          2,
          3,
        ])
        .reduce(
          (
            sum,
            value,
          ) => sum + value,
          0,
        );
      
      console.log(total); // 6

      终端方法会消费迭代器⁠。

      7.11.5 迭代器是一次性的

      const iterator = Iterator
        .from([
          1,
          2,
          3,
        ])
        .map(
          value => value * 2,
        );
      
      console.log(
        iterator.toArray(),
      );
      // [2, 4, 6]
      
      console.log(
        iterator.toArray(),
      );
      // []

      第一次调用已经耗尽源迭代器⁠。

      需要重复计算时⁠,应创建能够重新产生迭代器的函数⁠:

      function createValues() {
        return Iterator
          .from([
            1,
            2,
            3,
          ])
          .map(
            value => value * 2,
          );
      }
      
      console.log(
        createValues().toArray(),
      );
      
      console.log(
        createValues().toArray(),
      );

      7.11.6 Iterator 方法与数组方法的选择

      使用 Iterator Helpers⁠:

      • 数据源本身是迭代器⁠;
      • 数据量较大⁠;
      • 希望惰性转换⁠;
      • 只需要前若干结果⁠;
      • 处理无限序列⁠;
      • 希望避免不必要的中间数组⁠。

      使用数组方法⁠:

      • 需要随机访问⁠;
      • 需要重复遍历⁠;
      • 数据已经完整存储在数组中⁠;
      • 需要数组特有方法⁠;
      • 结果必须保留为稳定集合⁠。

      Iterator Helpers 在 2025 年进入标准⁠,但较旧环境可能不支持⁠。使用前应检查目标运行环境或提供兼容方案⁠。

      7.12 异步迭代协议

      同步迭代器的 next() 返回迭代结果对象⁠。异步迭代器的 next() 返回一个 Promise⁠,该 Promise 兑现为迭代结果对象⁠。

      {
        value: currentValue,
        done: false,
      }

      或⁠:

      {
        value: finalValue,
        done: true,
      }

      异步可迭代对象通过 [Symbol.asyncIterator]() 返回异步迭代器⁠。

      7.12.1 自定义异步可迭代对象

      function createAsyncRange(
        start,
        end,
        delay,
      ) {
        return {
          [Symbol.asyncIterator]() {
            let current = start;
      
            return {
              async next() {
                if (current > end) {
                  return {
                    value: undefined,
                    done: true,
                  };
                }
      
                await new Promise(
                  (resolve) => {
                    setTimeout(
                      resolve,
                      delay,
                    );
                  },
                );
      
                return {
                  value: current++,
                  done: false,
                };
              },
            };
          },
        };
      }

      消费⁠:

      for await (
        const value
        of createAsyncRange(
          1,
          3,
          100,
        )
      ) {
        console.log(value);
      }

      7.12.2 for await…of

      for await...of 可以消费异步可迭代对象⁠,也可以消费同步可迭代对象⁠:

      async function printValues() {
        for await (
          const value
          of [
            Promise.resolve(1),
            Promise.resolve(2),
            3,
          ]
        ) {
          console.log(value);
        }
      }

      同步可迭代对象会被适配为异步消费过程⁠,其值会经过 Promise 决议⁠。

      即使数据源是同步数组⁠,for await...of 也会引入异步等待步骤⁠,因此普通同步值应优先使用 for...of⁠。

      7.12.3 顺序消费

      for await (
        const value
        of asyncSource
      ) {
        await processValue(value);
      }

      这会按顺序⁠:

      1. 等待下一个值⁠;
      2. 等待当前值处理完成⁠;
      3. 再请求下一个值⁠。

      它适合流式顺序处理⁠。

      如果已经拥有一组彼此独立的 Promise⁠,并希望并发等待⁠,应使用 Promise.all()⁠,而不是通过 for await...of 无意串行化⁠。

      7.12.4 异步迭代器关闭

      异步迭代器可以实现 return()⁠,其返回 Promise⁠。for await...of 提前退出时⁠,会等待相应关闭过程⁠:

      const source = {
        [Symbol.asyncIterator]() {
          let value = 0;
      
          return {
            async next() {
              value += 1;
      
              return {
                value,
                done: false,
              };
            },
      
            async return() {
              console.log(
                "async cleanup",
              );
      
              return {
                done: true,
              };
            },
          };
        },
      };
      
      for await (const value of source) {
        console.log(value);
      
        if (value === 3) {
          break;
        }
      }

      7.13 异步生成器

      异步生成器使用 async function*⁠:

      async function* generateValues() {
        yield 1;
        yield 2;
        yield 3;
      }

      调用后得到异步生成器对象⁠:

      const generator = generateValues();

      其 next() 返回 Promise⁠:

      console.log(
        await generator.next(),
      );
      // {
      //   value: 1,
      //   done: false,
      // }

      7.13.1 组合 await 与 yield

      async function* loadPages(
        pageCount,
      ) {
        for (
          let page = 1;
          page <= pageCount;
          page += 1
        ) {
          const response = await fetch(
            `/api/items?page=${page}`,
          );
      
          if (!response.ok) {
            throw new Error(
              `HTTP ${response.status}`,
            );
          }
      
          const data = await response.json();
      
          yield data;
        }
      }

      消费⁠:

      for await (
        const page
        of loadPages(3)
      ) {
        console.log(page);
      }

      每页请求在上一轮完成后才开始⁠,属于串行分页读取⁠。

      7.13.2 逐项产生数据

      async function* loadItems(
        pageCount,
      ) {
        for await (
          const page
          of loadPages(pageCount)
        ) {
          for (const item of page.items) {
            yield item;
          }
        }
      }

      调用者不需要一次把全部数据保存在内存中⁠。

      7.13.3 错误与清理

      async function* source() {
        try {
          yield 1;
          throw new Error("failed");
        } finally {
          console.log("cleanup");
        }
      }
      
      try {
        for await (
          const value
          of source()
        ) {
          console.log(value);
        }
      } catch (error) {
        console.error(error);
      }

      异步生成器抛出的错误会使相应 next() Promise 拒绝⁠,并传播到 for await...of⁠。

      异步生成器适合⁠:

      • 分页数据⁠;
      • 网络流⁠;
      • 消息流⁠;
      • 需要异步等待的惰性序列⁠;
      • 逐批处理的大型数据⁠。

      它不是并行执行工具⁠。每次推进仍遵循异步迭代器的顺序协议⁠。

      7.14 Proxy

      Proxy 创建一个代理对象⁠,用于拦截目标对象的若干基本操作⁠。

      const target = {
        message: "Hello",
      };
      
      const proxy = new Proxy(
        target,
        {
          get(
            targetObject,
            property,
            receiver,
          ) {
            console.log(
              `read: ${String(property)}`,
            );
      
            return Reflect.get(
              targetObject,
              property,
              receiver,
            );
          },
        },
      );
      
      console.log(proxy.message);

      如果处理器没有定义对应陷阱⁠,操作通常转发到目标对象⁠。

      7.14.1 Proxy 拦截的是对象操作

      常见陷阱包括⁠:

      陷阱对应操作示例
      getproxy.key
      setproxy.key = value
      has"key" in proxy
      deletePropertydelete proxy.key
      ownKeysReflect.ownKeys(proxy)
      getOwnPropertyDescriptorObject.getOwnPropertyDescriptor(proxy, key)
      definePropertyObject.defineProperty(proxy, key, descriptor)
      getPrototypeOfObject.getPrototypeOf(proxy)
      setPrototypeOfObject.setPrototypeOf(proxy, prototype)
      isExtensibleObject.isExtensible(proxy)
      preventExtensionsObject.preventExtensions(proxy)
      applyproxy(...args)
      constructnew proxy(...args)

      Proxy 不能任意拦截所有 JavaScript 行为⁠。例如⁠:

      • 局部变量读取不是对象属性访问⁠;
      • === 不能被 Proxy 重写⁠;
      • 私有字段访问不走普通属性陷阱⁠;
      • 某些内置对象行为依赖内部槽⁠,而不是普通属性⁠。

      7.14.2 默认值代理

      function withDefaults(
        target,
        defaults,
      ) {
        return new Proxy(
          target,
          {
            get(
              targetObject,
              property,
              receiver,
            ) {
              if (
                Reflect.has(
                  targetObject,
                  property,
                )
              ) {
                return Reflect.get(
                  targetObject,
                  property,
                  receiver,
                );
              }
      
              return defaults[property];
            },
          },
        );
      }
      
      const settings = withDefaults(
        {
          theme: "dark",
        },
        {
          theme: "light",
          language: "en",
        },
      );
      
      console.log(settings.theme);    // dark
      console.log(settings.language); // en

      这里使用 Reflect.has() 会把继承属性也视为存在⁠。如果只希望检查自有属性⁠,应使用 Object.hasOwn()⁠。

      7.14.3 写入验证

      function createValidatedUser(
        initialValue,
      ) {
        return new Proxy(
          initialValue,
          {
            set(
              target,
              property,
              value,
              receiver,
            ) {
              if (
                property === "age"
                && (
                  !Number.isInteger(value)
                  || value < 0
                  || value > 150
                )
              ) {
                throw new RangeError(
                  "age 必须是 0 到 150 之间的整数",
                );
              }
      
              return Reflect.set(
                target,
                property,
                value,
                receiver,
              );
            },
          },
        );
      }
      
      const user = createValidatedUser({
        name: "Alice",
        age: 30,
      });
      
      user.age = 31;

      set 陷阱应返回布尔值⁠,表示写入是否成功⁠。严格模式下返回假值会导致赋值抛出 TypeError⁠。

      7.14.4 receiver 参数

      下面的原型对象包含 getter⁠:

      const target = {
        _value: 10,
      
        get value() {
          return this._value;
        },
      };

      代理⁠:

      const proxy = new Proxy(
        target,
        {
          get(
            targetObject,
            property,
            receiver,
          ) {
            return Reflect.get(
              targetObject,
              property,
              receiver,
            );
          },
        },
      );

      receiver 表示最初进行属性访问的对象⁠。对于继承⁠、getter 和代理链⁠,正确传递 receiver 可以保持普通属性访问语义⁠。

      如果直接写⁠:

      return targetObject[property];

      getter 中的 this 会固定为 targetObject⁠,可能与代理或派生对象的实际调用语义不同⁠。

      不过⁠,这不意味着 Proxy 陷阱中任何情况都必须机械地使用 Reflect⁠。自定义代理可能有意改变接收者⁠、返回值或目标行为⁠。Reflect 的价值是提供与默认对象操作相对应的转发接口⁠。

      7.14.5 Proxy 不变量

      代理不能违反目标对象的某些基本约束⁠。

      例如⁠,目标具有不可配置且不可写的数据属性时⁠,get 陷阱不能报告不同值⁠:

      const target = {};
      
      Object.defineProperty(
        target,
        "fixed",
        {
          value: 10,
          writable: false,
          configurable: false,
        },
      );
      
      const proxy = new Proxy(
        target,
        {
          get() {
            return 20;
          },
        },
      );
      
      // TypeError
      // console.log(proxy.fixed);

      其他典型不变量包括⁠:

      • ownKeys 不能遗漏不可配置的自有属性⁠;
      • 目标不可扩展时⁠,ownKeys 不能报告额外自有键⁠;
      • getPrototypeOf 不能在不可扩展目标上报告不同原型⁠;
      • isExtensible 必须与目标实际状态一致⁠;
      • defineProperty 不能声称完成目标不允许的属性定义⁠。

      这些限制保证代理仍然符合对象模型的核心一致性⁠。

      7.14.6 ownKeys 与可见属性

      const target = {
        publicValue: 1,
        _internalValue: 2,
      };
      
      const proxy = new Proxy(
        target,
        {
          ownKeys(targetObject) {
            return Reflect
              .ownKeys(targetObject)
              .filter(
                key => (
                  typeof key !== "string"
                  || !key.startsWith("_")
                ),
              );
          },
        },
      );
      
      console.log(
        Object.keys(proxy),
      );
      // ["publicValue"]

      如果 _internalValue 是不可配置属性⁠,陷阱不能把它省略⁠。

      代理过滤枚举结果不等于真正的访问控制⁠:

      console.log(
        proxy._internalValue,
      ); // 2

      需要阻止读取⁠,还必须处理 get⁠;即使如此⁠,持有原始目标引用的代码仍可绕过代理⁠。安全边界不能只依赖 Proxy 包装⁠。

      7.14.7 函数代理

      function multiply(
        first,
        second,
      ) {
        return (
          first
          * second
        );
      }
      
      const loggedMultiply = new Proxy(
        multiply,
        {
          apply(
            target,
            thisArgument,
            argumentsList,
          ) {
            console.log(
              "arguments:",
              argumentsList,
            );
      
            return Reflect.apply(
              target,
              thisArgument,
              argumentsList,
            );
          },
        },
      );
      
      console.log(
        loggedMultiply(3, 4),
      ); // 12

      只有目标本身可调用时⁠,代理才可调用⁠。

      构造代理⁠:

      class User {
        constructor(name) {
          this.name = name;
        }
      }
      
      const UserProxy = new Proxy(
        User,
        {
          construct(
            target,
            argumentsList,
            newTarget,
          ) {
            console.log(
              "constructing User",
            );
      
            return Reflect.construct(
              target,
              argumentsList,
              newTarget,
            );
          },
        },
      );
      
      const user = new UserProxy(
        "Alice",
      );

      只有目标具有 [[Construct]] 时⁠,代理才可构造⁠;construct 陷阱必须返回对象⁠。

      7.14.8 Proxy.revocable

      const {
        proxy,
        revoke,
      } = Proxy.revocable(
        {
          value: 10,
        },
        {},
      );
      
      console.log(proxy.value); // 10
      
      revoke();
      
      // TypeError
      // console.log(proxy.value);

      撤销后⁠,代理的基本操作会抛出 TypeError⁠。这适合临时授权和生命周期明确的接口⁠。

      撤销只影响代理⁠,不会销毁目标对象⁠。仍持有目标引用的代码可以继续使用目标⁠。

      7.15 Proxy 的边界

      7.15.1 代理与目标身份不同

      const target = {};
      const proxy = new Proxy(
        target,
        {},
      );
      
      console.log(
        proxy === target,
      ); // false

      Map 和 Set 会把它们视为不同键和值⁠。

      7.15.2 内置内部槽

      某些内置方法要求 this 具有特定内部槽⁠。简单代理不会自动获得目标的内部槽⁠:

      const map = new Map([
        [
          "key",
          "value",
        ],
      ]);
      
      const proxy = new Proxy(
        map,
        {},
      );
      
      // TypeError
      // proxy.get("key");

      Map.prototype.get 接收到的 this 是代理⁠,而代理本身没有 Map 的内部槽⁠。

      可以在 get 陷阱中把方法绑定到目标⁠:

      const proxy = new Proxy(
        map,
        {
          get(
            target,
            property,
            receiver,
          ) {
            const value = Reflect.get(
              target,
              property,
              receiver,
            );
      
            if (
              typeof value === "function"
            ) {
              return value.bind(target);
            }
      
            return value;
          },
        },
      );
      
      console.log(
        proxy.get("key"),
      ); // value

      但这种通用绑定会改变方法身份⁠,并可能错误绑定本来应该以代理为接收者的普通方法⁠。真实代码应根据目标类型和接口逐项设计⁠,而不是对所有函数属性无条件绑定⁠。

      Date⁠、Set⁠、TypedArray 和其他具有内部槽的对象也可能存在类似问题⁠。

      7.15.3 私有字段

      class Counter {
        #value = 0;
      
        increment() {
          this.#value += 1;
        }
      }
      
      const counter = new Counter();
      const proxy = new Proxy(
        counter,
        {},
      );
      
      // TypeError
      // proxy.increment();

      调用 proxy.increment() 时⁠,方法中的 this 是代理⁠。代理没有 Counter 的私有品牌⁠,因此访问失败⁠。

      可以把方法绑定到目标⁠,但同样会改变普通代理语义⁠。私有字段与透明代理不能天然结合⁠。

      7.15.4 性能与可优化性

      Proxy 可以使属性访问路径更加动态⁠,并限制部分引擎优化⁠。具体影响依赖⁠:

      • 陷阱种类⁠;
      • 访问频率⁠;
      • 目标结构⁠;
      • 引擎版本⁠;
      • 调用位置⁠;
      • 数据规模⁠。

      不能简单断言 Proxy 一定“⁠很慢⁠”⁠,也不能忽略其成本⁠。在热点路径使用代理前⁠,应通过实际性能分析验证⁠。

      7.15.5 适用场景

      Proxy 适合⁠:

      • API 适配层⁠;
      • 属性访问日志⁠;
      • 验证和规范化⁠;
      • 延迟对象⁠;
      • 响应式系统⁠;
      • 虚拟对象模型⁠;
      • 权限受限的接口外观⁠;
      • 测试替身⁠;
      • 元编程框架⁠。

      不适合⁠:

      • 仅为了简化普通属性读取⁠;
      • 需要无损透明代理所有内置对象⁠;
      • 作为真正安全沙箱⁠;
      • 取代明确的数据验证⁠;
      • 在未测量的高频路径中大范围使用⁠。

      7.16 Reflect

      Reflect 是不可构造的静态内置对象⁠,提供与若干对象内部操作对应的方法⁠。

      console.log(
        typeof Reflect,
      ); // object
      
      // TypeError
      // new Reflect();

      常见方法包括⁠:

      • Reflect.get()⁠;
      • Reflect.set()⁠;
      • Reflect.has()⁠;
      • Reflect.deleteProperty()⁠;
      • Reflect.ownKeys()⁠;
      • Reflect.getOwnPropertyDescriptor()⁠;
      • Reflect.defineProperty()⁠;
      • Reflect.getPrototypeOf()⁠;
      • Reflect.setPrototypeOf()⁠;
      • Reflect.isExtensible()⁠;
      • Reflect.preventExtensions()⁠;
      • Reflect.apply()⁠;
      • Reflect.construct()⁠。

      这些名称与 Proxy 陷阱高度对应⁠,因此适合实现默认转发⁠。

      7.16.1 Reflect.get 与 Reflect.set

      const object = {
        value: 10,
      };
      
      console.log(
        Reflect.get(
          object,
          "value",
        ),
      ); // 10
      
      console.log(
        Reflect.set(
          object,
          "value",
          20,
        ),
      ); // true

      Reflect.set() 返回布尔值⁠,而赋值表达式返回被赋的值⁠:

      const result = (
        object.value = 30
      );
      
      console.log(result); // 30

      7.16.2 Reflect.has

      console.log(
        Reflect.has(
          object,
          "value",
        ),
      ); // true

      它与 in 运算符语义对应⁠,包括原型链属性⁠。

      7.16.3 Reflect.deleteProperty

      console.log(
        Reflect.deleteProperty(
          object,
          "value",
        ),
      ); // true

      它提供函数式接口⁠,并返回删除是否成功⁠。

      7.16.4 Reflect.defineProperty

      const object = {};
      
      const succeeded = Reflect.defineProperty(
        object,
        "value",
        {
          value: 10,
        },
      );
      
      console.log(succeeded); // true

      Object.defineProperty() 失败时抛出异常⁠;Reflect.defineProperty() 在普通定义失败时返回 false⁠,但参数类型错误等情况仍可能抛出⁠。

      7.16.5 Reflect.apply

      function add(
        first,
        second,
      ) {
        return (
          this.offset
          + first
          + second
        );
      }
      
      const result = Reflect.apply(
        add,
        {
          offset: 10,
        },
        [
          1,
          2,
        ],
      );
      
      console.log(result); // 13

      与⁠:

      add.apply(
        {
          offset: 10,
        },
        [
          1,
          2,
        ],
      );

      相比⁠,Reflect.apply() 不依赖目标函数对象上可被覆盖的 apply 属性⁠。

      7.16.6 Reflect.construct

      class User {
        constructor(name) {
          this.name = name;
        }
      }
      
      const user = Reflect.construct(
        User,
        [
          "Alice",
        ],
      );

      第三个参数可以指定 newTarget⁠:

      class Administrator
        extends User {}
      
      const user = Reflect.construct(
        User,
        [
          "Alice",
        ],
        Administrator,
      );
      
      console.log(
        user instanceof Administrator,
      ); // true

      这属于高级构造控制⁠,应仅在代理⁠、框架和元编程中使用⁠。

      7.17 globalThis

      不同宿主环境曾经使用不同名称表示全局对象⁠:

      • 浏览器 Window⁠:window⁠;
      • Worker⁠:self⁠;
      • Node.js⁠:global⁠。

      globalThis 提供统一的标准访问方式⁠:

      console.log(globalThis);

      7.17.1 全局对象与全局绑定不是同一概念

      在浏览器传统脚本中⁠:

      var legacyValue = 1;
      let lexicalValue = 2;
      const constantValue = 3;

      var 创建的某些顶层绑定可能同时成为全局对象属性⁠:

      console.log(
        globalThis.legacyValue,
      ); // 1

      let 和 const 创建全局词法绑定⁠,但不会成为全局对象属性⁠:

      console.log(
        globalThis.lexicalValue,
      ); // undefined
      
      console.log(
        globalThis.constantValue,
      ); // undefined

      模块顶层绑定同样不会自动挂载到 globalThis⁠。

      因此⁠,不能通过遍历 globalThis 得到所有顶层变量⁠。

      7.17.2 显式全局状态

      globalThis.appConfig = {
        mode: "development",
      };

      这种写法会创建共享全局状态⁠,应谨慎使用⁠。更好的方式通常是⁠:

      • 模块导出⁠;
      • 显式依赖注入⁠;
      • 应用上下文对象⁠;
      • 工厂函数参数⁠。

      globalThis 适合⁠:

      • 跨宿主代码访问标准全局对象⁠;
      • polyfill 检测和安装⁠;
      • 明确需要全局注册的基础设施⁠。

      它不应成为普通模块之间传递状态的默认渠道⁠。

      7.18 new Function

      Function 构造器可以从字符串创建函数⁠:

      const add = new Function(
        "first",
        "second",
        "return first + second;",
      );
      
      console.log(
        add(1, 2),
      ); // 3

      最后一个参数是函数体⁠,前面的参数定义形参⁠。

      7.18.1 使用全局作用域

      通过 new Function() 创建的函数不闭包捕获创建位置的局部词法环境⁠:

      function createFunction() {
        const localValue = 10;
      
        return new Function(
          "return typeof localValue;",
        );
      }
      
      console.log(
        createFunction()(),
      ); // undefined

      函数体中的自由标识符按照全局环境解析⁠。

      它仍然可以访问全局值⁠:

      globalThis.sharedValue = 10;
      
      const readShared = new Function(
        "return sharedValue;",
      );
      
      console.log(
        readShared(),
      ); // 10
      
      delete globalThis.sharedValue;

      7.18.2 不继承严格模式

      外层处于严格模式⁠,不会自动使 Function 构造器生成的函数体变成严格代码⁠:

      "use strict";
      
      const inspectThis = new Function(
        "return this;",
      );
      
      console.log(
        inspectThis() === globalThis,
      ); // 通常为 true

      需要严格模式时⁠,应在函数体字符串中显式声明⁠:

      const inspectThis = new Function(
        '"use strict"; return this;',
      );
      
      console.log(
        inspectThis(),
      ); // undefined

      7.18.3 风险

      new Function() 与 eval() 一样需要运行时解析代码字符串⁠,可能带来⁠:

      • 代码注入⁠;
      • 内容安全策略阻止⁠;
      • 静态分析困难⁠;
      • 重构困难⁠;
      • 调试和错误定位复杂⁠;
      • 工具链优化受限⁠;
      • 运行时编译成本⁠。

      它不应接收未受信任输入⁠。

      合理使用场景较少⁠,例如⁠:

      • 受控表达式编译器的底层实现⁠;
      • 模板引擎内部⁠;
      • 开发工具⁠;
      • 明确受信任的代码生成⁠;
      • 性能经过验证的专用系统⁠。

      即使如此⁠,通常也应优先使用解析器⁠、查找表和普通函数组合⁠。

      7.19 eval

      eval() 解析并执行字符串中的 JavaScript 代码⁠:

      const result = eval(
        "1 + 2",
      );
      
      console.log(result); // 3

      它具有直接调用和间接调用两种不同语义⁠。

      7.19.1 直接 eval

      语法上直接调用当前 Realm 的内置 eval⁠:

      eval(sourceText);

      直接 eval 可以访问调用位置的局部作用域⁠:

      function calculate() {
        const first = 10;
        const second = 20;
      
        return eval(
          "first + second",
        );
      }
      
      console.log(
        calculate(),
      ); // 30

      这使引擎和静态工具必须考虑动态代码可能读取或修改局部名称⁠,从而显著增加分析难度⁠。

      7.19.2 间接 eval

      以下调用通常属于间接 eval⁠:

      const indirectEval = eval;
      indirectEval(sourceText);
      
      (0, eval)(sourceText);
      
      eval?.(sourceText);
      
      const object = {
        eval,
      };
      
      object.eval(sourceText);

      间接 eval 在全局作用域中执行⁠,不能访问调用位置的局部变量⁠:

      function calculate() {
        const first = 10;
        const second = 20;
      
        return eval?.(
          "typeof first",
        );
      }
      
      console.log(
        calculate(),
      ); // undefined

      7.19.3 严格模式

      直接 eval 在严格代码中以严格模式执行⁠:

      function run() {
        "use strict";
      
        return eval(
          "this",
        );
      }
      
      console.log(run()); // undefined

      间接 eval 不继承调用位置的严格模式⁠。只有源字符串自身包含严格模式指令时⁠,才以严格模式解析⁠:

      eval?.(
        '"use strict";'
        + "this",
      );

      在全局严格 eval 中⁠,this 的具体结果仍需依据全局代码语义理解⁠;“⁠严格模式⁠”不等于把任何全局代码的 this 都变成 undefined⁠。模块顶层 this 为 undefined 是模块语义⁠,不是所有严格代码的统一表现⁠。

      7.19.4 声明泄漏

      非严格直接 eval 中的 var 和部分函数声明可能影响周围变量环境⁠:

      function run() {
        eval(
          "var leaked = 10;",
        );
      
        console.log(leaked);
      }
      
      run(); // 10

      严格 eval 中⁠,eval 内部声明不会注入周围作用域⁠:

      function run() {
        "use strict";
      
        eval(
          "var local = 10;",
        );
      
        console.log(
          typeof local,
        ); // undefined
      }
      
      run();

      let⁠、const 和类声明具有 eval 自身的词法作用域⁠,不会像非严格 var 一样泄漏⁠。

      7.19.5 eval 不是沙箱

      无论直接还是间接 eval⁠,都不能被视为安全沙箱⁠。

      间接 eval 不能访问调用位置的局部变量⁠,但仍可能访问和修改全局对象⁠、调用网络接口⁠、读取可用 API⁠,并执行任意权限范围内的代码⁠。

      eval?.(
        "globalThis.compromised = true",
      );

      在同一 Realm 中执行不受信任 JavaScript 代码通常是不安全的⁠。真正隔离需要结合⁠:

      • 独立 Realm 或进程⁠;
      • Worker 与严格消息边界⁠;
      • iframe 沙箱⁠;
      • 权限模型⁠;
      • 内容安全策略⁠;
      • 专用解释器⁠;
      • 服务端隔离⁠;
      • 可信代码审计⁠。

      即使使用这些机制⁠,也需要完整威胁模型⁠。

      7.19.6 替代方案

      动态读取对象属性⁠:

      const operations = {
        add: (
          first,
          second,
        ) => first + second,
      
        subtract: (
          first,
          second,
        ) => first - second,
      };
      
      const operationName = "add";
      
      const result = operations[
        operationName
      ](1, 2);

      解析数据⁠:

      const data = JSON.parse(
        sourceText,
      );

      处理用户表达式⁠:

      • 使用明确语法的解析器⁠;
      • 构建抽象语法树⁠;
      • 只实现允许的运算⁠;
      • 不执行任意 JavaScript⁠。

      大多数 eval() 用途都可以被更安全⁠、结构更明确的方案取代⁠。

      7.20 Intl 国际化接口

      Intl 属于 ECMAScript Internationalization API⁠,由 ECMA-402 规范定义⁠。它提供区域敏感的⁠:

      • 字符串比较⁠;
      • 数字格式化⁠;
      • 日期时间格式化⁠;
      • 相对时间⁠;
      • 复数规则⁠;
      • 列表格式⁠; -显示名称⁠;
      • 文本分段⁠;
      • 持续时间格式等功能⁠。

      区域设置标识符通常使用 BCP 47 语言标签⁠,例如⁠:

      • "en-US"⁠;
      • "en-GB"⁠;
      • "zh-CN"⁠;
      • "zh-TW"⁠;
      • "de-DE"⁠。

      实际支持的区域数据取决于运行环境提供的国际化数据⁠。相同请求通常遵循规范⁠,但具体标点⁠、空格和本地化词形可能随实现数据版本变化⁠,因此不应把展示字符串作为稳定机器协议⁠。

      7.21 Intl.NumberFormat

      7.21.1 普通数字

      const formatter
        = new Intl.NumberFormat(
          "zh-CN",
        );
      
      console.log(
        formatter.format(
          1234567.89,
        ),
      );

      7.21.2 货币

      const formatter
        = new Intl.NumberFormat(
          "zh-CN",
          {
            style: "currency",
            currency: "CNY",
          },
        );
      
      console.log(
        formatter.format(
          1234.5,
        ),
      );

      货币代码应使用明确的 ISO 4217 代码⁠,例如⁠:

      • CNY⁠;
      • USD⁠;
      • EUR⁠;
      • JPY⁠。

      不要仅根据区域设置猜测货币⁠。语言区域与货币是不同概念⁠。

      7.21.3 百分比

      const formatter
        = new Intl.NumberFormat(
          "zh-CN",
          {
            style: "percent",
            maximumFractionDigits: 1,
          },
        );
      
      console.log(
        formatter.format(
          0.1234,
        ),
      );

      百分比样式会把数值 0.1234 表示为约 12.3%⁠,而不是把 12.34 解释为 12.34%⁠。

      7.21.4 单位

      const formatter
        = new Intl.NumberFormat(
          "en-GB",
          {
            style: "unit",
            unit: "kilometer-per-hour",
            unitDisplay: "long",
          },
        );
      
      console.log(
        formatter.format(
          90,
        ),
      );

      支持的单位集合由规范定义⁠,使用前应查阅目标环境和规范⁠。

      7.21.5 formatToParts

      需要自定义布局时⁠,不应拆分最终字符串⁠,而应使用结构化部件⁠:

      const formatter
        = new Intl.NumberFormat(
          "en-US",
          {
            style: "currency",
            currency: "USD",
          },
        );
      
      console.log(
        formatter.formatToParts(
          1234.5,
        ),
      );

      结果包含⁠:

      • currency⁠;
      • integer⁠;
      • group⁠;
      • decimal⁠;
      • fraction⁠;
      • 其他本地化部件⁠。

      7.22 Intl.DateTimeFormat

      7.22.1 日期格式

      const date = new Date(
        "2026-07-19T08:00:00Z",
      );
      
      const formatter
        = new Intl.DateTimeFormat(
          "zh-CN",
          {
            dateStyle: "long",
          },
        );
      
      console.log(
        formatter.format(date),
      );

      7.22.2 明确时区

      const formatter
        = new Intl.DateTimeFormat(
          "zh-TW",
          {
            dateStyle: "long",
            timeStyle: "medium",
            timeZone: "Asia/Taipei",
          },
        );
      
      console.log(
        formatter.format(date),
      );

      如果省略 timeZone⁠,通常使用运行环境的默认时区⁠。服务端渲染⁠、自动测试和跨地区协作中⁠,应显式指定时区⁠,避免结果依赖部署机器配置⁠。

      7.22.3 formatRange

      const start = new Date(
        "2026-07-19T08:00:00Z",
      );
      
      const end = new Date(
        "2026-07-19T10:00:00Z",
      );
      
      const formatter
        = new Intl.DateTimeFormat(
          "zh-TW",
          {
            dateStyle: "medium",
            timeStyle: "short",
            timeZone: "Asia/Taipei",
          },
        );
      
      console.log(
        formatter.formatRange(
          start,
          end,
        ),
      );

      格式化器能够根据区域规则省略重复部分⁠。

      7.23 Intl.Collator

      普通字符串比较基于代码单元关系⁠,不等同于自然语言排序⁠。

      const values = [
        "z",
        "ä",
        "a",
      ];
      
      const collator
        = new Intl.Collator(
          "de",
        );
      
      values.sort(
        collator.compare,
      );
      
      console.log(values);

      7.23.1 sensitivity

      const collator
        = new Intl.Collator(
          "en",
          {
            sensitivity: "base",
          },
        );
      
      console.log(
        collator.compare(
          "a",
          "A",
        ),
      ); // 通常视为相等

      常见敏感度⁠:

      • "base"⁠;
      • "accent"⁠;
      • "case"⁠;
      • "variant"⁠。

      具体排序仍由区域规则决定⁠。

      7.23.2 numeric

      const values = [
        "file2",
        "file10",
        "file1",
      ];
      
      const collator
        = new Intl.Collator(
          "en",
          {
            numeric: true,
          },
        );
      
      values.sort(
        collator.compare,
      );
      
      console.log(values);
      // ["file1", "file2", "file10"]

      7.23.3 search 与 sort

      const searchCollator
        = new Intl.Collator(
          "en",
          {
            usage: "search",
          },
        );

      搜索比较和排序比较可能采用不同规则⁠。不要假定 usage: "search" 的比较器适合生成稳定排序顺序⁠。

      7.24 其他 Intl 接口

      7.24.1 RelativeTimeFormat

      const formatter
        = new Intl.RelativeTimeFormat(
          "zh-CN",
          {
            numeric: "auto",
          },
        );
      
      console.log(
        formatter.format(
          -1,
          "day",
        ),
      );

      可能得到“⁠昨天⁠”一类本地化表达⁠。

      7.24.2 PluralRules

      const pluralRules
        = new Intl.PluralRules(
          "en",
        );
      
      console.log(
        pluralRules.select(1),
      ); // one
      
      console.log(
        pluralRules.select(2),
      ); // other

      它返回语言规则中的复数类别⁠,不直接生成完整句子⁠。

      const messages = {
        one: "1 item",
        other: "{count} items",
      };

      复杂产品通常使用成熟国际化消息系统处理占位⁠、复数和语法变化⁠。

      7.24.3 ListFormat

      const formatter
        = new Intl.ListFormat(
          "en",
          {
            style: "long",
            type: "conjunction",
          },
        );
      
      console.log(
        formatter.format([
          "JavaScript",
          "HTML",
          "CSS",
        ]),
      );

      7.24.4 DisplayNames

      const formatter
        = new Intl.DisplayNames(
          [
            "zh-CN",
          ],
          {
            type: "region",
          },
        );
      
      console.log(
        formatter.of("JP"),
      );

      7.24.5 Segmenter

      Intl.Segmenter 可以按照字素⁠、单词或句子边界分段⁠:

      const segmenter
        = new Intl.Segmenter(
          "zh-CN",
          {
            granularity: "word",
          },
        );
      
      const segments = [
        ...segmenter.segment(
          "学习JavaScript语言",
        ),
      ];
      
      console.log(segments);

      它比简单按空格拆词更适合不以空格分隔单词的语言⁠。

      按用户感知字符分割⁠:

      const graphemeSegmenter
        = new Intl.Segmenter(
          undefined,
          {
            granularity: "grapheme",
          },
        );
      
      console.log([
        ...graphemeSegmenter
          .segment("👨‍👩‍👧‍👦")
      ].length);

      复杂 emoji 可能由多个 Unicode 码点组成⁠,但在字素层面作为一个用户感知字符处理⁠。

      7.25 Intl 工程实践

      7.25.1 重用格式化器

      需要多次使用相同区域和选项时⁠,可以创建一次格式化器并重用⁠:

      const currencyFormatter
        = new Intl.NumberFormat(
          "zh-CN",
          {
            style: "currency",
            currency: "CNY",
          },
        );
      
      function formatPrice(value) {
        return currencyFormatter.format(
          value,
        );
      }

      这能够集中配置⁠,也避免在每次调用时重复构造对象⁠。是否具有可测性能收益仍应根据工作负载验证⁠。

      7.25.2 展示格式不用于机器解析

      formatter.format(
        1234.5,
      );

      结果可能包含⁠:

      • 不同数字字符⁠;
      • 不间断空格⁠;
      • 不同分组符⁠;
      • 不同货币位置⁠;
      • 区域特定文字⁠。

      不要把本地化展示字符串再次交给 Number() 或手工正则解析⁠。

      机器数据应保持结构化数值⁠、ISO 日期或明确协议⁠,只有界面层进行本地化格式化⁠。

      7.25.3 显式区域和时区

      依赖用户环境默认值可以提供自然体验⁠,但会降低测试和服务端输出的确定性⁠。

      可以根据场景选择⁠:

      • 用户界面⁠:使用用户配置区域⁠;
      • 日志⁠:使用统一时区和机器可读格式⁠;
      • 测试⁠:显式指定 locale 和 timeZone⁠;
      • API⁠:传输结构化值⁠,不传输仅供展示的本地化文本⁠。

      7.26 年度 ECMAScript 演进概览

      下表只列出与本系列讲义关系较大的代表性特性⁠,不是完整版本清单⁠。

      版本代表性特性
      ECMAScript 2015let⁠、const⁠、箭头函数⁠、类⁠、模块⁠、Promise⁠、Symbol⁠、Map⁠、Set⁠、Proxy⁠、生成器⁠、解构⁠、展开和模板字面量
      ECMAScript 2017async 与 await
      ECMAScript 2020BigInt⁠、Promise.allSettled()⁠、globalThis⁠、可选链⁠、空值合并
      ECMAScript 2021Promise.any()⁠、逻辑赋值⁠、数值分隔符⁠、replaceAll()⁠、WeakRef 与 FinalizationRegistry
      ECMAScript 2022顶级 await⁠、公有和私有类字段⁠、私有方法⁠、静态块⁠、Error.cause⁠、.at()
      ECMAScript 2023toSorted()⁠、toReversed()⁠、toSpliced()⁠、with()⁠、findLast()⁠、findLastIndex()
      ECMAScript 2024Promise.withResolvers()⁠、Object.groupBy()⁠、Map.groupBy()⁠、可调整 ArrayBuffer⁠、String.prototype.isWellFormed()
      ECMAScript 2025Iterator Helpers⁠、Set 集合方法⁠、JSON 模块和导入属性⁠、RegExp.escape()⁠、Promise.try()⁠、Float16Array

      年度名称表示规范版本⁠,不表示所有浏览器和运行时在该年份同时完整支持全部功能⁠。

      ECMAScript 2026 是当前系列讲义校对时采用的语言规范版本⁠。新提案只有在进入正式 ECMAScript 规范后⁠,才能作为标准语言特性介绍⁠;处于 TC39 提案阶段的功能应明确标注阶段和兼容性⁠,不能与正式标准混写⁠。

      7.27 兼容性与工具链

      7.27.1 规范存在不等于运行环境支持

      一个功能已经写入 ECMAScript 标准⁠,只表示其语义已经标准化⁠,不表示所有目标环境已经实现⁠。

      例如⁠,Iterator Helpers 在 ECMAScript 2025 中标准化⁠,但旧浏览器和旧 Node.js 版本可能仍然缺少相关接口⁠。

      7.27.2 语法转换与 API 填充

      较旧环境缺少功能时⁠,工具链可能采用两种方式⁠:

      语法转换(⁠Transpilation⁠)

      把新语法转换为旧语法⁠,例如⁠:

      • 箭头函数⁠;
      • 类⁠;
      • 可选链⁠;
      • 解构⁠;
      • async/await⁠。

      Polyfill

      提供缺失的运行时 API⁠,例如⁠:

      • Promise⁠;
      • Array.prototype.toSorted()⁠;
      • Iterator Helpers⁠;
      • Intl 的部分能力⁠。

      二者不能混为一谈⁠。语法转换器不会自动使所有新内置对象都存在⁠。

      7.27.3 无法完整 Polyfill 的机制

      某些语言机制无法在旧环境中用普通 JavaScript 完整模拟⁠,例如⁠:

      • Proxy⁠;
      • 私有字段的所有语义⁠;
      • WeakRef⁠;
      • 精确的尾部调用机制⁠;
      • 某些底层二进制和共享内存能力⁠。

      工具可能提供近似转换⁠,但行为⁠、性能和反射结果不一定完全一致⁠。

      7.27.4 功能检测

      const supportsIteratorHelpers = (
        typeof Iterator
        !== "undefined"
        && typeof Iterator.prototype.map
          === "function"
      );
      const supportsStructuredClone = (
        typeof structuredClone
        === "function"
      );

      功能检测比根据浏览器名称猜测更可靠⁠,但仍需考虑⁠:

      • API 是否完整⁠;
      • 是否存在已知实现缺陷⁠;
      • 是否需要特定选项⁠;
      • 是否满足性能和安全要求⁠。

      7.27.5 明确目标环境

      工程应定义支持范围⁠,例如⁠:

      • 最新两个主要浏览器版本⁠;
      • 特定企业浏览器⁠;
      • Node.js 长期支持版本⁠;
      • 桌面应用内置 WebView⁠;
      • 移动端最低系统版本⁠。

      随后再配置⁠:

      • 构建目标⁠;
      • Browserslist⁠;
      • polyfill⁠;
      • 自动化测试⁠;
      • 条件加载⁠;
      • 降级策略⁠。

      不要为了追求“⁠现代语法⁠”盲目使用尚未覆盖目标环境的接口⁠,也不要为了极旧环境永久放弃已经成熟的标准能力⁠。

      7.28 现代语法的设计原则

      7.28.1 语法简洁不等于逻辑简单

      const {
        profile: {
          settings: {
            appearance: {
              theme = "light",
            } = {},
          } = {},
        } = {},
      } = user ?? {};

      这段代码虽然只是一条解构语句⁠,但理解成本较高⁠。

      可以拆分⁠:

      const profile = user?.profile;
      const settings = profile?.settings;
      const appearance = settings?.appearance;
      
      const theme = (
        appearance?.theme
        ?? "light"
      );

      应以表达逻辑为目标⁠,而不是以减少行数为目标⁠。

      7.28.2 协议比具体类型更重要

      如果函数只需要可迭代输入⁠,可以接受广泛类型⁠:

      function collect(iterable) {
        return [
          ...iterable,
        ];
      }

      调用者可以传入⁠:

      • Array⁠;
      • Set⁠;
      • String⁠;
      • 自定义可迭代对象⁠;
      • 生成器⁠。

      这种基于协议的接口通常比强制要求数组更灵活⁠。

      7.28.3 惰性处理需要明确生命周期

      迭代器和生成器能够避免一次保存全部值⁠,但也引入一次性消费和资源关闭问题⁠。

      设计接口时应说明⁠:

      • 是否可以重复遍历⁠;
      • 谁负责关闭⁠;
      • 提前退出是否清理资源⁠;
      • 错误如何传播⁠;
      • 是否允许并发消费⁠;
      • 数据源是否仍然有效⁠。

      7.28.4 元编程应保持局部

      Proxy⁠、Reflect⁠、动态构造和自定义协议能够改变基础行为⁠,但会增加间接性⁠。

      应优先⁠:

      1. 使用普通属性和函数⁠;
      2. 通过明确接口解决问题⁠;
      3. 只在重复模式稳定后引入元编程⁠;
      4. 限制代理和动态代码的作用范围⁠;
      5. 为边界行为编写测试⁠;
      6. 记录与普通对象语义不同的部分⁠。

      7.28.5 国际化不是字符串替换

      本地化不仅是把英文文本替换为中文文本⁠,还包括⁠:

      • 数字和货币⁠;
      • 日期与时区⁠;
      • 复数⁠;
      • 列表连接⁠;
      • 字典序⁠;
      • 文本分段⁠;
      • 语言方向⁠;
      • 区域约定⁠。

      Intl 提供了底层格式化能力⁠,复杂产品还需要消息资源⁠、翻译流程和界面布局策略⁠。

      7.29 本章小结

      本章系统整理了现代 JavaScript 中的语法组合⁠、协议和元编程机制⁠:

      1. “⁠ES6+⁠”是非正式统称⁠,现代 ECMAScript 按年度发布规范⁠;
      2. 数组式解构依赖可迭代协议⁠,对象式解构依据属性键读取⁠;
      3. 解构默认值只在结果为 undefined 时使用⁠,不会替换 null⁠;
      4. 数组剩余元素创建数组⁠,对象剩余属性收集尚未排除的自有可枚举属性⁠;
      5. 解构和展开都是浅层操作⁠,不会自动深复制嵌套对象⁠;
      6. 函数和数组展开要求可迭代对象⁠,对象展开读取自有可枚举属性⁠;
      7. 标签模板把静态文本数组和插值值分别交给标签函数⁠,标签函数可以返回任意值⁠;
      8. 同一标签模板调用点会复用同一个冻结的模板数组⁠,并提供 raw 原始文本⁠;
      9. 标签模板本身不会自动防止 XSS⁠,安全编码必须依据 HTML⁠、属性⁠、URL⁠、CSS 等具体上下文⁠;
      10. 可迭代对象通过 [Symbol.iterator]() 产生迭代器⁠,迭代器通过 next() 产生迭代结果⁠;
      11. 迭代器可以通过 return() 响应提前结束并执行清理⁠;
      12. 生成器是创建惰性可迭代迭代器的语言机制⁠,但不是异步或并行机制⁠;
      13. next(value) 的参数会成为上一个 yield 表达式的结果⁠,第一次 next() 的参数通常被忽略⁠;
      14. yield* 可以把迭代委托给另一个可迭代对象⁠;
      15. ECMAScript 2025 的 Iterator Helpers 提供惰性的 map()⁠、filter()⁠、take() 和其他辅助方法⁠;
      16. Iterator Helpers 会消费一次性迭代器⁠,需要重复计算时应重新创建数据源⁠;
      17. 异步迭代器的 next() 返回 Promise⁠,for await...of 会按顺序等待每一步结果⁠;
      18. 异步生成器适合分页数据⁠、网络流和其他需要异步等待的惰性序列⁠;
      19. Proxy 可以拦截若干对象基本操作⁠,但不能拦截局部变量⁠、严格相等和私有字段等所有语言行为⁠;
      20. Proxy 陷阱必须遵守目标对象的不变量⁠,否则操作会抛出 TypeError⁠;
      21. receiver 对 getter⁠、继承和代理链的 this 语义很重要⁠,Reflect 提供了接近默认操作的转发接口⁠;
      22. Proxy 与目标身份不同⁠,也不能自动获得 Map⁠、Date 等目标对象的内部槽⁠;
      23. 私有字段依赖对象品牌⁠,普通代理无法透明转发私有字段访问⁠;
      24. Proxy.revocable() 可以撤销代理⁠,但不会销毁目标对象⁠;
      25. Reflect 提供函数式的对象操作接口⁠,并与 Proxy 陷阱名称基本对应⁠;
      26. globalThis 提供跨宿主访问全局对象的统一入口⁠,但全局词法绑定不一定是全局对象属性⁠;
      27. new Function() 在全局作用域中解析自由变量⁠,不捕获创建位置的局部词法环境⁠;
      28. 直接 eval() 可以访问局部作用域⁠,间接 eval() 在全局作用域执行⁠;
      29. eval() 和 new Function() 都不是安全沙箱⁠,不应执行未受信任输入⁠;
      30. Intl 由 ECMA-402 定义⁠,提供区域敏感的数字⁠、日期⁠、排序⁠、复数⁠、列表和文本分段能力⁠;
      31. 本地化输出只适合展示⁠,不应作为机器数据格式重新解析⁠;
      32. 标准化⁠、浏览器实现⁠、语法转换和 polyfill 是不同层次⁠,使用现代功能前应明确目标环境⁠;
      33. 现代语法的价值在于表达协议和结构⁠,而不是追求更短代码或未经验证的性能优势⁠。

      参考资料

      1. javascript.info: Destructuring assignment
      2. javascript.info: Iterables
      3. javascript.info: Generators
      4. javascript.info: Async iteration and generators
      5. javascript.info: Proxy and Reflect
      6. javascript.info: Global object
      7. javascript.info: The new Function syntax
      8. javascript.info: Eval: run a code string
      9. MDN JavaScript Guide: Iterators and generators
      10. MDN JavaScript Guide: Meta programming
      11. MDN JavaScript Guide: Internationalization
      12. MDN JavaScript Reference: Iteration protocols
      13. MDN JavaScript Reference: Template literals
      14. MDN JavaScript Reference: Iterator
      15. MDN JavaScript Reference: Proxy
      16. MDN JavaScript Reference: Reflect
      17. MDN JavaScript Reference: eval
      18. MDN JavaScript Reference: Function constructor
      19. MDN JavaScript Reference: Intl
      20. ECMAScript 2026 Language Specification
      21. ECMAScript 2026: ECMAScript Language—Expressions
      22. ECMAScript 2026: Control Abstraction Objects
      23. ECMAScript 2026: Reflection
      24. ECMAScript 2026: Global Object
      25. ECMAScript Internationalization API Specification
      上一篇 JavaScript 6. 原型继承与面向对象编程 2026 年 7 月 19 日 下一篇 JavaScript 8. 错误处理与异常流控制 2026 年 7 月 19 日
      © 2026 CHEN Hua All rights reserved
      闽ICP备2026003335号 · 粤公网安备44030002014022号
      © Hua Chen / PhysChen.com