- Promise.withResolvers
- Object.groupBy / Map.groupBy
- ArrayBuffer.prototype.resize
- ArrayBuffer.prototype.transfer
- String.prototype.isWellFormed
- String.prototype.toWellFormed
- Atomics.waitAsync
- 正则表达式 v 标志
Promise.withResolvers()
Promise.withResolvers() 允许创建一个新的 Promise,并同时获得 resolve 和 reject 函数。这在某些场景下非常有用,特别是当需要同时访问到 Promise 的 resolve 和 reject 函数时。
Promise.withResolvers() 完全等同于以下代码:
let resolve, reject
const promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
通常,当创建一个新的 Promise 时,会传递一个执行器函数给 Promise 构造函数,这个执行器函数接收两个参数:resolve 和 reject 函数。但在某些情况下,可能想要在 Promise 创建之后仍然能够访问到这两个函数。这就是 Promise.withResolvers() 的用武之地。
使用 Promise.withResolvers() 的一个例子:
const { promise, resolve, reject } = Promise.withResolvers()
setTimeout(() => {
resolve('成功!')
}, 1000)
promise.then(value => {
console.log(value)
})
在这个例子中,首先通过 Promise.withResolvers() 创建了一个新的 Promise,并同时获得了 resolve 和 reject 函数。然后,在 setTimeout 回调中使用 resolve 函数来解析这个 Promise。最后,添加了一个 .then 处理程序来处理 Promise 解析后的值。
使用 Promise.withResolvers() 关键的区别在于解决和拒绝函数现在与 Promise 本身处于同一作用域,而不是在执行器中被创建和一次性使用。这可能使得一些更高级的用例成为可能,例如在重复事件中重用它们,特别是在处理流和队列时。这通常也意味着相比在执行器内包装大量逻辑,嵌套会更少。
这个功能对于那些需要更细粒度控制 Promise 的状态,或者在 Promise 创建后仍然需要访问 resolve 和 reject 函数的场景来说非常有用。
Object.groupBy / Map.groupBy
Object.groupBy 和 Map.groupBy 方法用于数组分组。
假设有一个由表示人员的对象组成的数组,需要按照年龄进行分组。可以使用 forEach 循环来实现,代码如下:
const people = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 30 },
{ name: "Eve", age: 28 },
];
const peopleByAge = {};
people.forEach((person) => {
const age = person.age;
if (!peopleByAge[age]) {
peopleByAge[age] = [];
}
peopleByAge[age].push(person);
});
console.log(peopleByAge);
// 输出结果
{
"28": [{"name":"Alice","age":28}, {"name":"Eve","age":28}],
"30": [{"name":"Bob","age":30}]
}
也可以使用 reduce 方法:
const peopleByAge = people.reduce((acc, person) => {
const age = person.age;
if (!acc[age]) {
acc[age] = [];
}
acc[age].push(person);
return acc;
}, {});
无论哪种方式,代码都略显繁琐。每次都要检查对象,看分组键是否存在,如果不存在,则创建一个空数组,并将项目添加到该数组中。
Object.groupBy
可以通过以下方式来使用新的 Object.groupBy 方法
const peopleByAge = Object.groupBy(people, (person) => person.age);
不过需要注意,使用 Object.groupBy 方法返回一个没有原型(即没有继承任何属性和方法)的对象。这意味着该对象不会继承 Object.prototype 上的任何属性或方法,例如 hasOwnProperty 或 toString 等。虽然这样做可以避免意外覆盖 Object.prototype 上的属性,但也意味着不能使用一些与对象相关的方法。
const peopleByAge = Object.groupBy(people, (person) => person.age);
console.log(peopleByAge.hasOwnProperty("28"));
// TypeError: peopleByAge.hasOwnProperty is not a function
在调用 Object.groupBy 时,传递给它的回调函数应该返回一个字符串或 Symbol 类型的值。如果回调函数返回其他类型的值,它将被强制转换为字符串。
在这个例子中,回调函数返回的是一个数字类型的 age 属性值,但由于 Object.groupBy 方法要求键必须是字符串或 Symbol 类型,所以该数字会被强制转换为字符串类型。
console.log(peopleByAge[28]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
console.log(peopleByAge["28"]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
Map.groupBy
Map.groupBy 和 Object.groupBy 几乎做的是相同的事情,只是返回的结果类型不同。Map.groupBy 返回一个 Map 对象,而不是像 Object.groupBy 返回一个普通的对象。
const ceo = { name: "Jamie", age: 40, reportsTo: null };
const manager = { name: "Alice", age: 28, reportsTo: ceo };
const people = [
ceo
manager,
{ name: "Bob", age: 30, reportsTo: manager },
{ name: "Eve", age: 28, reportsTo: ceo },
];
const peopleByManager = Map.groupBy(people, (person) => person.reportsTo);
这里根据人的汇报上级将他们进行了分组。如果想通过对象来从这个 Map 中获取数据,那么要求这些对象具有相同的身份或引用。这是因为 Map 在比较键时使用的是严格相等(===),只有两个对象具有相同的引用,才能被认为是相同的键。
peopleByManager.get(ceo);
// => [{ name: "Alice", age: 28, reportsTo: ceo }, { name: "Eve", age: 28, reportsTo: ceo }]
peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined
在上面的例子中,如果尝试使用与 ceo 对象类似的对象作为键去访问 Map 中的项,由于这个对象与之前存储在 Map 中的 ceo 对象不是同一个对象,所以无法检索到对应的值。
ArrayBuffer.prototype.resize
ArrayBuffer 实例的 resize() 方法将 ArrayBuffer 调整为指定的大小,以字节为单位,前提是该 ArrayBuffer 是可调整大小的并且新的大小小于或等于该 ArrayBuffer 的 maxByteLength。
const buffer = new ArrayBuffer(8, { maxByteLength: 16 })
console.log(buffer.byteLength)
// 8
if(buffer.resizeable) {
console.log("缓冲区大小是可调整的!")
buffer.resize(12)
}
注意:
- 如果 ArrayBuffer 已分离或不可调整大小,则抛出该错误。
- 如果 newLength 大于该 ArrayBuffer 的 maxByteLength,则抛出该错误。
ArrayBuffer.prototype.transfer
transfer() 方法执行与结构化克隆算法相同的操作。它将当前 ArrayBuffer 的字节复制到一个新的 ArrayBuffer 对象中,然后分离当前 ArrayBuffer 对象,保留了当前 ArrayBuffer 的大小可调整性。
const buffer = new ArrayBuffer(8)
const view = new UintArray(buffer)
view[1] = 2
view[8] = 4
// 将缓冲区复制到另一个相同大小的缓冲区
const buffer2 = buffer.transfer()
console.log(buffer.detached) // true
console.log(buffer.byteLength) // 8
const view2 = new UintArray(buffer2)
console.log(view2[1]) // 2
console.log(view2[7]) // 4
// 将缓冲区复制到一个更小的缓冲区
const buffer3 = buffer2.transfer(4);
console.log(buffer3.byteLength); // 4
const view3 = new Uint8Array(buffer3);
console.log(view3[1]); // 2
console.log(view3[7]); // undefined
// 将缓冲区复制到一个更大的缓冲区
const buffer4 = buffer3.transfer(8);
console.log(buffer4.byteLength); // 8
const view4 = new Uint8Array(buffer4);
console.log(view4[1]); // 2
console.log(view4[7]); // 0
// 已经分离,抛出 TypeError
buffer.transfer(); // TypeError: Cannot perform ArrayBuffer.prototype.transfer on a detached ArrayBuffer
String.prototype.isWellFormed()
isWellFormed() 方法返回一个表示该字符串是否包含单独代理项的布尔值。
JavaScript 中的字符串是 UTF-16 编码的。UTF-16 编码中的代理对是指:
在 UTF-16 编码中,代理对(Surrogate Pair)是一种特殊的编码机制,用于表示那些超出基本多文种平面(BMP)的 Unicode 字符。这些字符的 Unicode 码点高于 U+FFFF,因此无法用一个 16 位的 UTF-16 码元来表示。为了解决这个问题,UTF-16 引入了代理对机制。
代理对是由两个 16 位的码元组成的,一个称为高代理(或高代理码元),其码点范围在 U+D800 到 U+DBFF 之间;另一个称为低代理(或低代理码元),其码点范围在 U+DC00 到 U+DFFF 之间。这两个码元合在一起,可以表示一个超出 BMP 的 Unicode 字符。
例如,Unicode 码点 U+10000(这是 BMP 之外的第一个码点)在 UTF-16 中的编码就是高代理码元 U+D800 和低代理码元 U+DC00 的组合,即 “D800 DC00”。同样,码点 U+10001 的 UTF-16 编码就是 “D800 DC01”,以此类推。
通过这种方式,UTF-16 编码能够完全表示所有 Unicode 字符,无论是 BMP 内的还是 BMP 外的。这种代理对机制是 UTF-16 编码方案的一个重要组成部分,使得 UTF-16 成为一种能够灵活处理各种语言字符的编码方式。
isWellFormed() 让你能够测试一个字符串是否是格式正确的(即不包含单独代理项)。由于引擎能够直接访问字符串的内部表示,与自定义实现相比 isWellFormed() 更高效。如果需要将字符串转换为格式正确的字符串,可以使用 toWellFormed() 方法。isWellFormed() 让你可以对格式正确和格式错误的字符串进行不同的处理,比如抛出一个错误或将其标记为无效。
const strings = [
// 单独的前导代理
"ab\uD800",
"ab\uD800c",
// 单独的后尾代理
"\uDFFFab",
"c\uDFFFab",
// 格式正确
"abc",
"ab\uD83D\uDE04c",
];
for (const str of strings) {
console.log(str.isWellFormed());
}
// 输出:
// false
// false
// false
// false
// true
// true
如果传递的字符串格式不正确, encodeURI 会抛出错误。可以通过使用 isWellFormed() 在将字符串传递给 encodeURI() 之前测试字符串来避免这种情况。
const illFormed = "https://example.com/search?q=\uD800";
try {
encodeURI(illFormed);
} catch (e) {
console.log(e); // URIError: URI malformed
}
if (illFormed.isWellFormed()) {
console.log(encodeURI(illFormed));
} else {
console.warn("Ill-formed strings encountered."); // Ill-formed strings encountered.
}
String.prototype.toWellFormed()
toWellFormed() 方法返回一个字符串,其中该字符串的所有单独代理项都被替换为 Unicode 替换字符 U+FFFD。
toWellFormed() 迭代字符串的码元,并将任何单独代理项替换为 Unicode 替换字符 U+FFFD。这确保了返回的字符串格式正确并可用于期望正确格式字符串的函数,比如 encodeURI。由于引擎能够直接访问字符串的内部表示,与自定义实现相比 toWellFormed() 更高效。
const strings = [
// 单独的前导代理
"ab\uD800",
"ab\uD800c",
// 单独的后尾代理
"\uDFFFab",
"c\uDFFFab",
// 格式正确
"abc",
"ab\uD83D\uDE04c",
];
for (const str of strings) {
console.log(str.toWellFormed());
}
// Logs:
// "ab�"
// "ab�c"
// "�ab"
// "c�ab"
// "abc"
// "ab😄c"
如果传递的字符串格式不正确, encodeURI 会抛出错误。可以先通过使用 toWellFormed() 将字符串转换为格式正确的字符串来避免这种情况。
const illFormed = "https://example.com/search?q=\uD800";
try {
encodeURI(illFormed);
} catch (e) {
console.log(e); // URIError: URI malformed
}
console.log(encodeURI(illFormed.toWellFormed())); // "https://example.com/search?q=%EF%BF%BD"
Atomics.waitAsync()
Atomics.waitAsync() 静态方法异步等待共享内存的特定位置并返回一个 Promise。与 Atomics.wait() 不同,waitAsync 是非阻塞的且可用于主线程。
下面来看一个简单的例子,给定一个共享的 Int32Array。
const sab = new SharedArrayBuffer(1024);
const int32 = new Int32Array(sab);
令一个读取线程休眠并在位置 0 处等待,预期该位置的值为 0。result.value 将是一个 promise。
const result = Atomics.waitAsync(int32, 0, 0, 1000);
// { async: true, value: Promise {<pending>} }
在该读取线程或另一个线程中,对内存位置 0 调用以令该 promise 解决为 "ok"。
Atomics.notify(int32, 0);
// { async: true, value: Promise {<fulfilled>: 'ok'} }
如果它没有解决为 "ok",则共享内存该位置的值不符合预期(value 将是 "not-equal" 而不是一个 promise)或已经超时(该 promise 将解决为 "time-out")。
正则表达式 v 标志
RegExp v 标志是 u 标志的超集,并提供了另外两个功能:
1、字符串的 Unicode 属性: 通过 Unicode 属性转义,可以使用字符串的属性。
const re = /^\p{RGI_Emoji}$/v;
// 匹配仅包含 1 个代码点的表情符号:
re.test('⚽'); // '\u26BD'
// → true ✅
// 匹配由多个代码点组成的表情符号:
re.test('👨🏾⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ✅
2、设置符号: 允许在字符类之间进行集合操作。
const re = /[\p{White_Space}&&\p{ASCII}]/v;
re.test('\n'); // → true
re.test('\u2028'); // → false
在 JavaScript 的正则表达式中,u 标志表示 “Unicode” 模式。当你在正则表达式中使用这个标志时,它会将模式视为 Unicode 序列的集合,而不仅仅是一组 ASCII 字符。这意味着正则表达式会正确地处理四个字节的 UTF-16 编码。
具体来说,u 标志有以下几个作用:
正确处理 Unicode 字符:不使用 u 标志时,正则表达式可能无法正确处理 Unicode 字符,尤其是那些超出基本多文种平面(BMP)的字符。使用 u 标志后,你可以匹配和处理任何有效的 Unicode 字符。
改变量词的行为:在 Unicode 模式下,量词(如 *、+、?、{n}、{n,}、{n,m})会匹配任何有效的 Unicode 字符,而不仅仅是 ASCII 字符。
允许使用 \p{...} 和 \P{...}:这两个是 Unicode 属性转义,允许匹配或不匹配具有特定 Unicode 属性的字符。例如,\p{Script=Arabic} 会匹配任何阿拉伯脚本的字符。
正确处理 Unicode 转义:在 Unicode 模式下,你可以使用 \u{...} 来表示一个 Unicode 字符,其中 {...} 是一个四位的十六进制数。
修正了某些正则表达式方法的行为:例如,String.prototype.match()、String.prototype.replace()、String.prototype.search() 和 RegExp.prototype.exec() 等方法在 Unicode 模式下会返回更准确的结果。