0%

ECMAScript 2024(ES15)都更新了什么?

ECMAScript 2024(ES15)都更新了什么?

ECMAScript 2024(ES15)于2024年6月正式发布,带来了多项增强 JavaScript 功能的新特性,提升了开发效率和代码可读性。以下是主要更新内容:

Object.groupBy() 与 Map.groupBy()

它们的共同作用是对数组或其他可迭代对象进行分组,但它们返回的数据结构有所不同:

  • Object.groupBy() 返回对象。
  • Map.groupBy() 返回Map实例。

Object.groupBy()

基本用法

Object.groupBy(iterable, callbackFn)

  • iterable:可迭代对象(例如数组)
  • callbackFn:分组回调函数,用于确定分组依据的键

用法示例

例如,按数字正负分类:

1
2
3
4
const nums = [-1, -5, 0, 3, 4];

const result = Object.groupBy(nums, (num) => Math.sign(num));
console.log(result);

输出:

1
2
3
4
5
{
"-1": [-1, -5],
"0": [0],
"1": [3, 4]
}

注意:

  • 返回的是普通 JavaScript 对象。
  • 键始终是字符串,自动转化为字符串类型。

实用示例场景

示例:按字符串长度分组

1
2
3
4
const words = ['apple', 'banana', 'kiwi', 'pear'];

const grouped = Object.groupBy(words, (word) => word.length);
console.log(grouped);

输出:

1
2
3
4
5
{
"4": ["kiwi", "pear"],
"5": ["apple"],
"6": ["banana"]
}

示例:按对象属性分组

1
2
3
4
5
6
7
8
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "admin" }
];

const groupedByRole = Object.groupBy(users, user => user.role);
console.log(groupedByRole);

输出:

1
2
3
4
5
6
7
8
9
{
"admin": [
{ name: "Alice", role: "admin" },
{ name: "Charlie", role: "admin" }
],
"user": [
{ name: "Bob", role: "user" }
]
}

Map.groupBy()

基本用法

Map.groupBy(iterable, callbackFn)

  • 返回的是Map 实例,而不是普通对象。
  • 键不限制为字符串,可以是任何数据类型,包括对象、数组等。

用法示例

以下使用对象作为键:

1
2
3
4
const items = [1, 2, 3, 4];

const grouped = Map.groupBy(items, (num) => (num % 2 === 0 ? 'even' : 'odd'));
console.log(grouped);

输出:

1
2
3
4
Map {
'odd' => [1, 3],
'even' => [2, 4]
}

使用复杂类型(对象)作为键:

1
2
3
4
5
6
7
8
const people = [
{ name: "Alice", age: 20 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 20 }
];

const groupedByAge = Map.groupBy(people, person => person.age);
console.log(groupedByAge);

输出:

1
2
3
4
Map {
20 => [{ name: "Alice", age: 20 }, { name: "Charlie", age: 20 }],
25 => [{ name: "Bob", age: 25 }]
}

注意这里键的类型是数字。

对比总结

特性 Object.groupBy() Map.groupBy()
返回类型 对象(Object) Map 实例
键类型 仅字符串 任意数据类型
键的易用性 自动转为字符串 支持任意复杂类型
性能特性 快速访问简单键 更灵活但需要get方法访问

适用场景:

Object.groupBy():

  • 数据量较小、键明确为字符串时。
  • 快速读取属性。

Map.groupBy():

  • 键类型复杂、非字符串类型或数据量较大。
  • 对键的顺序有明确要求。

兼容性及支持情况

这是 ES2024(ES15)新增功能,在现代浏览器和 Node.js 的最新版中逐渐得到支持。
对于尚未支持的环境,临时 polyfill 可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (!Object.groupBy) {
Object.groupBy = function(iterable, callback) {
return Array.from(iterable).reduce((acc, item) => {
const key = callback(item);
(acc[key] ||= []).push(item);
return acc;
}, {});
};
}

if (!Map.groupBy) {
Map.groupBy = function(iterable, callback) {
const map = new Map();
for (const item of iterable) {
const key = callback(item);
if (!map.has(key)) map.set(key, []);
map.get(key).push(item);
}
return map;
};
}

总结

Object.groupBy() 和 Map.groupBy() 提供了一种便捷的方式对数据进行分组归类。
根据实际情况选择合适的方法:

  • 若需键为复杂类型、顺序稳定,选择 Map.groupBy()。
  • 若仅需简单键快速访问,选择 Object.groupBy()。

以上功能的引入使 JavaScript 在处理数据时更加高效、直观,简化了日常数据处理的逻辑和代码量。

Promise.withResolvers()

引入背景

在 JavaScript 中,创建一个 Promise 通常是这样:

1
2
3
const promise = new Promise((resolve, reject) => {
// 异步操作,完成后调用 resolve 或 reject
});

