发布时间:2018年6月 ES9 主要完善了异步迭代、对象扩展、正则表达式等功能。
1. 异步迭代器(Async Iteration)
for await...of
用于遍历异步可迭代对象(异步生成器、异步的流等):
js
for await (const line of readLines(filePath)) {
console.log(line);
}
异步生成器
js
async function* asyncGenerator() {
let i = 0;
while (i < 3) {
await new Promise(res => setTimeout(res, 1000));
yield i++;
}
}
(async function() {
for await (let val of asyncGenerator()) {
console.log(val); // 每秒输出 0, 1, 2
}
})();
实际应用:分批获取数据
js
async function* fetchPages(urls) {
for (let url of urls) {
let res = await fetch(url);
yield await res.json();
}
}
(async function() {
let pages = fetchPages(['/api/page1', '/api/page2', '/api/page3']);
for await (let page of pages) {
console.log(page);
}
})();
与同步迭代器的区别
js
// 同步迭代器
obj[Symbol.iterator] // 返回 { next() => { value, done } }
// 异步迭代器
obj[Symbol.asyncIterator] // 返回 { next() => Promise({ value, done }) }
2. 对象展开运算符(Object Spread)
ES6 引入了数组展开运算符,ES9 将其扩展到对象:
展开合并对象
js
let defaults = { theme: 'light', lang: 'zh' };
let userPrefs = { theme: 'dark', fontSize: 14 };
// 合并对象(后面的覆盖前面的)
let settings = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'zh', fontSize: 14 }
克隆对象(浅拷贝)
js
let original = { a: 1, b: { c: 2 } };
let clone = { ...original };
clone.a = 10; // 不影响 original
clone.b.c = 20; // 影响 original(浅拷贝)
覆盖部分属性
js
let user = { name: '张三', age: 18, city: '北京' };
let updatedUser = { ...user, age: 19 };
// { name: '张三', age: 19, city: '北京' }
添加新属性
js
let user = { name: '张三' };
let withId = { ...user, id: 1 };
// { name: '张三', id: 1 }
注意事项
- 展开运算符只展开对象自身的可枚举属性
- 原型链上的属性不会被展开
- 值为 undefined 的属性仍会被包含
js
let obj = { a: undefined, b: null, c: 1 };
let copy = { ...obj }; // { a: undefined, b: null, c: 1 }
3. 对象剩余运算符(Object Rest)
解构对象时收集剩余属性:
基本用法
js
let { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 };
console.log(a); // 1
console.log(b); // 2
console.log(rest); // { c: 3, d: 4 }
函数参数中使用
js
function updateUser({ id, ...changes }) {
// id 单独取出,其余作为修改项
console.log(`更新用户 ${id},修改内容:`, changes);
}
updateUser({ id: 1, name: '李四', age: 20 });
// 更新用户 1,修改内容: { name: '李四', age: 20 }
剔除某些属性
js
let { password, ...safeUser } = { name: '张三', age: 18, password: '123' };
console.log(safeUser); // { name: '张三', age: 18 }
嵌套解构
js
let { a: { x, ...restA }, ...restObj } = { a: { x: 1, y: 2 }, b: 3 };
console.log(x); // 1
console.log(restA); // { y: 2 }
console.log(restObj); // { b: 3 }
注意
- 剩余属性必须在最后
- 剩余运算符得到的始终是普通对象
null和undefined不能用展开/剩余运算符
4. Promise.prototype.finally()
无论 Promise 成功还是失败,都会执行的回调:
基本用法
js
fetchData()
.then(data => console.log(data))
.catch(err => console.error(err))
.finally(() => {
console.log('请求结束,隐藏loading');
hideLoading();
});
特点
finally不接收参数,不知道 Promise 是成功还是失败- 返回的 Promise 会继承前面 Promise 的结果
js
Promise.resolve('ok')
.finally(() => {
console.log('清理资源');
// 没有返回值或返回普通值,不影响最终结果
})
.then(res => console.log(res)); // 'ok'
对比 try...catch...finally
js
// Promise 方式
fetchData()
.then(data => processData(data))
.catch(err => handleError(err))
.finally(() => cleanup());
// 等同于 try...catch...finally 的效果
async function handle() {
try {
let data = await fetchData();
processData(data);
} catch (err) {
handleError(err);
} finally {
cleanup();
}
}
实际应用
js
// 数据库连接
function queryDB() {
let conn = connectDB();
return conn.query('SELECT * FROM users')
.finally(() => conn.close()); // 确保连接关闭
}
// 文件操作
function readConfig() {
let file = openFile('config.json');
return readFile(file)
.finally(() => file.close()); // 确保文件关闭
}
5. 正则表达式扩展
5.1 命名捕获组(Named Capture Groups)
用 (?<name>...) 为捕获组命名:
js
// 旧写法:通过索引访问
let re = /(\d{4})-(\d{2})-(\d{2})/;
let match = '2023-12-25'.match(re);
console.log(match[1]); // '2023'(年)
console.log(match[2]); // '12'(月)
console.log(match[3]); // '25'(日)
// 新写法:通过名称访问
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
let match = '2023-12-25'.match(re);
console.log(match.groups.year); // '2023'
console.log(match.groups.month); // '12'
console.log(match.groups.day); // '25'
解构使用命名捕获组
js
let { groups: { year, month, day } } =
'2023-12-25'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
在 replace 中使用
js
let re = /(?<firstName>\w+)\s(?<lastName>\w+)/;
let str = 'John Smith'.replace(re, '$<lastName>, $<firstName>');
console.log(str); // 'Smith, John'
5.2 反向断言(Lookbehind Assertions)
在匹配位置的前面或后面添加条件判断:
js
// 正向先行断言(ES5 就有):后面必须跟某个模式
// (?=...) 后面跟...
// (?!...) 后面不跟...
// 反向先行断言(ES9 新增):前面必须跟某个模式
// (?<=...) 前面跟...
// (?<!...) 前面不跟...
// 匹配价格数字(前面有 $ 符号)
let str = '商品价格 $100,运费 $20';
let prices = str.match(/(?<=\$)\d+/g);
console.log(prices); // ['100', '20']
// 匹配不以 $ 开头的数字
let nums = 'a123 $456 c789'.match(/(?<!\$)\d+/g);
console.log(nums); // ['123', '789']
5.3 正则表达式 dotAll 模式
dotAll 标志 s,让 . 匹配包括换行符在内的所有字符:
js
// 默认情况下,. 不匹配换行符
/hello.world/.test('hello\nworld'); // false
// dotAll 模式下,. 匹配换行符
/hello.world/s.test('hello\nworld'); // true
5.4 正则表达式 Unicode 转义
在正则中可以使用 \p{...} 匹配 Unicode 字符类别:
js
// 匹配任何 Unicode 字母(包括中文等)
/\p{Letter}/u.test('你'); // true
/\p{Letter}/u.test('A'); // true
/\p{Letter}/u.test('1'); // false
// 匹配 Unicode 空白
/\p{White_Space}/u.test('\n'); // true
// 否定匹配
/\P{Letter}/u.test('1'); // true,非字母
总结
| 特性 | 说明 | 重要性 |
|---|---|---|
for await...of |
异步迭代器 | ⭐⭐⭐⭐ |
对象展开运算符 ... |
对象合并、克隆 | ⭐⭐⭐⭐ |
对象剩余运算符 ...rest |
解构时收集剩余属性 | ⭐⭐⭐⭐ |
Promise.finally() |
无论成败都执行 | ⭐⭐⭐⭐ |
| 命名捕获组 | 正则捕获组可命名 | ⭐⭐⭐ |
| 反向断言 | 正则前面匹配条件 | ⭐⭐⭐ |
| dotAll 模式 | . 匹配换行符 |
⭐⭐ |
| Unicode 转义 | 正则匹配 Unicode 类别 | ⭐⭐ |