目录
JavaScript 7. 现代语法、协议与元编程
“ES6+”是工程实践中的非正式说法,通常泛指 ECMAScript 2015 及其后的语言特性。ECMAScript 2015 曾经是一次规模较大的版本更新,此后 ECMAScript 改为按年度发布规范,因此现代 JavaScript 并不是一组一次性加入、边界固定的“ES6 语法”。
前面的章节已经系统介绍了若干现代特性:
- 第一章:
let、const、模板字面量基础、可选链和空值合并; - 第二章:箭头函数、剩余参数和函数作用域;
- 第四章:Promise、
async和await; - 第五章:Symbol、Map、Set 与现代数组方法;
- 第六章:类、私有元素和静态成员;
- 第九章:ES Modules 和动态导入。
本章不重复这些内容,而是集中讨论现代 JavaScript 中更具组合性的语言机制:
- 解构赋值与展开语法;
- 标签模板;
- 可迭代协议与迭代器协议;
- 生成器与异步生成器;
- ECMAScript 2025 引入的 Iterator Helpers;
- Proxy 与 Reflect;
globalThis、new Function()与eval();Intl国际化接口;- 年度标准与运行环境兼容性。
这些机制的共同特点是:它们并不只提供更短的语法,还建立了对象与语言结构之间的协议。例如,只要对象实现了 [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("<", "<")
.replaceAll(">", ">");
}
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);
}
这会按顺序:
- 等待下一个值;
- 等待当前值处理完成;
- 再请求下一个值。
它适合流式顺序处理。
如果已经拥有一组彼此独立的 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 拦截的是对象操作
常见陷阱包括:
| 陷阱 | 对应操作示例 |
|---|---|
get | proxy.key |
set | proxy.key = value |
has | "key" in proxy |
deleteProperty | delete proxy.key |
ownKeys | Reflect.ownKeys(proxy) |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor(proxy, key) |
defineProperty | Object.defineProperty(proxy, key, descriptor) |
getPrototypeOf | Object.getPrototypeOf(proxy) |
setPrototypeOf | Object.setPrototypeOf(proxy, prototype) |
isExtensible | Object.isExtensible(proxy) |
preventExtensions | Object.preventExtensions(proxy) |
apply | proxy(...args) |
construct | new 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 2015 | let、const、箭头函数、类、模块、Promise、Symbol、Map、Set、Proxy、生成器、解构、展开和模板字面量 |
| ECMAScript 2017 | async 与 await |
| ECMAScript 2020 | BigInt、Promise.allSettled()、globalThis、可选链、空值合并 |
| ECMAScript 2021 | Promise.any()、逻辑赋值、数值分隔符、replaceAll()、WeakRef 与 FinalizationRegistry |
| ECMAScript 2022 | 顶级 await、公有和私有类字段、私有方法、静态块、Error.cause、.at() |
| ECMAScript 2023 | toSorted()、toReversed()、toSpliced()、with()、findLast()、findLastIndex() |
| ECMAScript 2024 | Promise.withResolvers()、Object.groupBy()、Map.groupBy()、可调整 ArrayBuffer、String.prototype.isWellFormed() |
| ECMAScript 2025 | Iterator 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、动态构造和自定义协议能够改变基础行为,但会增加间接性。
应优先:
- 使用普通属性和函数;
- 通过明确接口解决问题;
- 只在重复模式稳定后引入元编程;
- 限制代理和动态代码的作用范围;
- 为边界行为编写测试;
- 记录与普通对象语义不同的部分。
7.28.5 国际化不是字符串替换
本地化不仅是把英文文本替换为中文文本,还包括:
- 数字和货币;
- 日期与时区;
- 复数;
- 列表连接;
- 字典序;
- 文本分段;
- 语言方向;
- 区域约定。
Intl 提供了底层格式化能力,复杂产品还需要消息资源、翻译流程和界面布局策略。
7.29 本章小结
本章系统整理了现代 JavaScript 中的语法组合、协议和元编程机制:
- “ES6+”是非正式统称,现代 ECMAScript 按年度发布规范;
- 数组式解构依赖可迭代协议,对象式解构依据属性键读取;
- 解构默认值只在结果为
undefined时使用,不会替换null; - 数组剩余元素创建数组,对象剩余属性收集尚未排除的自有可枚举属性;
- 解构和展开都是浅层操作,不会自动深复制嵌套对象;
- 函数和数组展开要求可迭代对象,对象展开读取自有可枚举属性;
- 标签模板把静态文本数组和插值值分别交给标签函数,标签函数可以返回任意值;
- 同一标签模板调用点会复用同一个冻结的模板数组,并提供
raw原始文本; - 标签模板本身不会自动防止 XSS,安全编码必须依据 HTML、属性、URL、CSS 等具体上下文;
- 可迭代对象通过
[Symbol.iterator]()产生迭代器,迭代器通过next()产生迭代结果; - 迭代器可以通过
return()响应提前结束并执行清理; - 生成器是创建惰性可迭代迭代器的语言机制,但不是异步或并行机制;
next(value)的参数会成为上一个yield表达式的结果,第一次next()的参数通常被忽略;yield*可以把迭代委托给另一个可迭代对象;- ECMAScript 2025 的 Iterator Helpers 提供惰性的
map()、filter()、take()和其他辅助方法; - Iterator Helpers 会消费一次性迭代器,需要重复计算时应重新创建数据源;
- 异步迭代器的
next()返回 Promise,for await...of会按顺序等待每一步结果; - 异步生成器适合分页数据、网络流和其他需要异步等待的惰性序列;
- Proxy 可以拦截若干对象基本操作,但不能拦截局部变量、严格相等和私有字段等所有语言行为;
- Proxy 陷阱必须遵守目标对象的不变量,否则操作会抛出
TypeError; receiver对 getter、继承和代理链的this语义很重要,Reflect 提供了接近默认操作的转发接口;- Proxy 与目标身份不同,也不能自动获得 Map、Date 等目标对象的内部槽;
- 私有字段依赖对象品牌,普通代理无法透明转发私有字段访问;
Proxy.revocable()可以撤销代理,但不会销毁目标对象;- Reflect 提供函数式的对象操作接口,并与 Proxy 陷阱名称基本对应;
globalThis提供跨宿主访问全局对象的统一入口,但全局词法绑定不一定是全局对象属性;new Function()在全局作用域中解析自由变量,不捕获创建位置的局部词法环境;- 直接
eval()可以访问局部作用域,间接eval()在全局作用域执行; eval()和new Function()都不是安全沙箱,不应执行未受信任输入;Intl由 ECMA-402 定义,提供区域敏感的数字、日期、排序、复数、列表和文本分段能力;- 本地化输出只适合展示,不应作为机器数据格式重新解析;
- 标准化、浏览器实现、语法转换和 polyfill 是不同层次,使用现代功能前应明确目标环境;
- 现代语法的价值在于表达协议和结构,而不是追求更短代码或未经验证的性能优势。
参考资料
- javascript.info: Destructuring assignment
- javascript.info: Iterables
- javascript.info: Generators
- javascript.info: Async iteration and generators
- javascript.info: Proxy and Reflect
- javascript.info: Global object
- javascript.info: The new Function syntax
- javascript.info: Eval: run a code string
- MDN JavaScript Guide: Iterators and generators
- MDN JavaScript Guide: Meta programming
- MDN JavaScript Guide: Internationalization
- MDN JavaScript Reference: Iteration protocols
- MDN JavaScript Reference: Template literals
- MDN JavaScript Reference: Iterator
- MDN JavaScript Reference: Proxy
- MDN JavaScript Reference: Reflect
- MDN JavaScript Reference: eval
- MDN JavaScript Reference: Function constructor
- MDN JavaScript Reference: Intl
- ECMAScript 2026 Language Specification
- ECMAScript 2026: ECMAScript Language—Expressions
- ECMAScript 2026: Control Abstraction Objects
- ECMAScript 2026: Reflection
- ECMAScript 2026: Global Object
- ECMAScript Internationalization API Specification