🎉 博客主页:【剑九_六千里-CSDN博客】【剑九_六千里-掘金社区】
🎨 上一篇文章:【JavaScript如何判断一个对象是否属于某个类?】
🎠 系列专栏:【面试题-八股系列】
💖 感谢大家点赞👍收藏⭐评论✍
文章目录
- 1. ES6 (ECMAScript 2015): 现代JavaScript的起点
- 1.10. 默认参数和剩余参数
- 2. ES7 (ECMAScript 2016): 数学运算的提升
- 3. ES8 (ECMAScript 2017): 异步编程的革命
- 4. ES9 (ECMAScript 2018): 异步迭代的引入
- 5. ES10 (ECMAScript 2019): 数组和字符串的增强
- 6. ES11 (ECMAScript 2020): 高级数据操作
- 7. ES12 (ECMAScript 2021)
- 8. ES13 (ECMAScript 2022)
- 9. ES14 (ECMAScript 2023)
- 10. ES15 (ECMAScript 2024)
- 11. 结语
引言:
自从ES6(ECMAScript 2015)
以来,JavaScript
作为一门语言经历了前所未有的变革,每年的新版本都带来了令人振奋的新特性和优化,极大地提升了开发者的生产力和代码的可维护性。本文将深入探讨从ES6
到ES15(ECMAScript 2024)
期间JavaScript
的演变历程,旨在为开发者提供一份全面的指南,涵盖语言的关键更新和实用示例。
1. ES6 (ECMAScript 2015): 现代JavaScript的起点
1.1. let 和 const
- let 用于声明块级作用域的变量,解决了变量提升的问题。
- const 用于声明不可重新赋值的常量,提高了代码的可预测性。
let count = 0;
const PI = 3.14;
1.2. 解构赋值
对象解构和数组解构提供了从复杂数据结构中快速提取数据的便捷方式。
const {firstName, lastName} = {firstName: 'John', lastName: 'Doe'};
console.log(firstName, lastName); // John Doe
const [first, second] = [1, 2];
console.log(first, second); // 1 2
测试:
1.3. 模板字符串
允许在字符串中嵌入表达式,极大地提高了字符串拼接的可读性和效率。
const name = 'Alice';
console.log(`Hello, ${name}!`); // Hello, Alice!
测试:
1.4. 箭头函数
提供了一种更简洁的函数定义方式,同时自动绑定this
,避免了闭包中的this
问题。
const add = (a, b) => a + b;
add(1, 2); // 3
测试:
1.5. 类
虽然JavaScript
本质上是基于原型的,但类语法提供了面向对象编程的语法糖,使代码更易于理解和维护。
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}`);
}
}
const person = new Person('John');
person.greet()
测试:
1.6. 模块系统
引入了import
和export
关键字,实现了真正的模块化编程,提高了代码的组织性和复用性。
// myModule.js
export const PI = 3.14;
// main.js
import {PI} from './myModule.js';
1.7. Promises
为异步编程提供了一个更优雅的解决方案,替代了回调地狱,使异步代码更易于理解和调试。
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Done!'), 1000);
});
console.log(promise)
测试:
1.8. Symbols
用于创建唯一属性键,避免了命名冲突,特别适用于私有属性的实现。
const uniqueId = Symbol('id');
console.log(uniqueId);
测试:
1.9. Array.from()
- 从类数组对象或可迭代对象创建新的数组实例。
const arrayLike = {length: 3, 0: 'a', 1: 'b', 2: 'c'};
console.log(arrayLike)
const arr = Array.from(arrayLike);
console.log(arr)
测试:
1.10. Map 和 Set
Map
是一种容器,用于存储键值对。与普通的JavaScript
对象不同,Map
的键可以是任意类型的值,包括函数、对象、基本类型等。Map
的一些主要特性包括:
- 键值对的顺序是基于插入顺序的。
- 可以通过键来快速检索值。
- 键和值都可以是任意类型的值。
- 提供了
get, set, delete, has, clear
等方法来操作数据。 Map
的大小可以通过size
属性获取。
const myMap = new Map();
myMap.set('key1', 'value1');
myMap.set(123, 'another value');
console.log(myMap.get('key1')); // 输出: value1
console.log(myMap.size); // 输出: 2
测试:
Set
是一种集合,用于存储唯一的元素列表。Set
自动确保没有重复的值。其主要特性包括:
- 元素的顺序也是基于插入顺序的。
- 可以检查元素是否存在,添加新元素,删除元素等。
Set
的元素可以是任何类型的值,但每个值只能出现一次。Set
的大小同样可以通过size
属性获取。
const mySet = new Set();
mySet.add('a');
mySet.add('b');
mySet.add('a'); // 这不会添加,因为 'a' 已经存在
console.log(mySet.has('a')); // 输出: true
console.log(mySet.size); // 输出: 2
测试:
1.11. Array.of()
- 从零个或多个参数创建新的数组实例。
const arr = Array.of(1, 2, 3);
console.log(arr)
测试:
1.12. 生成器函数 function* () {}
- 可以使用yield关键字暂停和恢复函数的执行。当生成器函数被调用时,它不会立即执行,而是返回一个迭代器对象。当迭代器的
next()
方法被调用时,函数会从上一次暂停的地方继续执行,直到遇到下一个yield
表达式或函数结束。 - 可以返回一个序列。生成器函数可以产生一系列的值,而不是像普通函数那样只能返回一个值。
- 可以接收外部传入的值。通过
next()
方法的参数,可以向生成器函数内部传递值。
function* numberGenerator() {
yield 1; // 第一次调用next()时返回
yield 2; // 第二次调用next()时返回
return 'done'; // 可以返回一个最终值,但通常不使用
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 'done', done: true }
console.log(gen.next()); // { value: undefined, done: true }
测试:
在这个例子中,numberGenerator
是一个生成器函数,它会产生两个数字,然后结束。每次调用gen.next()
都会执行生成器函数直到遇到下一个yield
表达式或者函数结束,并返回一个包含value
和done
属性的对象。value
属性包含了yield表达式产生的值,而done
属性表示生成器是否已经完成了所有的迭代。
1.10. 默认参数和剩余参数
- 函数参数变得更加灵活,支持默认值和收集不定数量的参数。
// 参数默认值
function sum(a = 10, ...nums) {
return a + nums.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum()); // 10
// 剩余参数
function sum(...nums) {
return nums.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
测试:
1.11. String.prototype.repeat()
- 重复字符串多次。
const str = 'abc'.repeat(3);
console.log(str); // 'abcabcabc'
测试:
2. ES7 (ECMAScript 2016): 数学运算的提升
2.1. 指数运算符 (**)
- 简化了幂运算的语法。
const result = 2 ** 3;
console.log(result); // 8
测试:
3. ES8 (ECMAScript 2017): 异步编程的革命
3.1. 异步函数 (async/await)
- 使异步代码看起来更像同步代码,极大地简化了异步流程的控制。
async function fetchData() {
const response = await fetch('/data');
return await response.json();
}
3.2. 扩展运算符 (…)
- 用于复制数组和对象中的元素,也用于函数调用时收集参数。
const arr = [...[1, 2], ...[3, 4]];
console.log(arr)
测试:
3.3. Array.prototype.includes()
- 检查数组是否包含特定元素,返回布尔值。
const arr = [1, 2, 3];
console.log(arr.includes(2)); // true
测试:
3.4. String.prototype.padStart() 和 String.prototype.padEnd()
- 在字符串的开始或结束填充字符。
const str1 = '123'.padStart(7, '0');
console.log(str1); // '0000123'
const str2 = '123'.padEnd(7, '0');
console.log(str2); // '1230000'
测试:
4. ES9 (ECMAScript 2018): 异步迭代的引入
4.1. 异步迭代 (async iterators)
- 支持异步遍历数据流,为处理大量数据提供了新的途径。
const asyncIterable = {
[Symbol.asyncIterator]() {
return {
next: () => Promise.resolve({done: false, value: 1})
};
}
};
console.log(asyncIterable)
测试:
4.2. String.prototype.trimLeft() 和 String.prototype.trimRight()
- 左右两侧去除空白字符。
const str1 = ' hello world ';
console.log(str1.trimLeft()); // 'hello world '
const str2 = ' hello world ';
console.log(str2.trimRight()); // ' hello world'
测试:
5. ES10 (ECMAScript 2019): 数组和字符串的增强
5.1. 扁平化数组 (flat and flatMap)
- 提供了将多维数组扁平化到指定深度的能力。
- 它可以接受一个可选的参数
depth
,表示要展开的数组的深度。如果不提供depth
参数,或者将其设置为 1,那么flat
将只会展开第一层的子数组。
const arr = [1, [2, [3, [4]]]];
const flatArr = arr.flat(2);
console.log(flatArr); // [1, 2, 3, [4]]
const deepArr = [1, [2, [3, [4, [5]]]]];
const deepFlatArr = deepArr.flat(4);
console.log(deepFlatArr); // [1, 2, 3, 4, 5]
测试:
5.2. Array.prototype.flatMap()
Array.prototype.flatMap()
结合了map()
和flat()
的功能。flatMap()
首先对数组中的每个元素应用一个映射函数,然后将结果展平一层。这使得处理和转换嵌套数组变得更加简单和高效。
语法:array.flatMap(callback(element[, index[, array]])[, thisArg])
flatMap()
接受一个回调函数作为参数,这个函数会被应用于数组的每个元素,并且可以返回一个新的数组。flatMap()
会自动将这些新数组展平到一个数组中,而不是返回一个嵌套数组。callback
: 必需。一个函数,其参数与map()
方法相同。element
: 当前被处理的数组元素。index
: 可选。当前元素的索引。array
: 可选。调用flatMap
的数组本身。thisArg
: 可选。指定执行回调函数时this
的值。
const numbers = [[1, 2], [3, 4], [5, 6]];
const flattened = numbers.flatMap(x => x);
console.log(flattened); // 输出: [1, 2, 3, 4, 5, 6]
测试:
5.3. String.prototype.trimStart 和 trimEnd
- 提供了更细粒度的字符串修剪功能。
const str1 = ' Hello World ';
console.log(str1.trimStart());
const str2 = ' Hello World ';
console.log(str2.trimEnd());
测试:
5.4. String.prototype.matchAll
- 返回字符串中所有匹配正则表达式的迭代器,便于处理复杂的文本模式。
const str = 'hello world';
const regex = /l/g;
const matches = str.matchAll(regex);
for (const match of matches) {
console.log(match);
}
测试:
6. ES11 (ECMAScript 2020): 高级数据操作
6.1. 可选链操作符 (?.)
- 安全地访问深层嵌套的属性,避免了运行时错误。
const obj = {a:{b: null}};
console.log(obj.a?.b?.c); // undefined
测试:
正常访问obj.a.b.c
会报错,因为c
属性不存在:
使用可选链操作符,可正常使用:
6.2. 空值合并运算符 (??)
- 当值为
null
或undefined
时提供默认值,简化了条件判断。
let a = null;
let b = "Hello";
console.log(a ?? b); // 输出: "Hello"
console.log(0 ?? b); // 输出: 0, 因为 0 不是 null 或 undefined
测试:
6.3. .at() 方法
在 JavaScript
中,我们通常使用方括号[]来访问数组的第 i 个元素。这个过程非常简单,但实际上我们只是访问了索引为 i-1
的数组属性而已。
const arr = ['a', 'b', 'c', 'd'];
console.log(arr[1]); // b
然而,当我们希望通过方括号来访问数组末尾的第 N
个元素时,我们需要使用索引 arr.length - N
。
const arr = ['a', 'b', 'c', 'd'];
// 从末尾开始第一个元素
console.log(arr[arr.length - 1]); // d
// 倒数第二个元素 console.log
console.log(arr[arr.length - 2]); // c
借助全新的at()
方法,可以以更加精简和富有表现力的方式来实现这一目标。要访问数组末尾的第N
个元素,只需将负值-N
作为参数传递给at()
方法即可。
const arr = ['a', 'b', 'c', 'd'];
// 从末尾开始第一个元素
console.log(arr.at(-1)); // d
// 倒数第二个元素 console.log
console.log(arr.at(-2)); // c
测试:
除了数组之外,字符串和TypedArray
对象现在也有at()
方法。
const str = 'Coding Beauty';
console.log(str.at(-1)); // y
console.log(str.at(-2)); // t
const typedArray = new Uint8Array([16, 32, 48, 64]);
console.log(typedArray.at(-1)); // 64
console.log(typedArray.at(-2)); // 48
测试:
6.4. BigInt 类型
- 支持任意精度的大整数,满足了对大数字运算的需求。
- 常规的
Number
类型在JavaScript
中有其大小限制(最大安全整数是 2^53 - 1),而BigInt
可以表示超出这个范围的整数。
6.4.1. 创建 BigInt:
通过后缀 n 创建:
const bigIntValue = 1234567890123456789012345678901234567890n;
console.log(bigIntValue);
console.log(typeof bigIntValue);
测试:
使用 BigInt()
函数转换:
const num = 1234567890123456789012345678901234567890;
const bigIntValue = BigInt(num);
console.log(bigIntValue);
console.log(typeof bigIntValue);
测试:
6.4.2. BigInt 的基本操作
加法、减法、乘法和除法都可以在 BigInt
类型上进行:
const a = 1234567890123456789012345678901234567890n;
const b = 9876543210987654321098765432109876543210n;
const sum = a + b; // 加法
const difference = a - b; // 减法
const product = a * b; // 乘法
const quotient = a / b; // 除法,注意除法结果仍然是一个 BigInt,但可能需要使用 `n` 后缀来表示结果,或者使用 `Math.floor` 或 `Math.round` 来获取整数部分
console.log(sum);
console.log(difference);
console.log(product);
console.log(quotient);
测试:
6.4.3. 注意事项
BigInt
不能与Number
直接进行数学运算,除非使用Number()
显式转换,但这会丢失BigInt
的精度优势。BigInt
的除法运算结果默认是BigInt
类型,如果需要得到精确的小数部分,通常需要转换成字符串或其他数据类型进行处理。
6.5. Promise.allSettled
- 等待所有
Promise
完成,无论结果是fulfilled
还是rejected
,提供了更完整的Promise
控制。
具体得使用可查看JavaScript异步编程规范->实现一个符合Promise A+规范的 Promise
7. ES12 (ECMAScript 2021)
7.1. String.prototype.replaceAll
const str = 'hello world';
console.log(str.replaceAll('l', 'L')); // heLLo worLd
7.2. Promise.any 和 AggregateError
promise.any可以返回任意一个提前resolve的结果,在现实的应用中,这种情况是非常常见的,我们来模拟一个例子:
const prom1 = new Promise((resolve, reject) => {
setTimeout(
() => resolve("promise one"),
Math.floor(Math.random() * 100)
);
});
const prom2 = new Promise((resolve, reject) => {
setTimeout(
() => resolve("promise two"),
Math.floor(Math.random() * 100)
);
});
const prom3 = new Promise((resolve, reject) => {
setTimeout(
() => resolve("promise three"),
Math.floor(Math.random() * 100)
);
});
(async function() {
const result = await Promise.any([prom1, prom2, prom3]);
console.log(result);
})();
上述代码可以随机输出promise one
,promise two
,promise three
。
如果将上述代码改成所有的都reject
,那么在 nodeJS
环境 会抛出AggregateError
:
const prom1 = new Promise((resolve, reject) => {
setTimeout(
() => reject("promise one rejected"),
Math.floor(Math.random() * 100)
);
});
const prom2 = new Promise((resolve, reject) => {
setTimeout(
() => reject("promise two rejected"),
Math.floor(Math.random() * 100)
);
});
const prom3 = new Promise((resolve, reject) => {
setTimeout(
() => reject("promise three rejected"),
Math.floor(Math.random() * 100)
);
});
try{
(async function() {
const result = await Promise.any([prom1, prom2, prom3]);
console.log(result);
})();
} catch(error) {
console.log(error.errors);
}
测试:
在浏览器环境则正常:
7.3. WeakRef
下面WearkRef的解释来自MDN:
示例:
这个例子演示了在一个 DOM
元素中启动一个计数器,当这个元素不存在时停止:
class Counter {
constructor(element) {
// Remember a weak reference to the DOM element
this.ref = new WeakRef(element);
this.start();
}
start() {
if (this.timer) {
return;
}
this.count = 0;
const tick = () => {
// Get the element from the weak reference, if it still exists
const element = this.ref.deref();
if (element) {
element.textContent = ++this.count;
} else {
// The element doesn't exist anymore
console.log("The element is gone.");
this.stop();
this.ref = null;
}
};
tick();
this.timer = setInterval(tick, 1000);
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = 0;
}
}
}
const counter = new Counter(document.getElementById("counter"));
counter.start();
setTimeout(() => {
document.getElementById("counter").remove();
}, 5000);
7.4. 逻辑运算符和赋值表达式(&&=,||=,??=)
已知 &&
和 ||
是被来进行逻辑操作的运算符。ES12
提供了&&
和 ||
的二元操作符:
let x = 1;
let y = 3;
x &&= y; // x = x && y; // 如果x为true,则x将被赋值为y的值
x ||= y; // x = x || y; // 如果x为false,则x将被赋值为y的值
console.log(x); // 3
console.log(y); // 3
测试:
还提供了??
的二元操作符:
let x;
let y = 2;
x ??= y; // 判断x是不是空,如果是空那么将y的值赋给x。
console.log(x); // 2
测试:
7.5. 数字分隔符
这个新特性是为了方便程序员看代码而出现的,如果数字比较大,那么看起来就不是那么一目了然,比如下面的长数字:
const number= 1000000000000;
一眼看不出这个数字的体量到底是多大,所以ES12
提供了数字分隔符_
。
分隔符不仅可以分割十进制,也可以分割二净值或者十六净值的数据,非常好用。
const number = 1_000_000_000_000; // 十进制
console.log(number);
const binary = 0b1010_0101_1111_1101; // 二进制
console.log(binary);
const hex = 0xAF_BF_C3; // 十六进制
console.log(hex);
测试:
8. ES13 (ECMAScript 2022)
8.1. 类
在 ES13
之前,类字段只能在构造函数中声明。与许多其他语言不同,无法在类的最外层作用域中声明或定义它们。
class Car {
constructor() {
this.color = 'blue';
this.age = 2;
}
}
const car = new Car();
console.log(car.color); // blue
console.log(car.age); //
而 ES13
消除了这个限制。现在我们可以编写这样的代码:
class Car {
color = 'blue';
age = 2;
}
const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2
8.3. 私有方法和字段
ES13
以前,不可能在类中声明私有成员。成员传统上带有下划线( \\_)
前缀,以表明它是私有的,但仍然可以从类外部访问和修改它。
class Person {
_firstName = 'Joseph';
_lastName = 'Stevens';
get name() {
return `${this._firstName} ${this._lastName}`;
}
}
const person = new Person();
console.log(person.name); // Joseph Stevens
// 仍可以从类外部访问, 原本打算设为私有的成员
console.log(person._firstName); // Joseph
console.log(person._lastName); // Stevens
// 也可以修改
person._firstName = 'Robert';
person._lastName = 'Becker';
console.log(person.name); // Robert Becker
测试:
ES13
之后,我们现在可以通过在类前面添加 ( #
) 来向类添加私有字段和成员。尝试从外部访问这些属性时,nodeJS
环境将会引发错误:
class Person {
#firstName = 'Joseph';
#lastName = 'Stevens';
get name() {
return `${this.#firstName} ${this.#lastName}`;
}
}
const person = new Person();
console.log(person.name);
console.log(person.#firstName);
console.log(person.#lastName);
测试:
而浏览器环境则可以正常访问:
8.4. await顶层操作
在 JavaScript
中,await
运算符用于暂停执行,直到 一个 Promise
被解决(执行或拒绝)。 以前只能在 async
中使用此运算符。ES13
以后可以在全局作用域中直接使用 await
。
function setTimeoutAsync(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timeout);
});
}
//语法错误:await 仅在异步函数中有效
await setTimeoutAsync(3000);
有了 ES13
,现在我们可以:
function setTimeoutAsync(timeout) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timeout);
});
}
// 等待超时 - 没有错误抛出
await setTimeoutAsync(3000);
8.5. 静态类字段和静态私有方法
现在可以在 ES13
中为类声明静态字段和静态私有方法。静态方法可以使用关键字this访问类中的其他私有/公共静态成员,实例方法可以使用this.constructor访问他们。
class Person {
static #count = 0; static getCount() {
return this.#count;
}
constructor() {
this.constructor.#incrementCount();
}
static #incrementCount() {
this.#count++;
}
}
const person1 = new Person();
const person2 = new Person();
console.log(Person.getCount()); // 2
测试:
8.6. 类静态块
-
ES13
引入了一项特性,允许开发者定义仅在创建类时执行一次的静态块。这一特性与其他面向对象编程语言(如C\#
和Java
)中的静态构造函数相似。 -
在一个类的主体中,你可以定义任意数量的静态 {} 初始化块。它们会按照声明的顺序与任何交错的静态字段初始值设定项一起执行。此外,你还可以通过块中的
super
关键字访问超类的静态属性。这为开发者提供了更多的灵活性和控制能力。
class Vehicle {
static defaultColor = 'blue';
}
class Car extends Vehicle {
static colors = [];
static {
this.colors.push(super.defaultColor, 'red');
}
static {
this.colors.push('green');
}
}
console.log(Car.colors); // [ 'blue', 'red', 'green' ]
测试:
这段代码展示了如何利用类的静态属性和静态代码块来初始化类的静态属性,以及如何从派生类访问基类的静态属性。这种方式可以用于在类加载时执行一些初始化逻辑,比如预加载数据或配置类的静态成员。下面是对代码的逐行解析:
- 定义了一个名为
Vehicle
的基类,其中包含一个静态属性defaultColor
,被设置为字符串'blue'
。 - 定义了一个名为
Car
的派生类,继承自Vehicle
。Car
类中包含一个静态数组属性colors
,初始为空数组。 - 第一个
static
代码块是在类体内部定义的,这种结构允许你在类加载时执行一些初始化代码。在这个代码块中,this.colors.push(super.defaultColor,'red')
被执行,将Vehicle
类的静态属性defaultColor(即'blue')
和字符串'red'
添加到Car
类的colors
数组中。 - 第二个
static
代码块继续向Car
类的colors
数组中添加字符串'green'
。 - 最后一行代码
console.log(Car.colors);
打印出Car
类的colors
静态属性,输出结果为[ 'blue', 'red', 'green' ]
,这正是两次调用push
方法后的数组内容。
8.7. 检查对象中的私有字段
开发者如今可以利用这一新功能,使用运算符in
来方便地检查对象是否包含某个特定的私有字段。
class Car {
#color;
hasColor() {
return #color in this;
}
}
class House {
#color;
hasColor() {
return #color in this;
}
}
const car = new Car();
const house = new House();
console.log(car.hasColor()); // true;
console.log(car.hasColor.call(house)); // false
console.log(house.hasColor()); // true
console.log(house.hasColor.call(car)); // false
通过运算符in
,可以准确区分不同类中具有相同名称的私有字段。
测试:
8.9. 正则表达式匹配索引
在ES13
之前,我们只能获取字符串中正则表达式匹配的起始索引:
const str = 'sun and moon';
const regex = /and/;
const matchObj = regex.exec(str);
console.log(matchObj); // [ 'and', index: 4, input: 'sun and moon', groups: undefined ]
ES13
之后,可以通过指定一个/d
正则表达式标志来获取匹配开始和结束的两个索引。这一特性赋予了更多的灵活性和控制能力。
const str = 'sun and moon';
const regex = /and/d;
const matchObj = regex.exec(str);
/**
[
'and',
index: 4,
input: 'sun and moon',
groups: undefined,
indices: [ [ 4, 7 ], groups: undefined ]
]
*/
console.log(matchObj);
测试:
设置标志后d
,返回的对象将具有indices
包含起始索引和结束索引的属性。
8.10. Object.hasOwn()方法
在 JavaScript
中,我们可以使用Object.prototype.hasOwnProperty()
方法来检查对象是否具有给定的属性。
class Car {
color = 'green';
age = 2;
}
const car = new Car();
console.log(car.hasOwnProperty('age')); // true
console.log(car.hasOwnProperty('name')); // false
测试:
然而,这种方法存在一些问题。首先,Object.prototype.hasOwnProperty()
方法并未受到保护,这意味着我们可以通过自定义的hasOwnProperty()
方法来覆盖它,而这个自定义方法可能会具有与Object.prototype.hasOwnProperty()
不同的行为。需要额外注意的是这一点。
class Car {
color = 'green';
age = 2; // This method does not tell us whether an object of
// this class has a given property.
hasOwnProperty() {
return false;
}
}
const car = new Car();
console.log(car.hasOwnProperty('age')); // false
console.log(car.hasOwnProperty('name')); // false
测试:
另外一个问题是,如果我们使用了 null
原型(通过 Object.create(null)
创建的对象),那么试图调用该方法将会产生错误。
const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
// TypeError: obj.hasOwnProperty 不是函数
console.log(obj.hasOwnProperty('color'));
测试:
为了克服这些问题,我们可以利用属性调用方法Object.prototype.hasOwnProperty.call()
来解决。具体示例如下所示:
const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;
console.log(Object.prototype.hasOwnProperty.call(obj, 'color')); // true
console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); // false
测试:
这种方式并不十分便利。为了避免重复,我们可以编写一个可重用的函数,这样可以使我们的代码更加简洁和高效:
function objHasOwnProp(obj, propertyKey) {
return Object.prototype.hasOwnProperty.call(obj, propertyKey);
}
const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;
console.log(objHasOwnProp(obj, 'color')); // true
console.log(objHasOwnProp(obj, 'name')); // false
测试:
现在不需要在那样做了,我们还可以使用全新的内置方法Object.hasOwn()
来处理这个问题。它与我们之前编写的可重用函数类似,接受对象和属性作为参数,并且返回一个布尔值,如果指定的属性是对象的直接属性,则返回true
;否则返回false
。
const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;
console.log(Object.hasOwn(obj, 'color')); // true
console.log(Object.hasOwn(obj, 'name')); // false
测试:
8.11. 错误原因属性
现在,错误对象增加了一个cause
属性,该属性用于指定导致错误抛出的原始错误。通过这种方式,我们可以为错误添加额外的上下文信息,从而更好地诊断意外的行为。要指定错误的原因,我们可以在作为构造函数的第二个参数传递给Error()
的对象中设置属性来实现。这种方法能够提供更丰富的错误追踪和调试信息。
function userAction() {
try {
apiCallThatCanThrow();
} catch (err) {
throw new Error('New error message', { cause: err });
}
}
try {
userAction();
} catch (err) {
console.log(err);
console.log(err.cause);
console.log(`Cause by: ${err.cause}`);
}
测试:
8.12. 从数组最后查找
在 JavaScript
中,我们已经可以使用Array
的find()
方法来查找数组中满足指定测试条件的元素。类似地,我们也可以使用findIndex()
方法来获取满足条件的元素的索引值。find()
和findIndex()
都是从数组的第一个元素开始搜索,但在某些情况下,从最后一个元素开始搜索可能会更有效。
有些情况下,我们知道从数组的末尾进行查找可能会获得更好的性能表现。例如,在这里我们尝试查找数组中prop
属性等于"value"
的项目。这时候,可以通过使用reverse()
方法将数组反转,然后使用find()
和findIndex()
方法来从末尾开始搜索。下面是具体的实现示例:
const letters = [
{ value: 'v' },
{ value: 'w' },
{ value: 'x' },
{ value: 'y' },
{ value: 'z' },
];
const found = letters.find((item) => item.value === 'y');
const foundIndex = letters.findIndex((item) => item.value === 'y');
console.log(found); // { value: 'y' }
console.log(foundIndex); // 3
测试:
上面的代码可以获取正确结果,但由于目标对象更接近数组的尾部,如果我们使用findLast()
和findLastIndex()
方法来从数组的末尾进行搜索,很可能能够显著提升程序的执行效率。通过这种方式,我们可以更快地找到所需的元素或索引,从而优化代码性能。
const letters = [
{ value: 'v' },
{ value: 'w' },
{ value: 'x' },
{ value: 'y' },
{ value: 'z' },
];
const found = letters.findLast((item) => item.value === 'y');
const foundIndex = letters.findLastIndex((item) => item.value === 'y');
console.log(found); // { value: 'y' }
console.log(foundIndex); // 3
测试:
在一些特定的使用场景中,我们需要从数组的末尾开始搜索来获取准确的元素。举个例子,假设我们要查找数字列表中的最后一个偶数,使用find()
或findIndex()
方法可能会导致错误的结果:
const nums = [7, 14, 3, 8, 10, 9];
// 给出 14,而不是 10
const lastEven = nums.find((value) => value % 2 === 0);
// 给出 1,而不是 4
const lastEvenIndex = nums.findIndex((value) => value % 2 === 0);
console.log(lastEven); // 14
console.log(lastEvenIndex); // 1
测试:
如果我们在调用reverse()
方法之前使用数组的slice()
方法创建新的数组副本,就可以避免不必要地改变原始数组的顺序。然而,在处理大型数组时,这种方法可能会导致性能问题,因为需要复制整个数组。
此外,findIndex()
方法在反转数组时仍然无法达到预期效果,因为元素的反转会导致它们在原始数组中的索引改变。为了获取元素的原始索引,我们需要进行额外的计算,这意味着需要编写更多的代码来处理这种情况。
const nums = [7, 14, 3, 8, 10, 9];
// 在调用reverse()之前使用展开语法复制整个数组
// calling reverse()
const reversed = [...nums].reverse();
// 正确给出 10
const lastEven = reversed.find((value) => value % 2 === 0);
// 给出 1,而不是 4
const reversedIndex = reversed.findIndex((value) => value % 2 === 0);
// 需要重新计算得到原始索引
const lastEvenIndex = reversed.length - 1 - reversedIndex;
console.log(lastEven); // 10
console.log(reversedIndex); // 1
console.log(lastEvenIndex); // 4
测试:
使用findLast()
和findLastIndex()
方法在需要查找数组中最后一个符合条件的元素或索引时非常实用。它们能够准确地定位目标对象,并且从数组末尾开始搜索,提供了高效的解决方案。
const nums = [7, 14, 3, 8, 10, 9];
const lastEven = nums.findLast((num) => num % 2 === 0);
const lastEvenIndex = nums.findLastIndex((num) => num % 2 === 0);
console.log(lastEven); // 10
console.log(lastEvenIndex); // 4
测试:
9. ES14 (ECMAScript 2023)
9.1. Array.prototype.toSorted
JavaScript
中 Array.prototype.sort()
具有排序的功能,但 Array.prototype.sort()
会产生副作用,会修改原数据:
let arr = [3, 5, 8, 2, 1];
console.log(arr.sort());
console.log(arr);
测试:
而 Array.prototype.toSorted
不仅可以排序,还是个纯函数,并不会改变原数据:
let arr = [3, 5, 8, 2, 1];
console.log(arr.toSorted());
console.log(arr);
测试:
同时 toSorted()
和 sort()
一样,接受一个可选参数作为比较函数。例如,我们可以使用 toSorted()
创建一个按降序排列的新数组:
let arr = [3, 5, 8, 2, 1];
let toSortedArr = arr.toSorted((a, b) => a - b); // a - b 升序,b - a 降序
console.log(toSortedArr);
console.log(arr);
测试:
toSorted()
也可以应用于对象数组。这种情况下,需要提供一个使用对象上的数据的比较函数,因为对象没有自然的排序方式:
- 通过字符串排列顺序比较
const objects = [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
{ name: "Bill", age: 40 },
{ name: "Mary", age: 20 }
];
const sortedObjects = objects.toSorted((a, b) => {
return a.name.localeCompare(b.name);
});
console.log(sortedObjects);
测试:
- 通过age升序排列:
const objects = [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
{ name: "Bill", age: 40 },
{ name: "Mary", age: 20 }
];
const sortedObjects = objects.toSorted((a, b) => {
return a.age - b.age;
});
console.log(sortedObjects);
测试:
9.2. Array.prototype.toReversed
JavaScript
中 Array.prototype.reverse()
具有反转数组的功能,但 Array.prototype.sort()
会产生副作用,会修改原数据:
let arr = [3, 5, 8, 2, 1];
console.log(arr.reverse());
console.log(arr);
测试:
而 Array.prototype.toReversed
不仅可以反转数组,还是个纯函数,并不会改变原数据:
let arr = [3, 5, 8, 2, 1];
console.log(arr.toSorted());
console.log(arr);
测试:
9.3. Array.prototype.with
Array.prototype.with()
允许你在不修改原始数组的情况下创建一个新的数组,其中某个元素已经被替换。这个方法提供了对数组的一种不可变更新方式,这对于函数式编程风格非常有用,因为它避免了直接修改数据,从而减少了副作用。
- 语法
arr.with(index, newValue);
- 参数
- index:你想要替换的元素的索引。
- newValue:你想要在新数组中放置的新值。
-
返回值
返回一个新的数组,其中index
位置的元素被newValue
替换。原始数组保持不变。
测试: -
代码示例
const originalArray = [1, 2, 3];
const newArray = originalArray.with(1, 4);
console.log(originalArray); // 输出: [1, 2, 3]
console.log(newArray); // 输出: [1, 4, 3]
测试:
9.4. Array.prototype.toSpliced
已知 Array.prototype.splice
返回被替换项,并可以在数组中插入一些内容,但是会改变原数组:
const arr = ["red", "orange", "yellow", "green", "blue", "purple"];
const newArr = arr.splice(2, 1, "pink", "cyan");
console.log(newArr); // ['yellow']
console.log(newArr[0]); // 'yellow'
console.log(newArr[2]); // undefined
console.log(arr); // ['red', 'orange', 'pink', 'cyan', 'green', 'blue', 'purple']
测试:
Array.prototype.toSpliced
是一个纯函数,返回新数组,并且不产生副作用,原数组不会被修改:
const arr = ["red", "orange", "yellow", "green", "blue", "purple"];
const newArr = arr.toSpliced(2, 1, "pink", "cyan");
console.log(newArr); // ["red", "orange", "pink", "cyan", "green", "blue", "purple"]
console.log(newArr[0]); // 'red'
console.log(newArr[2]); // 'pink'
console.log(arr); // ['red', 'orange', 'yellow', 'green', 'blue', 'purple']
测试:
9.5. 正式的 shebang 支持
在JavaScript
中,尤其是在Node.js
环境中,shebang
(也称为hashbang
)是一种特殊的第一行注释,用于指示脚本应该使用哪种解释器或运行时环境来执行。对于Node.js
脚本,shebang
通常看起来像这样:
#!/usr/bin/env node
这条shebang
行告诉系统使用node
命令(即Node.js
运行时)来执行脚本。/usr/bin/env
是一个Unix/Linux
实用程序,它允许你指定一个环境变量或路径查找列表中的可执行文件,这使得脚本可以在不同的系统上更灵活地运行,因为node
可能安装在不同的路径下。
为了使包含shebang
的脚本可执行,你需要更改文件的权限。在Unix/Linux
系统中,你可以使用chmod
命令来做到这一点:
chmod +x yourscript.js
之后,你就可以直接运行脚本,就像执行任何其他可执行文件一样:
./yourscript.js
请注意,在Windows
系统中,shebang
行不会被解释,因此你仍然需要通过node
命令来运行脚本:
node yourscript.js
尽管如此,shebang
行在跨平台项目中仍然有用,因为它可以确保在Unix-like
系统上的行为一致。
测试:
9.6. Symbol 作为 WeakMap 的键
WeakMap
被用来存储 Symbol
类型的键及其对应的值。由于 Symbol
值是全局唯一的,这使得它们成为 WeakMap
键的理想选择,因为它们可以确保键的唯一性。ES14
之前,WeakMap
仅允许对象作为键值,新特性更容易创建和共享key
。
// 定义一个 WeakMap 来存储 Symbol 类型的键和它们被调用的次数
let map = new WeakMap();
// 使用 Symbol 的全局方法来创建一个新的唯一 symbol
const symbolKey = Symbol('uniqueKey');
// 定义一个函数来处理 symbol 并记录它被调用的次数
function useSymbol(symbol) {
function doSomethingWith(s) {
// 检查 s 是否为 Symbol 类型,如果是则转换为字符串描述
const str = typeof s === 'symbol' ? s.toString() : s;
console.log(`Doing something with ${str}`);
}
// 获取 symbol 对应的调用次数,如果没有则默认为 0
let called = map.get(symbol) || 0;
// 执行操作并记录调用次数
doSomethingWith(symbol);
map.set(symbol, ++called);
// 输出当前的调用次数
console.log(`Called: ${called} times`);
}
// 调用 useSymbol 函数
useSymbol(symbolKey);
这段代码定义了一个WeakMap
来存储Symbol
类型的键和它们被调用的次数。然后使用Symbol
的全局方法创建了一个新的唯一Symbol
作为键。useSymbol
函数用于处理这个Symbol
并记录它的调用次数。在函数内部,首先定义了一个doSomethingWith
函数,用于执行对Symbol
的操作。然后通过WeakMap
获取该Symbol
对应的调用次数,如果没有则默认为0
。接下来执行操作并记录调用次数,最后输出当前的调用次数。通过调用useSymbol
函数,可以实现对Symbol
的处理和调用次数的记录。
测试:
10. ES15 (ECMAScript 2024)
10.1. Group By 分组
在 ES15 之前,JavaScript 并没有想 Java 那样提供分组的方法,以前如果需要实现一个分组功能,我们需要这么做:
- 以 age 属性来分组,需要自己实现一个groupBy方法
function myGroupBy(array, iteratee) {
return array.reduce((groups, value) => {
const key = iteratee(value);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(value);
return groups;
}, {});
}
// 示例使用
const data = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
{ name: 'David', age: 30 }
];
const groupedData = myGroupBy(data, item => item.age);
console.log(groupedData);
在这个例子中,myGroupBy
函数使用了 reduce
方法来累积分组的结果。iteratee
参数是一个函数,它决定了数组中的每个元素应该如何被分组。在上面的示例中,我们按照年龄 (age)
对数据进行分组。
如果你需要使用 TypeScript
,你可以添加类型注解以确保类型安全:
function myGroupBy<T, K>(array: T[], iteratee: (item: T) => K): Record<K, T[]> {
return array.reduce((groups, value) => {
const key = iteratee(value);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(value);
return groups;
}, {} as Record<K, T[]>);
}
这个版本的 myGroupBy
函数具有类型参数T
和 K
,分别表示数组元素的类型和分组键的类型。这将帮助 TypeScript
编译器推断正确的类型并在开发过程中提供更好的类型检查。
测试:
ES15 之后,直接就可以使用自带的 groupBy 方法来实现:
const data = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
{ name: 'David', age: 30 }
];
const groupedData = groupBy(data, item => item.age);
console.log(groupedData);
测试:
10.2. Math.sign()
Math.sign()
方法用于判断一个数的符号,即正负性:
- 如果参数是正数,返回
1
。 - 如果参数是零,返回
0
。 - 如果参数是
-0
(负零),返回-0
。 - 如果参数不是数字(
NaN
),返回NaN
。
let a = 2;
let b = +3;
let c = -4;
let d = 0;
let e = -0;
let f = NaN;
console.log(Math.sign(a)); // 1
console.log(Math.sign(b)); // 1
console.log(Math.sign(c)); // -1
console.log(Math.sign(d)); // 0
console.log(Math.sign(e)); // -0
console.log(Math.sign(f)); // NaN
测试:
10.3. Promise.withResolvers
Promise.withResolvers
,这是一个Promise API
扩展,它允许你创建一个Promise
对象,同时立即获得用于解析或拒绝该Promise
的函数。这与传统的Promise
构造函数不同,在传统构造函数中,你需要立即调用解析或拒绝的函数,而在Promise.withResolvers
中,你可以稍后调用这些函数。
Promise.withResolvers
返回一个对象,其中包含以下属性:
promise
:新创建的Promise
对象。resolve
:一个函数,用于在适当的时候解析Promise
。reject
:一个函数,用于在适当的时候拒绝Promise
。
例如:
const { promise, resolve, reject } = Promise.withResolvers();
// ...做一些异步操作...
// 当异步操作完成时,调用resolve或reject函数
resolve('Success!');
// 或者
reject(new Error('Failed!'));
10.4. 正则表达式标志 /v
在正则表达式中,通常使用的标志有:
g
:全局匹配,查找所有匹配项,而不仅仅是第一个。i
:忽略大小写。m
:多行模式,使^
和$
能够匹配每行的开始和结束,而不仅仅是整个字符串的开始和结束。s
:点号(.)
匹配所有字符,包括换行符。u
:Unicode
模式,使正则表达式能够正确处理Unicode
字符。y
:粘性匹配,确保正则表达式从字符串的当前位置开始匹配,而不是从任何位置开始。
ES15
新增了 /v
标识符,v
标志是 u
标志的“升级”,可启用更多与 Unicode
相关的功能。使用 v
标志,不光可以继承 u
标志所有的功能,还支持以下功能:
- 扩展的集合符号,允许计算字符或字符串集合的差异、相交和联合
const reg1 = /[\p{Decimal_Number}--[0-9]]/v; // Non-ASCII decimal digits
const reg2 = /[\p{ASCII}&&\p{Letter}]/v; // ASCII letters
const reg3 = /[[\p{ASCII}&&\p{Letter}]\p{Number}]/v; // ASCII letters, or any digit
console.log(reg1.test("1"));
console.log(reg2.test("1"));
console.log(reg3.test("1"));
测试:
- 字符串的属性,允许使用\p 转义的多节点属性
const reg4 = "Did you see the 👩🏿❤️💋👩🏾 emoji?".match(/\p{RGI_Emoji}/v) // ["👩🏿❤️💋👩🏾"]
console.log(reg4);
测试:
- 集合中的多节点字符串,使用一个新的\q 转义
const reg5 = /[\r\n\q{\r\n|NEWLINE}]/v; // Matches \r, \n, \r\n or NEWLINE
console.log(reg5);
console.log(reg5.test("\r"));
console.log(reg5.test("\r\n"));
测试:
10.5. ArrayBuffers 和 SharedArrayBuffers 的新功能
10.5.1. ArrayBuffers
在JavaScript
中,ArrayBuffer
是一种用于表示原始二进制数据的类数组对象。它提供了固定长度的缓冲区,用于存储二进制数据。ArrayBuffer
不直接操作数据,而是通过视图(如 TypedArray
和 DataView
)来读取和写入数据。
ArrayBuffer 的特性:
- 原始数据存储:
ArrayBuffer
提供了一块连续的、固定大小的内存区域,可以用来存储二进制数据。 - 低级接口:与
Blob
这样的高级接口不同,ArrayBuffer
更接近底层内存操作。 - 视图:
ArrayBuffer
通常与TypedArray
(如Int8Array, Uint8Array, Float32Array
等)或DataView
一起使用,以不同的数据类型和字节序来访问缓冲区中的数据。 - 共享内存:在
Web Workers
或其他多线程环境中,ArrayBuffer
可以被多个执行环境共享,这在SharedArrayBuffer
中表现得更为明显,后者支持真正的共享内存操作。
使用示例:
const buffer = new ArrayBuffer(8); // 创建一个 8 字节的 ArrayBuffer
console.log(buffer, 'buffer');
const view = new Uint8Array(buffer); // 创建一个视图,可以按无符号 8 位整数读写数据
console.log(view, 'view');
view[0] = 1; // 写入数据
console.log(view[0], 'view[0]'); // 读取数据
测试:
- ArrayBuffers 就地调整大小
ES15
之前ArrayBuffer
的大小一旦创建后就不能改变,ES15
之后ArrayBuffer
可通过
resize
方法就地调整大小:
// 创建一个 8 字节的 ArrayBuffer
const buffer = new ArrayBuffer(8);
console.log(buffer.maxByteLength, 'buffer.maxByteLength');
// 获取ArrayBuffer的长度
console.log(buffer.byteLength);
// 修改buffer的长度
buffer.resize(16);
// 获取ArrayBuffer的长度
console.log(buffer.byteLength);
测试:
此时可以看到报错了,这是因为此时创建的这个 ArrayBuffer
最大为 8
字节,当我们修改大小时超过了 ArrayBuffer
最大字节,修改如下,给 ArrayBuffer
传递第二个参数,手动设置最大长度:
// 创建一个 8 字节的 ArrayBuffer;{maxByteLength: 32} 设置最大长度
const buffer = new ArrayBuffer(8, {maxByteLength: 32});
console.log(buffer.maxByteLength, 'buffer.maxByteLength');
// 获取ArrayBuffer的长度
console.log(buffer.byteLength);
// 修改buffer的长度
buffer.resize(16);
// 获取ArrayBuffer的长度
console.log(buffer.byteLength);
测试:
- ArrayBuffers .transfer() 可转移
ES15
之前也是可以进行转移的,之前是通过参数进行配置的,现在对外提供了一个transfer
函数调用,更加方便了:
// 创建一个 8 字节的 ArrayBuffer
const buffer = new ArrayBuffer(8);
console.log(buffer.detached, 'buffer创建');
// buffer 转移
const transferred = buffer.transfer();
console.log(buffer.detached, 'buffer转移');
console.log(transferred.detached, 'transferred.detached');
测试:
detached
属性是一个访问器属性,其 set
访问器函数是 undefined
,这意味着你只能读取此属性。该属性的值在创建 ArrayBuffer
时设置为 false
。如果 ArrayBuffer
已被传输,则该值将变为 true
,这将使该实例从其底层内存中分离。一旦缓冲区被分离,它就不再可用。
10.5.2. SharedArrayBuffers
SharedArrayBuffer
是 ArrayBuffer
的一个子类,设计用于实现共享内存,使得多个 JavaScript
执行上下文(例如 Web Workers
)可以在同一段内存上进行读写操作。这在多线程编程中非常有用,尤其是在需要高性能数据共享和同步的场景下。
SharedArrayBuffer 特性:
- 共享内存:
SharedArrayBuffer
实例表示的缓冲区可以在多个执行上下文中共享,允许线程间通信而无需复制数据。 - 持久性:
SharedArrayBuffer
不会被垃圾回收器回收,除非整个页面被卸载或者SharedArrayBuffer
被撤销(如通过Atomics.waitAbort
)。
使用示例一:
// 创建一个 8 字节的 SharedArrayBuffer
const sab = new SharedArrayBuffer(8);
console.log(sab, 'sab');
// 创建一个视图
const i32a = new Int32Array(sab);
console.log(i32a, 'i32a');
i32a[0] = 42; // 写入数据
console.log(i32a[0]); // 读取数据
const transferred = sab.transfer();
console.log(transferred, 'transferred');
测试:
SharedArrayBuffers
可以调整大小,但它们只能增长而不能缩小。它们不可转移,因此无法获取 ArrayBuffers
的方法 .transfer()
。
使用示例二(在多线程中使用):
// 在主线程中
const worker = new Worker('worker.js');
const sab = new SharedArrayBuffer(8);
const i32a = new Int32Array(sab);
i32a[0] = 42;
worker.postMessage({arrayBuffer: sab}, [sab]);
// 在 worker.js 中
self.onmessage = function(event) {
const receivedSab = event.data.arrayBuffer;
const receivedI32a = new Int32Array(receivedSab);
console.log(receivedI32a[0]); // 输出应该是 42
};
在这个例子中,postMessage
的第二个参数是一个数组,包含了要共享所有权的 ArrayBuffer
或 SharedArrayBuffer
对象。这样,Worker
就可以直接访问和修改共享内存了。
10.6. String.prototype.isWellFormed
String.prototype.isWellFormed()
方法返回一个表示该字符串是否包含单独代理项的布尔值,如果字符串不包含单独代理项,返回 true,否则返回 false。JavaScript
中的字符串是 UTF-16
编码的。UTF-16
编码中有代理对的概念。
字符串基本上表示为 UTF-16
码元的序列。在 UTF-16
编码中,每个码元都是 16
位长。这意味着最多有 216
个或 65536
个可能的字符可表示为单个 UTF-16 码元。
然而,整个 Unicode
字符集比 65536
大得多。额外的字符以**代理对(surrogate pair)**的形式存储在 UTF-16
中,代理对是一对 16
位码元,表示一个单个字符。为了避免歧义,配对的两个部分必须介于 0xD800
和 0xDFFF
之间,并且这些码元不用于编码单码元字符。
前导代理,也称为高位代理,其值在 0xD800
和 0xDBFF
之间(含),而后尾代理,也称为低位代理,其值在 0xDC00 和 0xDFFF
之间(含)。
“单独代理项(lone surrogate)”是指满足以下描述之一的 16 位码元:
- 它在范围
0xD800
到0xDBFF
内(含)(即为前导代理),但它是字符串中的最后一个码元,或者下一个码元不是后尾代理。 - 它在范围
0xDC00
到0xDFFF
内(含)(即为后尾代理),但它是字符串中的第一个码元,或者前一个码元不是前导代理。
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() 错误:
如果传递的字符串格式不正确, 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.
}
10.7. String.prototype.toWellFormed
如果你需要将字符串转换为格式正确的字符串,可以使用 toWellFormed()
方法。toWellFormed()
方法返回一个字符串,其中该字符串的所有单独代理项都被替换为 Unicode
替换字符 U+FFFD
。
toWellFormed()
方法返回新的字符串是原字符串的一个拷贝,其中所有的单独代理项被替换为 Unicode
替换字符 U+FFFD
。如果 str
是格式正确的,仍然会返回一个新字符串(本质上是 str
的一个拷贝)。
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() 错误:
如果传递的字符串格式不正确, 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"
测试:
10.8. Atomics.waitAsync()
与 Atomics.waitAsync
对应的方法是 Atomics.wait
,它们都是在等待 SharedArrayBuffer
所代表的共享内存的某个位置的值变化。
Atomics.wait
是让线程睡眠来等待,从而致使线程阻塞,这种阻塞在 UI
线程是不可接受的,它会导致整个页面卡死无法响应,因此如果你在 UI
主线程调用 Atomics.wait
,浏览器会抛出异常。
而 Atomics.waitAsync()
静态方法异步等待共享内存的特定位置并返回一个 Promise
。
备注: 此操作仅适用于基于 SharedArrayBuffer 的 Int32Array 或 BigInt64Array 视图。
语法:
Atomics.waitAsync(typedArray, index, value)
Atomics.waitAsync(typedArray, index, value, timeout)
参数:
-
typedArray:基于
SharedArrayBuffer
的Int32Array
或BigInt64Array
。 -
index:
typedArray
中要等待的位置。 -
value:要测试的期望值。
-
timeout (可选):等待时间,以毫秒为单位。
NaN
(以及会被转换为NaN
的值,例如undefined
)会被转换为Infinity
。负值会被转换为0
。
返回值:
一个 Object
,包含以下属性:
-
async:一个布尔值,指示
value
属性是否为Promise
。 -
value:如果
async
是false
,它将是一个内容为"not-equal"
或"timed-out"
的字符串(仅当timeout
参数为0
时)。如果async
是true
,它将会是一个Promise
,其兑现值为一个内容为"ok"
或"timed-out"
的字符串。这个promise
永远不会被拒绝。
异常:
-
TypeError:如果
typedArray
不是一个基于SharedArrayBuffer
的Int32Array
或BigInt64Array
,则抛出该异常。 -
RangeError:如果
index
超出typedArray
的范围,则抛出该异常。
使用示例:
给定一个共享的 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"
)。
11. 结语
从ES6到ES15,JavaScript的发展轨迹清晰地展示了其作为一门语言的成熟过程。新特性的引入不仅丰富了语言本身,也为开发者提供了更多的工具和表达方式,使得JavaScript成为构建现代Web应用的首选语言。掌握这些新特性,不仅能提高代码质量和开发效率,还能让你的项目更具竞争力。随着未来版本的继续演进,JavaScript的潜力无限,值得我们持续关注和学习。