这种方式的问题在于:

  • 必须在构造函数的回调函数内定义异步操作和 Promise 控制。
  • 如果异步操作需要外部进行触发或控制(例如:事件监听器),则这种方式不够灵活。

什么是 Promise.withResolvers()?

为了更灵活地控制 Promise,ES2024 提供了新的静态方法 Promise.withResolvers()。

1
const { promise, resolve, reject } = Promise.withResolvers();

这个方法返回一个对象,包含:

  • 一个新的 promise
  • 与该 promise 绑定的 resolve 和 reject 方法,可以从外部调用
  • promise:返回一个新的 Promise 实例。
  • resolve(value):调用此方法,Promise 状态变为成功(fulfilled)。
  • reject(reason):调用此方法,Promise 状态变为失败(rejected)。

基本用法示例

最基本的示例:

1
2
3
4
5
6
7
8
9
10
const { promise, resolve, reject } = Promise.withResolvers();

// 外部控制 promise
setTimeout(() => {
resolve('任务成功');
}, 1000);

promise.then((value) => {
console.log(value); // 一秒后输出:"任务成功"
});

在这个例子中,resolve 可以在外部被调用,非常适合需要在事件或其他条件触发时才解决 Promise 的情况。

异步事件监听器场景:

假设我们想等待某个按钮被点击:

1
2
3
4
5
6
7
8
9
const { promise, resolve } = Promise.withResolvers();

document.querySelector('button').addEventListener('click', () => {
resolve('按钮已点击');
});

promise.then((message) => {
console.log(message); // 按钮点击后输出:"按钮已点击"
});

与传统方式对比

传统方式控制外部 resolve/reject:

1
2
3
4
5
6
let resolvePromise;
const promise = new Promise((resolve, reject) => {
resolvePromise = resolve; // 将resolve赋给外部变量
});

resolvePromise('done');

这样虽然也能做到,但写法不优雅且容易出错。

使用 Promise.withResolvers() 更直观、更安全:

1
2
const { promise, resolve } = Promise.withResolvers();
resolve('done'); // 直接调用resolve

典型应用场景

  • 事件驱动编程: 等待用户交互、DOM 事件等。
  • 条件触发: 外部异步任务(如网络请求、WebSockets)在外部状态改变时触发。
  • 复杂异步流程控制: 外部管理多个异步任务时,更清晰的分离逻辑和控制。

例如,一个简单的异步确认框:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function asyncConfirm(message) {
const { promise, resolve } = Promise.withResolvers();

const confirmed = window.confirm(message);
resolve(confirmed);

return promise;
}

asyncConfirm('确定要删除吗?').then((confirmed) => {
if (confirmed) {
console.log('用户确认删除。');
} else {
console.log('用户取消删除。');
}
});

兼容性及支持情况

ECMAScript 2024(ES15)引入的新特性,现代浏览器和 Node.js 新版逐渐支持。

如果在旧环境使用,可暂时通过 polyfill 实现类似效果:

1
2
3
4
5
6
7
8
Promise.withResolvers = Promise.withResolvers || function () {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};

总结

Promise.withResolvers() 提供了一种更为灵活的创建和控制 Promise 的方式。它帮助开发者将 Promise 的控制权明确分离,从而更清晰、更灵活地实现异步逻辑。这种方式尤其适用于需要外部条件触发或更复杂的异步控制场景。

正则表达式新标志 /v

这个新标志称为集合标志(Set Notation flag),用于增强 Unicode 字符集的匹配能力,并允许更复杂的字符集合操作。

为什么要引入 /v 标志?

传统 JavaScript 正则表达式的字符集合匹配能力比较有限,尤其涉及到 Unicode 字符集的时候:

  • 无法简单地表示字符集之间的交集、差集或嵌套组合。
  • 对 Unicode 的支持局限于简单的字符类,难以描述更复杂的字符集合。

新的 /v 标志提供了集合操作的能力,可以轻松描述复杂的字符集合。

/v 标志的主要特性与语法

使用 /v 标志后,可以在字符类 ([]) 中使用以下运算符:

  • 并集(Union):[A B] 或 [A||B]
  • 交集(Intersection):[A&&B]
  • 差集(Subtraction):[A–B]
  • 字符类嵌套(Nested character classes):可直接在字符类中嵌套另一字符类。

示例:

1
2
3
4
5
6
// 匹配同时属于十六进制和ASCII字母的字符 (即 A-F, a-f)
const regex = /[\p{ASCII}&&\p{Hex_Digit}]/v;

regex.test('F'); // true
regex.test('9'); // true
regex.test('G'); // false

/v 标志与 Unicode 属性转义的结合

/v 标志可以结合 Unicode 属性(如\p{})进行更强大的匹配:

示例:匹配拉丁字母或希腊字母:

