跳到主要内容

ES9 (ES2018)

· 阅读需 10 分钟

ES2018(ES9)引入了一些重要的新特性,尤其是对正则表达式、异步迭代和对象的增强。这些更新为开发者提供了更强大的工具来处理字符串匹配、异步数据流和对象操作,进一步提升了 JavaScript 的功能性和开发效率。

ES2018(ES9)引入了一些重要的新特性,尤其是对正则表达式、异步迭代和对象的增强。

  1. Asynchronous Iteration (for await...of)
  2. Rest/Spread Properties for Objects
  3. RegExp Named Capture Groups
  4. RegExp Lookbehind Assertions
  5. RegExp Unicode Property Escapes
  6. s (dotAll) Flag for Regular Expressions
  7. Promise.prototype.finally( )

1. Asynchronous Iteration (for await...of)

ES2018 添加了 for await...of 循环,用于迭代异步可迭代对象。这种语法允许你在异步流中逐步获取数据,非常适合处理异步数据流(如网络请求或读取文件流)。

一个基础的示例:

const asyncObj = {
async *[Symbol.asyncIterator]() {
yield 1;
yield 2;
yield 3;
},
};

async function proceeAsyncData(data) {
for await (let item of data) {
console.log(item);
}
}

proceeAsyncData(asyncObj);

实际应用场景举例:

假设我们开发一个从服务器上分页获取用户数据的应用,每次只能获取一页(如10个用户)的数据,直到所有数据加载完成。这个场景在实际开发中非常常见,尤其是在处理大数据集时。

/**
* 根据传入的页码数返回对应页码的用户数据
* @param {*} page 页码数
*/
async function fetchUser(page) {
// 假设每页10个用户
const pageSize = 10;

// 模拟服务器延迟
await new Promise((resolve) => setTimeout(resolve, 1500));

// 总共的用户数据
const totalUsers = 45;

// 计算当前页对应的起始值和结束值
const start = (page - 1) * pageSize;
const end = Math.min(start + pageSize, totalUsers);

// 如果没有更多的用户,就返回空数组
if (start >= totalUsers) return [];

// 生成当前页的用户数据
const users = Array.from({ length: end - start }, (_, i) => {
return {
id: start + i + 1,
name: `用户${start + i + 1}`,
};
});

return users;
}

// 异步生成器函数:分页获取所有的用户
async function* fetchAllUsers() {
let page = 1; // 一开始从第一页开始
let hasMore = true; // 是否还有更多的用户数据

while (hasMore) {
// 根据当前页码拿到对应的用户数据
const users = await fetchUser(page);
if (users.length === 0) {
hasMore = false;
} else {
yield users;
page++;
}
}
}

async function processAllUsers() {
let allUsers = [];

for await (let users of fetchAllUsers()) {
console.log(`当前页的用户数据:`, users);
allUsers = allUsers.concat(users);
}

console.log(`所用的用户数据:`, allUsers);
}
processAllUsers();

2. Rest/Spread Properties for Objects

在 ES2018 中,... 运算符(也称为"扩展运算符")可以用于对象。

  • 通过 rest 运算符,可以将对象的其余属性提取到新对象中
  • 通过 spread 运算符,可以将对象的属性合并到另一个对象中

rest示例:

const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4, e: 5 };
console.log(a, b, rest); // 1 2 { c: 3, d: 4, e: 5 }

spread示例:

const o1 = {
a: 1,
b: 2,
};
const o2 = {
c: 3,
d: 4,
};
const merged = { ...o1, ...o2 };
console.log(merged);

3. RegExp Named Capture Groups

ES2018 增加了在正则表达式中使用 命名捕获组 的功能。通过命名捕获组,你可以给捕获的子字符串命名,便于后续处理和引用。

先看一下以前捕获组没有命名的例子:

// 没有使用命名捕获组
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const result = regex.exec("2024-09-11");

console.log(result[0]); // '2024-09-11' (整个匹配的字符串)
console.log(result[1]); // '2024' (第一组捕获到的年份)
console.log(result[2]); // '09' (第二组捕获到的月份)
console.log(result[3]); // '11' (第三组捕获到的日期)

现在可以直接为捕获组添加命名了:

const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = regex.exec("2024-09-11");

console.log(result);
console.log(result.groups); // { year: '2024', month: '09', day: '11' }
console.log(result.groups.year); // '2024' (第一组捕获到的年份)
console.log(result.groups.month); // '09' (第二组捕获到的月份)
console.log(result.groups.day); // '11' (第三组捕获到的日期)

4. RegExp Lookbehind Assertions