1
2
3
4
5
const regex = /[\p{Script=Latin}||\p{Script=Greek}]/v;

regex.test('A'); // true (拉丁字母)
regex.test('α'); // true (希腊字母)
regex.test('你'); // false (汉字)

使用 /v 标志的典型示例

并集(Union)

匹配数字或大写字母:

1
2
3
4
5
const regex = /[\p{Number}||\p{Uppercase_Letter}]/v;

regex.test('A'); // true
regex.test('3'); // true
regex.test('a'); // false

交集(Intersection)

匹配ASCII字符集中同时是字母的字符:

1
2
3
4
5
6
const regex = /[\p{ASCII}&&\p{Letter}]/v;

regex.test('A'); // true
regex.test('g'); // true
regex.test('1'); // false
regex.test('你'); // false

差集(Subtraction)

匹配所有小写字母但排除元音字母:

1
2
3
4
const regex = /[\p{Lowercase_Letter}--[aeiou]]/v;

regex.test('b'); // true
regex.test('a'); // false (元音字母)

字符类嵌套

字符类内再嵌套一个字符类:

1
2
3
4
5
const regex = /[\p{Number}||[\p{Letter}&&\p{Uppercase_Letter}]]/v;

regex.test('3'); // true
regex.test('A'); // true
regex.test('a'); // false

这里的含义是匹配数字或大写字母。

/v 标志与其它正则标志的关系

  • /v 标志与 /u 标志是互斥的,不能同时使用。
  • /v 标志自动支持完整的 Unicode 模式,具备 /u 的能力且更加高级。
1
2
const regex = /[a-z&&[^aeiou]]/v; // 有效
const regex = /[a-z]/uv; // 无效,会抛出语法错误

常见注意事项与限制

  • /v 标志要求字符类使用明确的集合表示法,且不能与传统字符类混用旧式范围(如 [a-z])和集合操作。
  • 建议使用 Unicode 属性表示字符类,以充分发挥 /v 标志的优势。

支持情况与兼容性

各主流 JavaScript 引擎(Chrome、Firefox、Safari、Node.js)逐步实现中,注意版本更新。如需兼容老旧环境,可以继续使用 Babel 或正则表达式库如 XRegExp 处理复杂情况。

使用场景与优势总结

使用 /v 标志的优势:

  • 简化复杂字符集合的定义,增强代码可读性和维护性。
  • 更好地支持国际化(i18n)字符处理场景。
  • 大幅简化以前需要多个正则表达式组合的复杂匹配逻辑。

典型使用场景包括:

  • 多语言输入验证
  • Unicode 字符范围匹配(例如密码验证)
  • 字符串内容过滤与清洗

示例综合案例

验证用户名(仅允许拉丁字母、希腊字母和数字):

1
2
3
4
5
const usernameRegex = /^[\p{Script=Latin}||\p{Script=Greek}||\p{Number}]+$/v;

usernameRegex.test('Alex123'); // true
usernameRegex.test('Αλέξανδρος'); // true (希腊字母)
usernameRegex.test('用户'); // false

总结

正则表达式的新标志 /v 为开发者提供了更加强大的集合操作能力,使 JavaScript 中的字符匹配变得更加灵活与精准。此功能尤其适用于 Unicode 和国际化处理,提供更直观、更简洁的正则表达式写法,极大提升开发效率和表达能力。

ArrayBuffer 与 SharedArrayBuffer 的增强

背景介绍

ArrayBuffer 简介

  • ArrayBuffer 用于表示固定长度的二进制数据缓冲区。
  • 它本身并不能直接操作数据,通常搭配 视图(如TypedArray) 来读写。
1
2
3
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[0] = 255;

SharedArrayBuffer 简介

  • SharedArrayBuffer 允许多个线程(Web Workers)共享同一块内存区域,适用于多线程环境。
  • 通常用于并发编程、数据同步等场景。
1
2
3
// 主线程
const sharedBuffer = new SharedArrayBuffer(1024);
worker.postMessage(sharedBuffer);

Resizable ArrayBuffer (可调整大小)

在 ES2024 中,ArrayBuffer 允许在创建后动态地调整大小(增大或缩小):

创建可调整大小的 ArrayBuffer:

1
const buffer = new ArrayBuffer(8, { maxByteLength: 64 });

参数:

  • 初始大小 (例如:8字节)
  • maxByteLength 表示缓冲区的最大允许大小。

调整大小方法:

1
buffer.resize(newByteLength);
  • 如果新尺寸超过 maxByteLength,将抛出错误。
  • 调整后原来的数据保留(如果新尺寸较大,多出的部分用零填充;如果尺寸缩小,尾部数据丢弃)。

示例:

1
2
3
4
5
6
7
8
const buffer = new ArrayBuffer(8, { maxByteLength: 16 });
console.log(buffer.byteLength); // 8