ES2018 中,正则表达式支持后行断言,也称为"lookbehind assertions"。

正则表达式中的"断言"可以理解为对某个位置做出一个条件检查,但本身并不消耗字符。断言的本质是"在这里的前面/后面是否有某个东西",而不会将这个东西作为匹配结果的一部分

快速上手示例:检查一个数字后面是否有 px 字符串

假设想匹配一个数字,但这个数字后面必须有 px,且你只想要这个数字,不包括 px 本身。这时我们可以使用正向前行断言来实现:

const reg = /\d+(?=px)/; //匹配数字,并且后面是px
console.log("123px".match(reg)); // [ '123', index: 0, input: '123px', groups: undefined ]

早期只支持 前行断言

  1. 正向前行断言 (?=...):检查某个模式的 后面 是否特定字符。

    const reg = /\d+(?=px)/;
  2. 负向前行断言 (?!...):检查某个模式的 后面 是否 没有 特定字符。

    const reg = /\d+(?!px)/; //匹配数字,并且后面不是px
    console.log("123ab".match(reg)); // [ '123', index: 0, input: '123ab', groups: undefined ]

从 ES2018 开始支持 后行断言

  1. 正向后行断言(?<=...):确保某个模式前面 是否特定字符

    // 匹配数字,但是数字前面能有$
    const reg = /(?<=\$)\d+/;
    console.log("100$456".match(reg)); // [ '456', index: 4, input: '100$456', groups: undefined ]
  2. 负向后行断言(?<!...):确保某个模式前面 是否没有特定字符

    const reg = /(?<!\$)\d+/;
    console.log("100$456".match(reg)); // [ '100', index: 0, input: '100$456', groups: undefined ]

5. RegExp Unicode Property Escapes

ES2018 为正则表达式引入了Unicode 属性转义,使得开发者可以通过 \p 和 \P 语法匹配具有特定 Unicode 属性的字符。这极大增强了处理多语言字符的能力。

  • \p : 指定对应的 Unicode 字符(指定汉语、拉丁语、希腊语...)
  • \P: 指定不能有对应的 Unicode 字符(相当于排除)
const regExp = /\p{Script=Greek}/u;
console.log("α".match(regExp)); // [ 'α', index: 0, input: 'α', groups: undefined ]
console.log("a".match(regExp)); // null


const regExp2 = /\p{Script=Han}/u;
console.log(regExp2.test("你好")); // true

const regExp3 = /\p{Emoji}/u;
console.log(regExp3.test("😄")); // true

6. s (dotAll) Flag for Regular Expressions

正则表达式中的 . 是一个通配符,用来匹配除换行符(\n)之外的任何单个字符。在 ES2018 之前,. 不能匹配换行符,因此当你需要匹配跨行的文本时,需要编写更复杂的正则表达式来处理换行符。

举个例子:

const regex = /foo.bar/;
const str = "foo\nbar";
console.log(regex.test(str)); // false

于是只能曲线救国,使用 [\s\S],其中 \s 匹配空白字符(包括换行符),\S 匹配非空白字符,这样可以让 . 的功能扩展到所有字符。

const regex = /foo[\s\S]bar/;
const str = "foo\nbar";
console.log(regex.test(str)); // true

ES2018 引入了 s 标志(也叫 dotAll 模式),让 . 能够匹配包括换行符在内的所有字符,从而简化了跨行文本的匹配。

const regex = /foo.bar/s;
const str = "foo\nbar";
console.log(regex.test(str)); // true

7. Promise.prototype.finally( )

ES2018 为 Promise 对象添加了 finally( ) 方法,它允许你在 Promise 被处理完(不论是成功还是失败)之后执行代码。无论 Promise 的结果是 resolve 还是 reject,finally( ) 中的代码都会执行。这非常适合进行清理操作。

fetch("./data.json")
.then((response) => response.json())
.catch((error) => console.error("Error:", error))
.finally(() => console.log("Finished fetching data."));

实际应用场景举例:数据加载时的网络请求 + 加载动画

function showLoading() {
console.log("loading...");
}

function hideLoading() {
console.log("hide loading...");
}

// 请求数据
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const isSuccess = Math.random() > 0.5;
if (isSuccess) {
resolve("数据加载成功");
} else {
reject("数据加载失败");
}
}, 2000);
});
}

function loadData() {
showLoading();
fetchData()
.then((data) => {
console.log("成功:", data);
})
.catch((err) => {
console.log("失败:", err);
})
.finally(() => {
// 无论请求成功还是失败都会执行
hideLoading();
});
}
loadData();

-EOF-