buffer.resize(12);
console.log(buffer.byteLength); // 12

buffer.resize(6);
console.log(buffer.byteLength); // 6

Transferable ArrayBuffer (可转移)

新的 .transfer() 方法允许你将一个 ArrayBuffer 的内存转移到另一个缓冲区:

基本语法:

1
const newBuffer = buffer.transfer(newByteLength);
  • 调用后,原来的 buffer 会被标记为“detached”,无法再访问。
  • 新的缓冲区可以更大或更小;原有数据根据新尺寸截断或填充。

示例:

1
2
3
4
5
6
7
8
9
10
11
const buffer1 = new ArrayBuffer(8);
const view1 = new Uint8Array(buffer1);
view1.set([1, 2, 3, 4]);

const buffer2 = buffer1.transfer(16);
const view2 = new Uint8Array(buffer2);

console.log(view2); // Uint8Array(16) [1, 2, 3, 4, 0, 0, ...]

// buffer1 此时已经被分离(detached),不可用
console.log(buffer1.byteLength); // 抛出错误

Resizable SharedArrayBuffer(可扩展共享内存)

SharedArrayBuffer 在 ES2024 中也获得了扩展内存大小的能力

创建可扩展的 SharedArrayBuffer:

1
const sharedBuffer = new SharedArrayBuffer(1024, { maxByteLength: 4096 });

可共享的缓冲区初始大小1024字节,最大可扩展到4096字节。

扩展缓冲区大小:

1
sharedBuffer.grow(newByteLength);
  • 注意:SharedArrayBuffer 只能扩大,不能缩小。
  • 扩展后的新空间由零填充。

示例:

1
2
3
4
5
6
const sharedBuffer = new SharedArrayBuffer(1024, { maxByteLength: 2048 });

sharedBuffer.grow(1536);
console.log(sharedBuffer.byteLength); // 1536

sharedBuffer.grow(512); // 错误,不能缩小

与视图(TypedArray)配合使用时的注意事项

TypedArray 在底层缓冲区发生变化时有特殊表现:

  • Resizable ArrayBuffer 调整大小后,基于该缓冲区的视图(如Uint8Array)可能会因超出范围而被自动调整大小。
  • Transferable ArrayBuffer 在转移后,原来的视图会失效(无法再访问)。
  • SharedArrayBuffer 在扩展后,视图仍保持原始长度,若需要访问新增部分,需重新创建视图。

示例:

1
2
3
4
5
6
const buffer = new ArrayBuffer(8, { maxByteLength: 16 });
const view = new Uint8Array(buffer);
console.log(view.length); // 8

buffer.resize(12);
console.log(view.length); // 自动调整为12

适用场景与优势总结

这些增强功能尤其适用于:

  • 动态内存管理(例如:音视频处理、大数据传输)
  • 多线程、高性能计算(Web Workers、WebAssembly)
  • 性能敏感且内存需求动态变化的场景(游戏开发、图形处理)

优势:

  • 提高内存管理的灵活性。
  • 避免内存浪费,提升性能。
  • 降低开发复杂性(无需频繁创建新的缓冲区)。

兼容性说明

  • ES2024(ES15)引入的新功能,正在逐步被现代浏览器和Node.js环境支持。
  • 尚未支持的环境需等待更新或使用Polyfill / Babel进行兼容处理。

总结

ES2024 中的 ArrayBuffer 与 SharedArrayBuffer 增强为 JavaScript 带来了更高效、更灵活、更安全的底层内存控制能力:

功能 ArrayBuffer SharedArrayBuffer
可调整大小(Resizable) 可增大或者减小 仅可以增大
可转移(Transferable) 可转移(transfer) 不可转移
用途 单线程灵活内存管理 多线程共享内存

开发者通过使用这些功能,可以更精准地管理内存、优化性能,特别是在更为复杂和性能敏感的应用场景中。

字符串格式验证方法

什么是“格式正确”(Well-Formed)的字符串?

在 Unicode 中,格式正确(Well-Formed) 的字符串意味着字符串没有包含:

  • 孤立的代理项(lone surrogate)
  • Unicode 代理对(surrogate pair)由 高代理项(High surrogate, \uD800 - \uDBFF)和低代理项(Low surrogate, \uDC00 - \uDFFF) 组成,用于表示超出BMP(基本多语言平面)的字符。
  • 孤立代理项是指只有高代理项或只有低代理项而没有匹配的配对项。

例如:

1
const invalid = "\uD800"; // 孤立的高代理项,不合规

这样的字符串是无效的,可能在后续处理中导致错误或异常。

String.prototype.isWellFormed()

作用:

检查字符串是否为格式正确的Unicode字符串。
返回一个布尔值(true/false):

  • true:格式正确,无孤立代理项。
  • false:包含孤立代理项或其他无效字符。

基本用法示例:

1
2
3
4
5
const validStr = "Hello";
const invalidStr = "Hello\uD800World";

console.log(validStr.isWellFormed()); // true
console.log(invalidStr.isWellFormed()); // false

第一个字符串无问题,因此返回 true。
第二个字符串包含孤立代理项,因此返回 false。

String.prototype.toWellFormed()

作用:

  • 将无效的(ill-formed)字符串转换为格式正确的字符串。
  • 替换孤立代理项为 Unicode 替代字符(U+FFFD,�)。

基本用法示例:

1
2
3
4
5
const validStr = "Hello";
const invalidStr = "Hello\uD800World";

console.log(validStr.isWellFormed()); // true
console.log(invalidStr.isWellFormed()); // false

第一个字符串无问题,因此返回 true。
第二个字符串包含孤立代理项,因此返回 false。

1
2
3
4
5
const invalidStr = "Hello\uD800World";

const correctedStr = invalidStr.toWellFormed();
console.log(correctedStr);
// 输出: "Hello�World"

\uD800 是孤立代理项,因此被替换成

典型使用场景

安全的网络通信

确保发送给服务器或API的数据总是格式正确,避免服务器端处理异常:

1
2
3
4
5
6
7
function safeSend(data) {
const safeData = data.toWellFormed();
fetch('/api/send', {
method: 'POST',
body: JSON.stringify({ data: safeData })
});
}

用户输入校验

检查用户输入文本,避免无效的字符导致异常:

1
2
3
4
5
6
const userInput = inputElement.value;

if (!userInput.isWellFormed()) {
alert("输入中包含无效字符,已自动替换!");
inputElement.value = userInput.toWellFormed();
}

日志记录与数据存储

保证日志或数据记录中的字符串是安全且格式化正确的:

1
2
3
4
function logMessage(msg) {
const safeMsg = msg.toWellFormed();
console.log(`[Log]: ${safeMsg}`);
}

常见问题和注意事项

  • 这两个方法不改变原字符串,而是返回新的字符串或布尔值。
  • 无法修复语义上的问题,仅在字符编码层面修正孤立代理项等无效格式。

兼容性和Polyfill方案

目前ES2024标准新推出,现代浏览器和Node.js版本逐步支持中。

临时Polyfill示例:

1
2
3
4
5
6
7
8
9
10
11
if (!String.prototype.isWellFormed) {
String.prototype.isWellFormed = function() {
return !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(this);
};
}

if (!String.prototype.toWellFormed) {
String.prototype.toWellFormed = function() {
return this.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
};
}

这个Polyfill用正则表达式临时实现类似功能,但可能性能不如原生实现。

方法对比总结

方法 功能 返回值
isWellFormed() 检查字符串格式 布尔值(true 或 false)
toWellFormed() 修正字符串格式 新的安全字符串

示例

1
2
3
4
5
6
7
8
9
10
11
const data = ["Hello", "World\uD800", "😊", "\uD800Test"];

data.forEach(item => {
if (!item.isWellFormed()) {
console.warn(`检测到无效字符串: ${item}`);
const fixed = item.toWellFormed();
console.log(`修复后的字符串: ${fixed}`);
} else {
console.log(`有效字符串: ${item}`);
}
});
1
2
3
4
5
6
有效字符串: Hello
检测到无效字符串: World�
修复后的字符串: World�
有效字符串: 😊
检测到无效字符串: �Test
修复后的字符串: �Test

总结

  • 提高了JavaScript处理Unicode字符串的安全性和可靠性。
  • 简单易用,有效避免潜在的字符串格式问题。
  • 推荐广泛应用于网络通信、用户输入校验、数据处理等场景,保障系统的健壮性与数据安全性。

Atomics.waitAsync()

背景:什么是 Atomics.wait()?

在引入 Atomics.waitAsync() 前,JavaScript 已有 Atomics.wait() 方法:

  • Atomics.wait() 是同步阻塞调用。
  • 仅能在Worker线程中使用,主线程无法使用,否则会抛出异常。
  • 等待指定的共享内存位置的值变化。

示例:

1
2
3
4
5
// worker.js 中
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

Atomics.wait(sharedArray, 0, 0); // 阻塞等待直到索引0的值不是0

缺点:

  • 主线程无法使用,导致场景受限。
  • 同步阻塞可能影响性能和响应性。

引入原因:为什么需要 Atomics.waitAsync()?

为了解决上述问题,ES2024 推出了 Atomics.waitAsync():

  • 异步非阻塞,返回一个 Promise。
  • 可在主线程和 Worker线程中使用。
  • 提升多线程通信性能,避免阻塞主线程UI。

基本语法与用法

语法:

1
Atomics.waitAsync(typedArray, index, value[, timeout]);
  • typedArray:共享内存的 Int32Array 或 BigInt64Array。
  • index:要检查的元素索引。
  • value:期望等待的值。
  • timeout (可选):等待超时时间(毫秒),默认无限等待。

返回值:

  • 返回一个对象,其中的 .value 是一个 Promise:
1
2
3
4
{
async: true, // 总是true
value: Promise<{value: "ok" | "not-equal" | "timed-out"}>
}

用法示例详解

基本异步等待(主线程中可用)

1
2
3
4
5
6
7
8
9
10
11
12
13
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

// 异步等待 sharedArray[0] 的值变为非0
Atomics.waitAsync(sharedArray, 0, 0).value.then(result => {
console.log(result); // { value: "ok" }
});

// 假设worker线程会在1秒后更改 sharedArray[0] 的值
setTimeout(() => {
Atomics.store(sharedArray, 0, 1); // 修改共享内存的值
Atomics.notify(sharedArray, 0); // 唤醒等待者
}, 1000);

1秒后打印 { value: “ok” },表明成功等待到值变化。

带有超时的等待

1
2
3
4
5
6
7
Atomics.waitAsync(sharedArray, 0, 0, 5000).value.then(result => {
if (result.value === "timed-out") {
console.log('等待超时');
} else {
console.log('值发生变化:', result.value);
}
});

若5秒内未发生变化,则打印 ‘等待超时’。

返回值状态说明

Atomics.waitAsync() 返回的 Promise 解决时包含以下三种可能状态:

  • ok:指定位置的值被更改,并触发了Atomics.notify()。
  • not-equal:调用时,位置的值已经不等于期待值,无需等待。
  • timed-out:等待超时,位置的值未发生变化。

示例(立刻返回的情况):

1
2
3
4
5
6
7
const sharedArray = new Int32Array(new SharedArrayBuffer(4));
sharedArray[0] = 1;

// 当前值不等于预期值0,因此立即返回"not-equal"
Atomics.waitAsync(sharedArray, 0, 0).value.then(result => {
console.log(result); // { value: "not-equal" }
});

Atomics.notify() 与 Atomics.waitAsync() 的配合使用

通常,等待线程(主线程或Worker线程)使用waitAsync()等待,另一线程用notify()通知:

1
2
3
4
5
6
7
8
9
10
// 等待线程 (主线程或Worker)
Atomics.waitAsync(sharedArray, 0, 0).value.then(result => {
if (result.value === 'ok') {
console.log('收到通知,值已变化');
}
});

// 通知线程 (通常是Worker)
Atomics.store(sharedArray, 0, 1);
Atomics.notify(sharedArray, 0);

与 Atomics.wait() 的差异

特性 Atomics.wait() Atomics.waitAsync()
阻塞方式 同步阻塞 异步非阻塞
主线程可用性 不支持 支持
返回值 直接返回状态字符串 返回Promise对象
场景适用性 Worker线程(计算密集) 主线程&Worker线程

适用场景与优势总结

场景:

  • 主线程异步等待共享状态:避免UI线程阻塞,提高应用响应性。
  • WebAssembly 与JavaScript 交互:异步等待WASM计算完成。
  • 游戏开发、多线程渲染、音视频处理等需要高性能且非阻塞的场景。

优势:

  • 避免线程阻塞,提高程序效率。
  • 更友好的多线程通信模型。
  • 主线程中实现高效等待,避免频繁轮询(polling)。

兼容性说明与Polyfill

  • 现代浏览器逐步支持(Chrome、Firefox、Safari逐渐适配)。
  • 尚未广泛支持时,可退化到传统的Atomics.wait()(仅Worker可用)或基于消息的通信机制。

完整代码示例(主线程异步等待Worker线程计算结果)

主线程:

1
2
3
4
5
6
7
8
9
10
11
12
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

const worker = new Worker('worker.js');
worker.postMessage(sharedBuffer);

// 主线程异步等待worker线程结果
Atomics.waitAsync(sharedArray, 0, 0).value.then(result => {
if (result.value === 'ok') {
console.log('Worker计算完成,结果:', sharedArray[0]);
}
});

Worker线程 (worker.js):

1
2
3
4
5
6
7
8
9
onmessage = (event) => {
const sharedArray = new Int32Array(event.data);

// 模拟计算
setTimeout(() => {
sharedArray[0] = 42; // 写入结果
Atomics.notify(sharedArray, 0); // 通知主线程
}, 1000);
};

小结

Atomics.waitAsync() 是对JavaScript多线程环境的重要增强,极大提高主线程与Worker之间通信的灵活性和性能。开发者可用它实现高效、非阻塞的异步等待,特别适用于需要频繁通信或状态同步的高性能应用场景。

管道操作符 |>

管道操作符是一种语法糖,用于更清晰、更简洁地实现函数的链式调用,尤其适合连续多个函数处理同一个数据的场景。

为什么引入管道操作符?

管道操作符通过一种更优雅的方式解决以下问题:

1
const result = value |> first |> second |> third;

在没有管道操作符之前,函数链式调用通常是以下形式之一:

嵌套调用(Nested functions)

1
const result = third(second(first(value)));

缺点:嵌套层次深时可读性差。

临时变量

1
2
3
const result1 = first(value);
const result2 = second(result1);
const result3 = third(result2);

缺点:引入额外临时变量。

基本语法与规则

管道操作符的基本语法为:

1
expression |> function

管道操作符会将左侧表达式的结果作为右侧函数的第一个参数。左侧表达式的结果自动成为函数调用的输入。

1
2
3
4
5
6
const double = x => x * 2;
const increment = x => x + 1;

const result = 3 |> double |> increment;
// 等价于 increment(double(3))
console.log(result); // 输出 7

与传统调用方式的对比示例

传统方式:

1
2
3
4
5
6
7
8
9
10
function trim(str) {
return str.trim();
}

function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

const result = capitalize(trim(" hello "));
console.log(result); // "Hello"

使用管道操作符:

1
2
3
4
5
const result = "   hello   "
|> trim
|> capitalize;

console.log(result); // "Hello"

优势:

  • 更直观、更容易阅读。
  • 减少代码嵌套。

匿名函数与箭头函数的用法

管道操作符也支持使用匿名函数或箭头函数:

1
2
3
4
5
const result = [1, 2, 3, 4, 5]
|> (arr => arr.filter(x => x % 2 === 0))
|> (evens => evens.map(x => x * 10));

console.log(result); // [20, 40]

使用场景示例

管道操作符适用于:

  • 数据变换(data transformation)
  • 函数式编程(functional programming)
  • 链式方法调用

场景1:数据处理链

1
2
3
4
5
6
7
const fetchData = () => [3, 1, 4, 1, 5, 9];

const sortedUniqueData = fetchData()
|> (data => [...new Set(data)]) // 去重
|> (uniqueData => uniqueData.sort()); // 排序

console.log(sortedUniqueData); // [1, 3, 4, 5, 9]

场景2:字符串处理

1
2
3
4
5
6
7
8
9
10
11
const normalizeText = text => text.toLowerCase();
const removePunctuation = text => text.replace(/[.,!?]/g, '');
const splitWords = text => text.split(' ');

const words = "Hello, World! Welcome to JavaScript."
|> normalizeText
|> removePunctuation
|> splitWords;

console.log(words);
// ["hello", "world", "welcome", "to", "javascript"]

管道操作符的注意事项

函数调用方式:

管道右侧必须是单一参数函数或明确接受左侧值为首参数的函数:

正确:

1
const result = value |> someFunction;

错误(直接调用函数):

1
2
// 错误写法(因为此处someFunction()立即调用,没有传入左侧值)
const result = value |> someFunction();

正确方式是:

1
const result = value |> (v => someFunction(v));

暂不支持的调用:

当前标准(ES2024)暂时不支持部分应用(partial application)语法,如:

1
2
// 暂不支持的语法
const result = value |> someFunction(?, extraArg);

如需额外参数,可以通过箭头函数实现:

1
const result = value |> (v => someFunction(v, extraArg));

兼容性与polyfill方案

目前为 ES2024 新特性,浏览器和 Node.js 最新版本逐步支持。
Babel 提供插件用于支持旧版本环境:Babel插件 babel-plugin-proposal-pipeline-operator

安装与配置示例:

1
npm install @babel/plugin-proposal-pipeline-operator --save-dev

在 .babelrc 中:

1
2
3
4
5
{
"plugins": [
["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }]
]
}

优势总结

  • 提高代码可读性与维护性。
  • 消除多层嵌套调用,降低代码复杂度。
  • 使函数链调用更直观。

综合示例:数据处理链

1
2
3
4
5
6
7
8
9
10
const data = [5, 2, 8, 3, 5, 2];

const processData = arr => arr
|> (data => [...new Set(data)]) // 去重
|> (uniqueData => uniqueData.sort()) // 排序
|> (sorted => sorted.map(x => x * x)); // 求平方

const result = processData(data);

console.log(result); // [4, 9, 25, 64]

上述代码直观且易于理解:首先去重 → 排序 → 再求平方。

总结

管道操作符(|>)是 ES2024 中最重要的语法增强之一:

  • 简洁清晰:提升代码可读性。
  • 灵活高效:适合处理复杂的数据流与函数链调用。

在未来JavaScript开发中,它可能成为函数式编程风格的重要组成部分,使得代码编写和维护更加高效且易懂。

不可变的数据结构:记录(Record)与元组(Tuple)

引入背景:什么是不变性(Immutability)?

不可变数据结构:

  • 一旦创建,其内部数据便不能再被修改。
  • 修改操作只会生成新的数据结构,不会更改旧的结构。

不可变结构的优势:

  • 数据安全性:避免意外的副作用,简化调试。
  • 性能优化:数据引用可安全共享,减少内存拷贝。
  • 状态管理更简单:在 React 和其他框架中,不可变数据结构广泛用于状态管理(如 Redux)。

记录(Record)简介与用法

基本定义:

  • 记录(Record)类似于普通 JavaScript 对象,但具有不可变性。
  • 使用特殊语法 #{} 定义:
1
const record = #{ x: 10, y: 20 };

特性说明

不可修改属性:

1
record.x = 30; // 错误,无法修改

属性值必须是:

  • 原始值(number, string, boolean, null, undefined)
  • 另一个 Record 或 Tuple
1
const nestedRecord = #{ a: 1, b: #{ nested: true } };

引用相等性(Reference Equality):
两个记录值相同时,共享同一个引用(类似于字符串的 interning):

1
#{a: 1} === #{a: 1}  // true

用法示例:

1
2
3
4
5
6
7
const user = #{ name: "Alice", age: 30 };

// 创建新的记录(扩展或修改)
const updatedUser = #{ ...user, age: 31 };

console.log(user.age); // 30
console.log(updatedUser.age); // 31

元组(Tuple)简介与用法

基本定义:

  • 元组(Tuple)类似于数组,但同样具有不可变性。
  • 使用特殊语法 #[] 定义:
1
const tuple = #[1, 2, 3];

特性说明:

不可修改元素:
1
tuple[0] = 10; // 错误,不允许修改
元素必须是:
  • 原始值
  • 记录(Record)
  • 另一个元组(Tuple)
1
const complexTuple = #[1, #[2, 3], #{ a: 4 }];
引用相等性:

两个元组内容相同时,共享引用:

1
#[1,2] === #[1,2]; // true

用法示例:

1
2
3
4
5
6
7
const point = #[10, 20];

// 创建新元组
const movedPoint = #[...point, 30];

console.log(point); // #[10, 20]
console.log(movedPoint); // #[10, 20, 30]

记录与元组的组合用法示例

记录和元组可以互相嵌套:

1
2
3
4
5
6
7
const data = #{
user: #{ name: "Bob", tags: #["admin", "editor"] },
scores: #[10, 20, 30]
};

console.log(data.user.name); // "Bob"
console.log(data.scores[1]); // 20

不可变数据结构与普通结构的比较

特性 普通对象与数组 Record & Tuple(不可变)
可变性
引用相等性(内容相同时) 是(相同内容共享引用)
支持嵌套复杂性
线程安全 是(因不可变)
内存使用效率 一般 高(引用共享减少内存使用)

记录和元组的适用场景

React状态管理

1
2
3
4
5
const initialState = #{
user: #{ id: 1, name: "Alice" },
loggedIn: true,
roles: #["user", "admin"]
};

优势:

  • 状态不被意外改变。
  • 简化 React 中的 PureComponent / React.memo 判断。

高效缓存数据

1
2
3
4
5
6
7
8
const cache = new Map();

const key = #{ x: 10, y: 20 };
cache.set(key, "cached value");

// 相同内容的记录,引用相同,可以直接命中
const lookupKey = #{ x: 10, y: 20 };
cache.get(lookupKey); // "cached value"

优势:

  • 提高缓存命中率,减少内存开销。

综合示例

1
2
3
4
5
6
7
8
9
10
11
12
const todos = #[
#{ id: 1, text: "Buy milk", done: false },
#{ id: 2, text: "Clean room", done: true }
];

// 标记任务完成,生成新的元组
const updatedTodos = todos.map(todo =>
todo.id === 1 ? #{...todo, done: true} : todo
);

console.log(todos[0].done); // false (原数据不变)
console.log(updatedTodos[0].done); // true (新数据更新)

通过记录和元组,JavaScript 在原生层面提供了高效的不可变数据支持,极大地提升了代码的安全性和性能,并为前端开发中复杂数据管理场景提供了极佳的解决方案。

注意事项与限制

  • 记录和元组不允许存储函数或具有状态的对象,只能存储不可变的值。
  • 操作记录或元组时,总是产生新的结构,原有结构保持不变。

兼容性与 Polyfill

ES2024 新引入,目前仅最新的浏览器和Node.js版本支持。对旧环境,可考虑第三方库 immutable.js 来模拟类似行为(语法不同):

1
2
3
4
const { Map, List } = require('immutable');

const record = Map({ a: 1 });
const tuple = List([1, 2, 3]);

总结

  • 安全性更高:不可变性保障数据安全。
  • 高效性更强:引用共享提高性能和减少内存占用。
  • 状态管理更容易:特别适合React、Redux等状态管理框架。