器→工具, 编程语言

JavaScript学习之版本特性

钱魏Way · · 12 次浏览

自 ECMAScript 2015(ES6)以来,JavaScript 标准每年都会发布新版本,增加新特性和改进已有功能。这些更新使得 JavaScript 变得更加强大和高效,提升了开发者的编程体验。

以下是一些重要的新增特性:

  • ECMAScript 2015(ES6):块级作用域、箭头函数、模板字符串、解构赋值、默认参数、展开运算符、Rest 参数、..of 循环、模块系统、类、Promise 和生成器等
  • ECMAScript 2016 (ES7):指数运算符 (**)、prototype.includes
  • ECMAScript 2017 (ES8):异步函数 (async/await)、values、Object.entries、字符串填充方法、对象解构中的剩余属性
  • ECMAScript 2018 (ES9):Rest/Spread 属性、异步迭代器、prototype.finally、正则表达式改进
  • ECMAScript 2019 (ES10):prototype.flat和 flatMap、Object.fromEntries、字符串修剪方法、Symbol.prototype.description、可选 catch 绑定
  • ECMAScript 2020 (ES11):可选链操作符 (?.)、空值合并操作符 (??)、动态导入 (import())、BigInt、allSettled、globalThis
  • ECMAScript 2021 (ES12):逻辑赋值运算符、数值分隔符、prototype.replaceAll、Promise.any、WeakRef 和 FinalizationRegistry
  • ECMAScript 2022 (ES13):顶层await、类字段、类的私有字段和方法、错误原因捕获、prototype.at 方法、正则表达式 d 标志、Object.hasOwn
目录

块级作用域(Block Scope)

块级作用域是 JavaScript 中的一种作用域机制,通过它可以在 {} 大括号内声明变量,这些变量只在该块内有效。块级作用域可以帮助避免变量提升(Hoisting)带来的潜在问题,提高代码的可靠性和可维护性。

块级作用域的声明

ECMAScript 2015 (ES6) 引入了 let 和 const 两种新的变量声明方式,用于创建块级作用域的变量和常量。

  • let关键字,let 用于声明一个块级作用域的变量。该变量只能在声明它的块内访问。
  • const关键字,const 用于声明一个块级作用域的常量。与 let 不同的是,const 声明的常量一旦被赋值,就不能重新赋值。

块级作用域的特性

变量遮蔽(Variable Shadowing)

块级作用域允许在内部块中声明与外部块同名的变量,这样内部块的变量会遮蔽(shadow)外部块的变量。

let x = 1;

if (true) {
  let x = 2; // 内部块的 x 遮蔽了外部块的 x
  console.log(x); // 输出: 2
}

console.log(x); // 输出: 1

在上面的示例中,内部块中的 x 变量遮蔽了外部块中的 x 变量,因此在内部块中访问 x 时,输出的是内部块的 x 的值。

循环中的块级作用域

使用 let 或 const 在循环中声明变量,每一次迭代都会创建一个新的块级作用域,避免了变量提升带来的问题。

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出: 0, 1, 2
  }, 100);
}

在上面的示例中,使用 let 声明的 i 变量在每次循环迭代中都有自己的作用域,因此输出结果是预期的 0, 1, 2。

let 和 const 的作用域行为

暂时性死区(Temporal Dead Zone, TDZ)

在块级作用域中,let 和 const 声明的变量在其声明之前是不可访问的,这个区域称为暂时性死区(TDZ)。

console.log(a); // 报错: ReferenceError: Cannot access 'a' before initialization
let a = 10;

console.log(b); // 报错: ReferenceError: Cannot access 'b' before initialization
const b = 20;

在上面的示例中,变量 a 和常量 b 在声明之前访问会导致错误,因为它们处于暂时性死区。

不允许重复声明

在同一块级作用域内,let 和 const 不允许重复声明同名变量或常量。

let x = 1;
let x = 2; // 报错: SyntaxError: Identifier 'x' has already been declared

const y = 1;
const y = 2; // 报错: SyntaxError: Identifier 'y' has already been declared

在上面的示例中,重复声明同名变量或常量会导致语法错误。

对比 var

在 ES6 之前,JavaScript 只有函数作用域和全局作用域,没有块级作用域。使用 var 声明的变量是函数作用域或全局作用域,并且会发生变量提升。

变量提升(Hoisting)

var 声明的变量会被提升到函数或全局作用域的顶部,但不会提升赋值部分。

console.log(x); // 输出: undefined
var x = 10;
console.log(x); // 输出: 10

在上面的示例中,变量 x 被提升到作用域顶部,因此第一次访问时是 undefined。

作用域问题

使用 var 声明的变量在块级内,会提升到函数或全局作用域,可能导致意外行为。

if (true) {
  var z = 30;
}
console.log(z); // 输出: 30

在上面的示例中,变量 z 在 if 块内声明,但由于 var 的提升行为,它实际上是全局作用域的变量。

箭头函数(Arrow Functions)

箭头函数(Arrow Function)是 ECMAScript 2015 (ES6) 引入的一种新的函数定义方式。与传统的函数表达式相比,箭头函数语法更加简洁,并且它们的 this 绑定行为也有所不同。

语法

箭头函数的基本语法如下:

(param1, param2, ..., paramN) => { statements }

如果箭头函数只有一个参数,参数列表的圆括号可以省略:

singleParam => { statements }

如果箭头函数只有一个表达式,且需要返回这个表达式的值,可以省略大括号 {} 和 return 关键字:

(param1, param2, ..., paramN) => expression

如果没有参数,需要使用空括号:

() => { statements }

示例

无参数

const greet = () => {
  console.log('Hello, world!');
};

greet(); // 输出: Hello, world!

一个参数

const square = x => x * 2;

console.log(square(5)); // 输出: 10

多个参数

const add = (a, b) => a + b;

console.log(add(2, 3)); // 输出: 5

特性

this 绑定

箭头函数没有自己的 this 绑定,this 的值由外层作用域决定。这意味着箭头函数在定义时就 “记住” 了 this 的值。

function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++;
    console.log(this.age);
  }, 1000);
}

const person = new Person();
// 输出: 1, 2, 3, ..., 每隔一秒输出一次

在上面的示例中,箭头函数中的 this 继承自 Person 构造函数的 this,因此可以正确地访问和修改 age 属性。如果使用传统的函数表达式,则需要手动绑定 this。

function Person() {
  this.age = 0;

  setInterval(function() {
    this.age++;
    console.log(this.age);
  }.bind(this), 1000);
}

const person = new Person();
// 输出: 1, 2, 3, ..., 每隔一秒输出一次

不能作为构造函数

箭头函数不能使用 new 关键字实例化对象,因为它们没有 [[Construct]] 方法。

const Person = () => {};
const p = new Person(); // 报错: TypeError: Person is not a constructor

没有 arguments 对象

箭头函数没有 arguments 对象,如果需要访问参数,可以使用剩余参数(rest parameter)语法。

const sum = (...args) => {
  return args.reduce((acc, curr) => acc + curr, 0);
};

console.log(sum(1, 2, 3, 4)); // 输出: 10

没有 prototype 属性

箭头函数没有 prototype 属性,因此它们不能用于定义类的方法,也不能作为构造函数使用。

const Foo = () => {};
console.log(Foo.prototype); // 输出: undefined

不支持 yield 关键字

箭头函数不能用作生成器函数,因此它们不支持 yield 关键字。

const gen = () => {
  yield 1; // 报错: SyntaxError: Unexpected number
};

使用场景

回调函数

箭头函数常用于回调函数中,尤其是那些需要访问外层 this 的场景,如事件处理、定时器、异步操作等。

ocument.getElementById('button').addEventListener('click', event => {
  console.log('Button clicked!');
});

数组方法

箭头函数常用于 Array.prototype.map、filter、reduce 等数组方法中,使代码更加简洁。

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // 输出: [2, 4, 6, 8, 10]

简化匿名函数

箭头函数可以简化匿名函数的定义,使代码更清晰。

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(function(n) {
  return n % 2 === 0;
});
console.log(evenNumbers); // 输出: [2, 4]

const evenNumbersArrow = numbers.filter(n => n % 2 === 0);
console.log(evenNumbersArrow); // 输出: [2, 4]

模板字符串(Template Literals)

模板字符串(Template Literals)是 ECMAScript 2015 (ES6) 引入的一种新的字符串表示法,提供了更强大的功能和更简洁的语法。模板字符串使用反引号(`)包围,可以包含内嵌的变量和表达式,并支持多行字符串和标签模板等特性。

基本语法

模板字符串使用反引号(`)而不是单引号(’)或双引号(”)来定义。

const name = 'John';
const greeting = `Hello, ${name}!`;
console.log(greeting); // 输出: "Hello, John!"

在上面的示例中,${name} 语法用于内嵌变量 name 的值到字符串中。

多行字符串

模板字符串允许直接在字符串中使用换行符,这使得多行字符串的定义更加直观和方便。

const multilineString = `This is a string
that spans across
multiple lines.`;
console.log(multilineString);

输出:

This is a string
that spans across
multiple lines.

内嵌表达式

模板字符串不仅可以嵌入变量,还可以嵌入任意的 JavaScript 表达式。

const a = 5;
const b = 10;
const result = `The sum of ${a} and ${b} is ${a + b}.`;
console.log(result); // 输出: "The sum of 5 and 10 is 15."

在上面的示例中,${a + b} 计算表达式的结果并插入到字符串中。

嵌套模板字符串

模板字符串还可以嵌套其他模板字符串。

const person = { name: 'Alice', age: 25 };
const description = `Name: ${person.name}, Age: ${person.age}, Greeting: ${`Hello, ${person.name}!`}`;
console.log(description); // 输出: "Name: Alice, Age: 25, Greeting: Hello, Alice!"

标签模板(Tagged Templates)

标签模板是一种高级用法,可以通过标签函数处理模板字符串。标签函数的第一个参数是字符串的数组,后续参数是内嵌表达式的值。

function tagged(strings, ...values) {
  console.log(strings); // 输出: ["Hello, ", "! You are ", " years old."]
  console.log(values);  // 输出: ["John", 30]
  return `${strings[0]}${values[0]}${strings[1]}${values[1]}${strings[2]}`;
}

const name = 'John';
const age = 30;
const result = tagged`Hello, ${name}! You are ${age} years old.`;
console.log(result); // 输出: "Hello, John! You are 30 years old."

标签函数可以用于多种场景,如国际化、多语言支持、转义 HTML 字符等。

模板字符串的特性

自动转义

模板字符串支持自动转义特殊字符,如换行符、制表符等。

const text = `This is a line with a newline\nand a tab\tcharacter.`;
console.log(text);

输出:

This is a line with a newline
and a tab    character.

嵌套复杂表达式

模板字符串允许嵌套复杂的表达式,包括函数调用、三元运算符等。

const user = { name: 'John', age: 30 };
const greet = user => `Hello, ${user.name}!`;
const message = `User Info: ${greet(user)}, Age: ${user.age > 18 ? 'Adult' : 'Minor'}.`;
console.log(message); // 输出: "User Info: Hello, John!, Age: Adult."

嵌入对象属性

模板字符串可以直接嵌入对象的属性,方便快捷。

const person = { name: 'Alice', age: 25 };
const description = `Name: ${person.name}, Age: ${person.age}`;
console.log(description); // 输出: "Name: Alice, Age: 25"

使用场景

动态生成 HTML

模板字符串可以用于动态生成 HTML 内容。

const user = { name: 'John', age: 30 };
const html = `
  <div>
    <h1>${user.name}</h1>
    <p>Age: ${user.age}</p>
  </div>
`;
document.body.innerHTML = html;

动态生成 SQL 语句

模板字符串可以用于动态生成 SQL 查询语句。

const table = 'users';
const condition = 'age > 18';
const query = `SELECT * FROM ${table} WHERE ${condition};`;
console.log(query); // 输出: "SELECT * FROM users WHERE age > 18;"

日志记录

模板字符串可以用于格式化日志输出。

const name = 'John';
const action = 'login';
const timestamp = new Date().toISOString();
const log = `[${timestamp}] User ${name} performed ${action}.`;
console.log(log); // 输出: "[2023-10-05T12:34:56.789Z] User John performed login."

解构赋值(Destructuring Assignment)

解构赋值(Destructuring Assignment)是 ECMAScript 2015 (ES6) 引入的一种语法,允许我们从数组或对象中提取值,并将其赋值给变量。解构赋值使得代码更加简洁和可读,尤其在处理复杂的数据结构时非常有用。

数组解构赋值

数组解构赋值允许我们从数组中提取值,并将其直接赋值给变量。

基本语法

const [a, b] = [1, 2];
console.log(a); // 输出: 1
console.log(b); // 输出: 2

跳过元素

我们可以跳过数组中的某些元素,只提取需要的值。

const [a, , c] = [1, 2, 3];
console.log(a); // 输出: 1
console.log(c); // 输出: 3

默认值

在解构数组时,可以为变量指定默认值。

const [a = 10, b = 5] = [1];
console.log(a); // 输出: 1
console.log(b); // 输出: 5

交换变量值

解构赋值提供了一种简洁的方式来交换两个变量的值。

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a); // 输出: 2
console.log(b); // 输出: 1

剩余元素

使用剩余元素(rest element)语法,可以将数组中的剩余部分赋值给一个变量。

const [a, ...rest] = [1, 2, 3, 4];
console.log(a); // 输出: 1
console.log(rest); // 输出: [2, 3, 4]

对象解构赋值

对象解构赋值允许我们从对象中提取属性值,并将其赋值给变量。与数组解构不同,对象解构是基于属性名而不是位置的。

基本语法

const {name, age} = {name: 'Alice', age: 25};
console.log(name); // 输出: "Alice"
console.log(age); // 输出: 25

重命名变量

我们可以使用冒号(:)语法来重命名变量。

const {name: personName, age: personAge} = {name: 'Alice', age: 25};
console.log(personName); // 输出: "Alice"
console.log(personAge); // 输出: 25

默认值

在解构对象时,可以为变量指定默认值。

const {name, age = 30} = {name: 'Alice'};
console.log(name); // 输出: "Alice"
console.log(age); // 输出: 30

嵌套解构

对象解构支持嵌套结构,可以从嵌套对象中提取值。

const user = {
  name: 'Alice',
  address: {
    city: 'New York',
    zip: '10001'
  }
};
const {name, address: {city, zip}} = user;
console.log(name); // 输出: "Alice"
console.log(city); // 输出: "New York"
console.log(zip); // 输出: "10001"

剩余属性

使用剩余属性(rest properties)语法,可以将对象中的剩余属性赋值给一个变量。

const {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}

解构赋值的应用场景

函数参数

解构赋值常用于函数参数,使得参数的处理更加简洁。

function greet({name, age}) {
  console.log(`Hello, my name is ${name} and I am ${age} years old.`);
}

const user = {name: 'Alice', age: 25};
greet(user); // 输出: "Hello, my name is Alice and I am 25 years old."

返回多个值

解构赋值可以用于从函数返回多个值,使得代码更加清晰。

function getUser() {
  return {name: 'Alice', age: 25};
}

const {name, age} = getUser();
console.log(name); // 输出: "Alice"
console.log(age); // 输出: 25

从数组中提取部分值

解构赋值可以方便地从数组中提取部分值,例如从 API 响应中提取特定字段。

const response = [200, 'OK', {data: [1, 2, 3]}];
const [statusCode, statusMessage, {data}] = response;
console.log(statusCode); // 输出: 200
console.log(statusMessage); // 输出: "OK"
console.log(data); // 输出: [1, 2, 3]

默认参数(Default Parameters)

JavaScript 默认参数(Default Parameters)是 ECMAScript 2015 (ES6) 引入的一种特性,允许在函数定义时为参数指定默认值。如果调用函数时没有提供这些参数,或者提供的参数值是 undefined,则使用默认值。默认参数使得函数更具可读性和健壮性,减少了处理未定义参数的重复代码。

基本语法

在函数定义中,可以直接为参数指定一个默认值。当调用函数时,如果没有提供该参数,或者提供的参数值是 undefined,则使用这个默认值。

function greet(name = 'Guest') {
  console.log(`Hello, ${name}!`);
}

greet(); // 输出: "Hello, Guest!"
greet('Alice'); // 输出: "Hello, Alice!"

在上面的示例中,函数 greet 的参数 name 默认值是 ‘Guest’。当调用 greet 时没有提供 name 参数,默认值 ‘Guest’ 会被使用。

多个参数的默认值

可以为多个参数同时指定默认值。

function greet(name = 'Guest', age = 18) {
  console.log(`Hello, ${name}! You are ${age} years old.`);
}

greet(); // 输出: "Hello, Guest! You are 18 years old."
greet('Alice'); // 输出: "Hello, Alice! You are 18 years old."
greet('Bob', 25); // 输出: "Hello, Bob! You are 25 years old."

与解构赋值结合使用

默认参数可以与解构赋值结合使用,使得函数参数的处理更加简洁和灵活。

对象解构

function greet({name = 'Guest', age = 18} = {}) {
  console.log(`Hello, ${name}! You are ${age} years old.`);
}

greet(); // 输出: "Hello, Guest! You are 18 years old."
greet({}); // 输出: "Hello, Guest! You are 18 years old."
greet({name: 'Alice'}); // 输出: "Hello, Alice! You are 18 years old."
greet({name: 'Bob', age: 25}); // 输出: "Hello, Bob! You are 25 years old."

在上面的示例中,函数 greet 的参数是一个对象,默认值是一个空对象 {}。通过解构赋值为 name 和 age 指定默认值。

数组解构

function sum([a = 0, b = 0] = []) {
  return a + b;
}

console.log(sum()); // 输出: 0
console.log(sum([1])); // 输出: 1
console.log(sum([1, 2])); // 输出: 3

在上面的示例中,函数 sum 的参数是一个数组,默认值是一个空数组 []。通过解构赋值为数组中的元素 a 和 b 指定默认值。

参数默认值的求值顺序

参数默认值的求值顺序是从左到右的,这意味着在为后面的参数计算默认值时,可以使用前面已经计算过的参数。

function greet(name = 'Guest', age = name === 'Guest' ? 18 : 30) {
  console.log(`Hello, ${name}! You are ${age} years old.`);
}

greet(); // 输出: "Hello, Guest! You are 18 years old."
greet('Alice'); // 输出: "Hello, Alice! You are 30 years old."

在上面的示例中,age 参数的默认值是根据 name 参数的值计算的。

常见用途

提供默认行为

默认参数常用于提供默认行为,避免在函数内部编写额外的逻辑来处理未定义的参数。

function makeRequest(url, method = 'GET') {
  console.log(`Making ${method} request to ${url}`);
}

makeRequest('https://api.example.com'); // 输出: "Making GET request to https://api.example.com"
makeRequest('https://api.example.com', 'POST'); // 输出: "Making POST request to https://api.example.com"

提供默认配置

默认参数常用于提供默认配置,允许调用者只传递需要修改的配置项。

function createUser({name = 'Guest', age = 18, isAdmin = false} = {}) {
  return {name, age, isAdmin};
}

console.log(createUser()); // 输出: { name: 'Guest', age: 18, isAdmin: false }
console.log(createUser({name: 'Alice'})); // 输出: { name: 'Alice', age: 18, isAdmin: false }
console.log(createUser({name: 'Bob', age: 25, isAdmin: true})); // 输出: { name: 'Bob', age: 25, isAdmin: true }

注意事项

 undefined 区别

默认参数只会在参数值为 undefined 时生效,而不是在参数值为 null 或其他假值时生效。

function test(value = 'default') {
  console.log(value);
}

test(); // 输出: "default"
test(undefined); // 输出: "default"
test(null); // 输出: null
test(''); // 输出: ""
test(0); // 输出: 0

参数默认值与严格模式

使用默认参数不会自动启用严格模式(use strict),但如果函数体中包含 use strict,则同样适用。

模块(Modules)

JavaScript 模块(Modules)是用于组织代码的一种机制,它允许开发者将代码分割成独立的文件,并通过导出和导入机制在文件之间共享代码。ECMAScript 2015 (ES6) 规范引入了官方的模块系统,称为 ES6 模块。模块化有助于提高代码的可维护性和可读性,并且促进代码的重用和分发。

基本概念

在 ES6 模块系统中,主要有两个关键操作:导出和导入。

  • 导出(Export):将模块内的某些变量、函数、类等导出,使它们可以在其他模块中使用。
  • 导入(Import):从其他模块中导入已导出的变量、函数、类等。

导出

命名导出(Named Export)

命名导出允许你导出多个变量、函数或类。可以在声明时使用 export 关键字,或者在声明后单独导出。

// module.js
export const name = 'Alice';
export const age = 25;

export function greet() {
  console.log(`Hello, my name is ${name}`);
}

// 或者
const name = 'Alice';
const age = 25;
function greet() {
  console.log(`Hello, my name is ${name}`);
}

export { name, age, greet };

默认导出(Default Export)

默认导出用于导出一个默认的值,通常是一个函数或类。每个模块只能有一个默认导出。

// module.js
export default function() {
  console.log('This is the default export');
}

// 或者
const defaultFunction = function() {
  console.log('This is the default export');
};

export default defaultFunction;

导入

导入命名导出

导入命名导出时,需要使用与导出时相同的名字。也可以使用 as 关键字重命名。

// main.js
import { name, age, greet } from './module.js';
console.log(name); // 输出: "Alice"
console.log(age); // 输出: 25
greet(); // 输出: "Hello, my name is Alice"

// 使用重命名
import { name as userName, age as userAge } from './module.js';
console.log(userName); // 输出: "Alice"
console.log(userAge); // 输出: 25
greet(); // 输出: "Hello, my name is Alice"

导入默认导出

导入默认导出时,可以使用任意名字。

// main.js
import defaultFunction from './module.js';
defaultFunction(); // 输出: "This is the default export"

导入所有导出

可以使用 * 关键字一次性导入所有导出,并将它们放在一个命名空间对象中。

// main.js
import * as module from './module.js';
console.log(module.name); // 输出: "Alice"
console.log(module.age); // 输出: 25
module.greet(); // 输出: "Hello, my name is Alice"

重新导出(Re-export)

模块还可以重新导出其他模块的导出。这对于创建模块的公共接口非常有用。

// module1.js
export const name = 'Alice';

// module2.js
export const age = 25;

// main.js
export { name } from './module1.js';
export { age } from './module2.js';

// app.js
import { name, age } from './main.js';
console.log(name); // 输出: "Alice"
console.log(age); // 输出: 25

动态导入(Dynamic Import)

JavaScript 的动态导入(Dynamic Import)是 ES2020 引入的特性,允许在运行时按需加载模块,这为代码分割和懒加载提供了强大的支持。在传统的静态导入(import)中,模块在编译时就被加载,而动态导入则是在代码运行时按需加载模块,使得应用的性能和加载时间得以优化。

基本语法

动态导入使用 import() 函数,它返回一个 Promise,该 Promise 解析为模块的导出对象。

import(moduleSpecifier)
  .then(module => {
    // 使用模块
  })
  .catch(error => {
    // 处理错误
  });

moduleSpecifier: 一个字符串,指定要导入的模块路径或名称。

基本示例

// main.js
document.getElementById('loadButton').addEventListener('click', () => {
  import('./module.js')
    .then(module => {
      module.doSomething();
    })
    .catch(error => {
      console.error('Error loading module:', error);
    });
});

// module.js
export function doSomething() {
  console.log('Module loaded and function executed.');
}

在这个示例中,当用户点击按钮时,module.js 会被动态导入。

实际应用场景

按需加载模块

动态导入非常适合在用户操作后按需加载模块,例如点击按钮、路由切换等。

// 假设我们有一个路由系统
function loadComponent(route) {
  switch (route) {
    case 'home':
      import('./home.js').then(module => {
        module.renderHome();
      });
      break;
    case 'about':
      import('./about.js').then(module => {
        module.renderAbout();
      });
      break;
    default:
      console.error('Unknown route:', route);
  }
}

// 使用示例
loadComponent('home');

懒加载

懒加载是一种优化策略,只有在需要时才加载资源,可以显著减少初始加载时间。

// main.js
function init() {
  document.getElementById('lazyLoadButton').addEventListener('click', () => {
    import('./largeModule.js').then(module => {
      module.init();
    });
  });
}

init();

// largeModule.js
export function init() {
  console.log('Large module loaded and initialized.');
}

结合异步函数

可以将动态导入与 async/await 结合使用,使得代码更加简洁和易读。

async function loadModule() {
  try {
    const module = await import('./module.js');
    module.doSomething();
  } catch (error) {
    console.error('Error loading module:', error);
  }
}

document.getElementById('loadButton').addEventListener('click', loadModule);

处理错误

动态导入返回的 Promise 可以捕获和处理导入模块时发生的错误。

import('./nonExistentModule.js')
  .then(module => {
    // 这个代码不会执行,因为模块不存在
  })
  .catch(error => {
    console.error('Failed to load module:', error);
  });

动态路径

动态导入中,模块路径可以是动态生成的,这为构建动态应用提供了极大的灵活性。

const moduleName = 'someModule';
import(`./modules/${moduleName}.js`)
  .then(module => {
    module.doSomething();
  })
  .catch(error => {
    console.error('Error loading module:', error);
  });

与 Webpack 的结合

动态导入与 Webpack 等模块打包工具结合,可以实现代码分割和懒加载。例如,在使用 Webpack 时,可以通过 import() 实现代码分割。

// Webpack 会为每个动态导入的模块创建一个单独的 chunk
import('./module.js')
  .then(module => {
    module.doSomething();
  });

Webpack 配置可能需要启用相关插件来支持动态导入和代码分割。

动态导入的注意事项

  • 浏览器支持:确保目标浏览器支持动态导入,现代浏览器大部分都已经支持,但在某些旧版浏览器中可能需要 polyfill。
  • 路径解析:动态导入的路径是相对当前文件的路径,如果路径错误会导致加载失败。
  • 性能考虑:动态导入可以减少初始加载时间,但频繁的动态导入可能会导致性能问题,需要合理规划。

复杂示例:多模块动态导入

假设我们有一个复杂的应用,需要根据用户的操作动态导入多个模块,并进行组合使用。

// main.js
async function loadMultipleModules() {
  try {
    const [moduleA, moduleB] = await Promise.all([
      import('./moduleA.js'),
      import('./moduleB.js')
    ]);
    moduleA.init();
    moduleB.init();
  } catch (error) {
    console.error('Error loading modules:', error);
  }
}

document.getElementById('loadButton').addEventListener('click', loadMultipleModules);

// moduleA.js
export function init() {
  console.log('Module A initialized.');
}

// moduleB.js
export function init() {
  console.log('Module B initialized.');
}

在这个示例中,我们使用 Promise.all 进行并行动态导入多个模块,并在全部加载完成后进行初始化操作。

模块的特性

  • 模块作用域:模块内声明的变量、函数、类等默认是模块私有的,除非显式导出。
  • 顶级作用域的this:在模块中,顶级作用域的 this 是 undefined。
  • 严格模式:模块默认运行在严格模式(strict mode)下。
  • 延迟加载:模块的代码只在首次导入时执行一次,并缓存结果,后续导入使用缓存。

使用场景

代码组织

模块化帮助组织代码,将相关功能分割到独立的文件中,使得代码更加清晰和易于维护。

// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// app.js
import { add, subtract } from './math.js';
console.log(add(2, 3)); // 输出: 5
console.log(subtract(5, 2)); // 输出: 3

代码重用

模块化促进代码重用,可以将常用的功能封装到模块中,在多个项目中共享使用。

// utils.js
export function formatDate(date) {
  return date.toISOString().split('T')[0];
}

// app.js
import { formatDate } from './utils.js';
const date = new Date();
console.log(formatDate(date)); // 输出: "2023-10-05"

动态加载

动态导入允许在需要时加载模块,优化应用的性能,特别是对于大型应用或按需加载的功能。

// app.js
document.getElementById('loadButton').addEventListener('click', async () => {
  const module = await import('./module.js');
  module.doSomething();
});

浏览器支持和打包工具

现代浏览器原生支持 ES6 模块,通过 <script type=”module”> 标签引入。

<!DOCTYPE html>
<html>
<head>
  <title>ES6 Modules</title>
</head>
<body>
  <script type="module" src="main.js"></script>
</body>
</html>

对于不支持 ES6 模块的旧浏览器,可以使用打包工具如 Webpack、Rollup 或 Parcel,将模块打包成兼容的代码。

类(Class)

JavaScript 中的类(Class)是 ECMAScript 2015 (ES6) 引入的一种语法糖,用于创建对象的模板。类的引入使得面向对象编程(OOP)在 JavaScript 中变得更加直观和易于理解。尽管底层实现仍然基于原型链,但类语法提供了更清晰、结构化的方式来定义和继承对象。

定义类

使用 class 关键字来定义一个类。一个类通常包含构造函数(constructor)、实例方法和静态方法。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // 实例方法
  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }

  // 静态方法
  static species() {
    return 'Homo sapiens';
  }
}

// 使用类
const alice = new Person('Alice', 25);
alice.greet(); // 输出: "Hello, my name is Alice and I am 25 years old."

console.log(Person.species()); // 输出: "Homo sapiens"

构造函数

构造函数是一个特殊的方法,用于初始化类的实例。在类中,constructor 方法会在新建对象时自动调用。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const bob = new Person('Bob', 30);
console.log(bob.name); // 输出: "Bob"
console.log(bob.age); // 输出: 30

实例方法

实例方法是定义在类的原型上的方法,可以通过类的实例调用。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

const alice = new Person('Alice', 25);
alice.greet(); // 输出: "Hello, my name is Alice and I am 25 years old."

静态方法

静态方法是定义在类本身上的方法,而不是在实例上。静态方法通常用于实现与类相关但不依赖于实例的数据和行为。

class MathUtils {
  static add(a, b) {
    return a + b;
  }
}

console.log(MathUtils.add(2, 3)); // 输出: 5

继承

通过 extends 关键字可以实现类的继承,从而创建一个子类。子类可以继承父类的属性和方法,并且可以重写父类的方法。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

class Student extends Person {
  constructor(name, age, grade) {
    super(name, age); // 调用父类的构造函数
    this.grade = grade;
  }

  study() {
    console.log(`${this.name} is studying in grade ${this.grade}.`);
  }

  // 重写父类的方法
  greet() {
    console.log(`Hi, I'm ${this.name}, a grade ${this.grade} student.`);
  }
}

const alice = new Student('Alice', 18, 12);
alice.greet(); // 输出: "Hi, I'm Alice, a grade 12 student."
alice.study(); // 输出: "Alice is studying in grade 12."

类的私有字段和方法

JavaScript 的私有字段和方法是 ES2022(ECMAScript 2022)引入的特性,用于在类定义中声明只能在类内部访问的成员。私有字段和方法通过 # 前缀来标识,提供了一种更强的封装和数据保护机制。

私有字段

私有字段是在类定义中以 # 开头的字段,表示这些字段只能在类的内部访问和修改,外部无法直接访问。

基本语法

class MyClass {
  #privateField = 'private value';

  getPrivateField() {
    return this.#privateField;
  }

  setPrivateField(value) {
    this.#privateField = value;
  }
}

const instance = new MyClass();
console.log(instance.getPrivateField()); // 输出: 'private value'
instance.setPrivateField('new value');
console.log(instance.getPrivateField()); // 输出: 'new value'

// 直接访问私有字段会导致错误
// console.log(instance.#privateField); // SyntaxError: Private field '#privateField' must be declared in an enclosing class

在上面的示例中,#privateField 是一个私有字段,只能在类的内部访问和修改,外部访问会导致语法错误。

私有方法

私有方法是在类定义中以 # 开头的方法,表示这些方法只能在类的内部调用,外部无法直接调用。

基本语法

class MyClass {
  #privateMethod() {
    return 'private method called';
  }

  callPrivateMethod() {
    return this.#privateMethod();
  }
}

const instance = new MyClass();
console.log(instance.callPrivateMethod()); // 输出: 'private method called'

// 直接调用私有方法会导致错误
// console.log(instance.#privateMethod()); // SyntaxError: Private method '#privateMethod' must be declared in an enclosing class

在上面的示例中,#privateMethod 是一个私有方法,只能在类的内部调用,外部调用会导致语法错误。

使用场景

私有字段和方法提供了更强的数据封装和保护机制,适用于需要保护内部状态和实现细节的场景。

封装内部状态

私有字段可以用于封装类的内部状态,使得外部无法直接修改,增强数据的安全性。

class Counter {
  #count = 0;

  increment() {
    this.#count++;
  }

  getCount() {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.getCount()); // 输出: 1

// 直接访问私有字段会导致错误
// console.log(counter.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class

隐藏实现细节

私有方法可以用于隐藏类的实现细节,使得外部无法直接调用类的内部方法。

class SecretKeeper {
  #secret = 'hidden message';

  revealSecret() {
    return this.#getSecret();
  }

  #getSecret() {
    return this.#secret;
  }
}

const keeper = new SecretKeeper();
console.log(keeper.revealSecret()); // 输出: 'hidden message'

// 直接调用私有方法会导致错误
// console.log(keeper.#getSecret()); // SyntaxError: Private method '#getSecret' must be declared in an enclosing class

组合使用私有字段和方法

在实际应用中,私有字段和方法通常组合使用,以实现复杂的封装和数据保护逻辑。

class BankAccount {
  #balance = 0;

  #validateAmount(amount) {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }
  }

  deposit(amount) {
    this.#validateAmount(amount);
    this.#balance += amount;
  }

  withdraw(amount) {
    this.#validateAmount(amount);
    if (amount > this.#balance) {
      throw new Error('Insufficient balance');
    }
    this.#balance -= amount;
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 输出: 100
account.withdraw(50);
console.log(account.getBalance()); // 输出: 50

// 直接访问私有字段和方法会导致错误
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
// console.log(account.#validateAmount(10)); // SyntaxError: Private method '#validateAmount' must be declared in an enclosing class

在上面的示例中,我们创建了一个银行账户类,通过私有字段 #balance 和私有方法 #validateAmount 来保护账户余额和验证金额。

私有字段和方法的注意事项

私有字段和方法的名称冲突

私有字段和方法以 # 开头,它们的名称是独立的,不会与普通字段和方法冲突。

class MyClass {
  #field = 'private';
  field = 'public';

  printFields() {
    console.log(this.#field); // 输出: 'private'
    console.log(this.field);  // 输出: 'public'
  }
}

const instance = new MyClass();
instance.printFields();

私有字段和方法的性能

私有字段和方法的访问有一定的性能开销,但在大多数情况下,这种开销是可以忽略不计的。

子类继承

私有字段和方法不能在子类中直接访问或重写。

class Parent {
  #privateField = 'parent';

  getPrivateField() {
    return this.#privateField;
  }
}

class Child extends Parent {
  // 不能直接访问或重写父类的私有字段
  // #privateField = 'child'; // SyntaxError
}

const child = new Child();
console.log(child.getPrivateField()); // 输出: 'parent'

访问限制

私有字段和方法只能在声明它们的类内部访问,外部访问会导致语法错误。

复杂示例:银行系统

假设我们要创建一个银行系统,其中包含账户和交易记录,需要保护账户的余额和交易记录的隐私。

class BankAccount {
  #balance = 0;
  #transactions = [];

  #recordTransaction(type, amount) {
    const date = new Date();
    this.#transactions.push({ type, amount, date });
  }

  deposit(amount) {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }
    this.#balance += amount;
    this.#recordTransaction('deposit', amount);
  }

  withdraw(amount) {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }
    if (amount > this.#balance) {
      throw new Error('Insufficient balance');
    }
    this.#balance -= amount;
    this.#recordTransaction('withdraw', amount);
  }

  getBalance() {
    return this.#balance;
  }

  getTransactions() {
    return this.#transactions.slice();
  }
}

const account = new BankAccount();
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 输出: 300
console.log(account.getTransactions()); // 输出: [{ type: 'deposit', amount: 500, date: ... }, { type: 'withdraw', amount: 200, date: ... }]

在这个示例中,我们创建了一个银行账户类,通过私有字段 #balance 和 #transactions 来保护账户余额和交易记录,并通过私有方法 #recordTransaction 来记录交易。

类字段

JavaScript 的类字段(Class Fields)是 ES2022(ECMAScript 2022)引入的一种语法,用于在类定义中声明和初始化类的实例字段或静态字段。类字段为类定义提供了更简洁和直观的语法,使得类的属性声明更加方便。

类字段的基本语法

实例字段

实例字段是每个类实例独有的字段,可以直接在类定义中声明和初始化。

class MyClass {
  myField = 'default value';

  constructor(value) {
    if (value) {
      this.myField = value;
    }
  }

  printField() {
    console.log(this.myField);
  }
}

const instance = new MyClass();
instance.printField(); // 输出: 'default value'

在上面的示例中,myField 是一个实例字段,它被初始化为 ‘default value’,然后可以在构造函数或类的其他方法中修改。

静态字段

静态字段是类本身而不是实例的属性,可以通过 static 关键字来声明。

class MyClass {
  static myStaticField = 'static value';

  static printStaticField() {
    console.log(this.myStaticField);
  }
}

MyClass.printStaticField(); // 输出: 'static value'

在上面的示例中,myStaticField 是一个静态字段,它被初始化为 ‘static value’,并且可以通过类本身访问。

使用示例

实例字段示例

实例字段在每个类的实例中都是独立的,可以用于存储与具体实例相关的数据。

class Person {
  name = 'Unnamed';
  age = 0;

  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  printInfo() {
    console.log(`Name: ${this.name}, Age: ${this.age}`);
  }
}

const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

person1.printInfo(); // 输出: Name: Alice, Age: 30
person2.printInfo(); // 输出: Name: Bob, Age: 25

静态字段示例

静态字段用于存储与类本身相关的数据,例如计数器、配置等。

class Counter {
  static count = 0;

  constructor() {
    Counter.count++;
  }

  static getCount() {
    return Counter.count;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
const counter3 = new Counter();

console.log(Counter.getCount()); // 输出: 3

初始化顺序

类字段的初始化顺序对于理解类的行为非常重要。类字段会在构造函数执行前初始化。

class MyClass {
  field1 = 'field1 value';
  field2;

  constructor(value) {
    this.field2 = value;
  }

  printFields() {
    console.log(`field1: ${this.field1}, field2: ${this.field2}`);
  }
}

const instance = new MyClass('field2 value');
instance.printFields(); // 输出: field1: field1 value, field2: field2 value

在上面的示例中,field1 在构造函数执行前被初始化,而 field2 在构造函数中被赋值。

类字段的优势

  • 简洁语法:类字段提供了一种更简洁和直观的方式来声明和初始化类的字段。
  • 私有字段:通过私有字段的支持,可以更好地封装数据,增强类的安全性和完整性。
  • 静态字段:静态字段使得与类相关的数据存储和访问更加方便。

注意事项

  • 浏览器支持:类字段是 ES2022 引入的特性,确保目标浏览器或 JavaScript 环境支持。如果需要在不支持的环境中使用,可以使用 Babel 等工具进行转译。
  • 私有字段:私有字段只能在声明它们的类内部访问,无法通过常规的对象方法访问或修改。

复杂示例:使用类字段构建复杂对象

假设我们要创建一个用户管理系统,其中用户类需要存储用户的基本信息、权限和私有数据。

class User {
  // 实例字段
  name;
  role;
  #privateData;

  // 静态字段
  static systemRole = 'USER';

  constructor(name, role) {
    this.name = name;
    this.role = role;
    this.#privateData = {};
  }

  // 实例方法
  setPrivateData(key, value) {
    this.#privateData[key] = value;
  }

  getPrivateData(key) {
    return this.#privateData[key];
  }

  // 静态方法
  static printSystemRole() {
    console.log(`System role: ${User.systemRole}`);
  }
}

// 创建用户实例
const user1 = new User('Alice', 'Admin');
const user2 = new User('Bob', 'User');

// 设置和获取私有数据
user1.setPrivateData('password', 'alice123');
console.log(user1.getPrivateData('password')); // 输出: alice123

// 尝试直接访问私有字段会导致错误
// console.log(user1.#privateData); // SyntaxError: Private field '#privateData' must be declared in an enclosing class

// 访问静态字段和方法
User.printSystemRole(); // 输出: System role: USER

在这个示例中,我们使用了实例字段来存储用户的基本信息,私有字段来存储敏感数据,静态字段来存储与类相关的全局信息。通过这种方式,我们能够更好地组织和管理类的属性和方法。

Getter 和 Setter

类中可以定义 getter 和 setter 方法,用于访问和修改属性值。getter 和 setter 方法看起来像属性,但实际上是方法。

class Person {
  constructor(name, age) {
    this._name = name;
    this._age = age;
  }

  get name() {
    return this._name;
  }

  set name(newName) {
    this._name = newName;
  }

  get age() {
    return this._age;
  }

  set age(newAge) {
    if (newAge > 0) {
      this._age = newAge;
    } else {
      console.log('Age must be positive');
    }
  }
}

const alice = new Person('Alice', 25);
console.log(alice.name); // 输出: "Alice"
alice.age = 30;
console.log(alice.age); // 输出: 30
alice.age = -5; // 输出: "Age must be positive"

使用场景

创建对象

类提供了一种简单的方式来创建对象,并初始化对象的属性。

class Car {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  }

  displayInfo() {
    console.log(`Car: ${this.year} ${this.make} ${this.model}`);
  }
}

const myCar = new Car('Toyota', 'Corolla', 2020);
myCar.displayInfo(); // 输出: "Car: 2020 Toyota Corolla"

继承和多态

继承和多态是面向对象编程的核心概念,通过类继承可以实现代码的复用和扩展。

class Animal {
  speak() {
    console.log('Animal is speaking...');
  }
}

class Dog extends Animal {
  speak() {
    console.log('Dog is barking...');
  }
}

const myPet = new Dog();
myPet.speak(); // 输出: "Dog is barking..."

封装

类提供了一种封装数据和行为的方式,通过私有字段和方法,可以隐藏实现细节,只公开必要的接口。

class BankAccount {
  #balance = 0;

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount();
account.deposit(100);
account.withdraw(50);
console.log(account.getBalance()); // 输出: 50

Symbol

Symbol 是 JavaScript 中的一种基本数据类型,它由 ES6(ECMAScript 2015)引入,用于创建唯一且不可变的标识符。Symbol 最常见的用途是作为对象的键,以避免属性名冲突。

基本语法

要创建一个 Symbol,可以使用 Symbol() 函数。每次调用 Symbol() 都会返回一个唯一的 Symbol 值。

const sym1 = Symbol();
const sym2 = Symbol();

console.log(sym1 === sym2); // 输出: false

可以为 Symbol 提供一个描述(description),这在调试时非常有用。

const sym = Symbol('mySymbol');
console.log(sym.description); // 输出: 'mySymbol'

作为对象的键

Symbol 可以用作对象的键,这使得属性名是唯一的,避免了与其他属性名冲突。使用 Symbol 作为键时,不能使用点(.)语法,只能使用方括号([])语法。

const sym = Symbol('mySymbol');
const obj = {
  [sym]: 'value'
};

console.log(obj[sym]); // 输出: 'value'

使用场景

避免属性名冲突

在大型代码库或使用第三方库时,可能会有很多不同的对象属性名称。使用 Symbol 可以确保属性名的唯一性,避免冲突。

const sym1 = Symbol('key');
const sym2 = Symbol('key');

const obj = {
  [sym1]: 'value1',
  [sym2]: 'value2'
};

console.log(obj[sym1]); // 输出: 'value1'
console.log(obj[sym2]); // 输出: 'value2'

模拟私有属性

虽然 Symbol 不能完全实现私有属性,但可以用于模拟某种程度的私有性,因为 Symbol 属性不会出现在常规的遍历操作中(例如 for…in、Object.keys 等)。

const sym = Symbol('private');

class MyClass {
  constructor() {
    this[sym] = 'private value';
  }

  getPrivate() {
    return this[sym];
  }
}

const instance = new MyClass();
console.log(instance.getPrivate()); // 输出: 'private value'
console.log(instance[sym]); // 输出: 'private value'
console.log(Object.keys(instance)); // 输出: []
console.log(Object.getOwnPropertyNames(instance)); // 输出: []

内置的 Symbol 值

JavaScript 提供了一些内置的 Symbol 值,这些 Symbol 是作为语言内部使用的,主要用于定义对象的某些行为。

Symbol.iterator

Symbol.iterator 定义了对象的默认迭代器方法,用于 for…of 循环。

const iterable = {
  [Symbol.iterator]() {
    let step = 0;
    return {
      next() {
        step++;
        if (step <= 3) {
          return { value: step, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (const value of iterable) {
  console.log(value); // 输出: 1, 2, 3
}

Symbol.asyncIterator

Symbol.asyncIterator 定义了对象的默认异步迭代器方法,用于 for await…of 循环。

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let step = 0;
    return {
      async next() {
        step++;
        if (step <= 3) {
          return { value: step, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // 输出: 1, 2, 3
  }
})();

Symbol.toStringTag

Symbol.toStringTag 定义了 Object.prototype.toString 方法返回的字符串标签,用于自定义对象的类型标签。

class MyClass {
  get [Symbol.toStringTag]() {
    return 'MyCustomClass';
  }
}

const instance = new MyClass();
console.log(Object.prototype.toString.call(instance)); // 输出: '[object MyCustomClass]'

Symbol.toPrimitive

Symbol.toPrimitive 定义了对象在转换为原始值时的行为。

const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'string') {
      return 'string value';
    }
    return 42;
  }
};

console.log(`${obj}`); // 输出: 'string value'
console.log(+obj);     // 输出: 42
console.log(obj + ''); // 输出: '42'

其他内置 Symbol

  • hasInstance: 定义instanceof 操作的行为。
  • isConcatSpreadable: 定义对象是否可以在数组concat 方法中展开。
  • match,Symbol.replace, Symbol.search, Symbol.split: 定义字符串匹配相关的方法。
  • species: 定义派生对象的构造函数。
  • unscopables: 定义with 环境中不可用的属性。

Symbol 的全局注册表

通过 Symbol.for 和 Symbol.keyFor 方法,可以在全局注册表中创建和查询 Symbol。

Symbol.for

Symbol.for 方法会在全局注册表中查找是否存在具有指定键的 Symbol。如果存在,则返回该 Symbol;如果不存在,则创建一个新的 Symbol 并在全局注册表中注册。

const sym1 = Symbol.for('shared');
const sym2 = Symbol.for('shared');

console.log(sym1 === sym2); // 输出: true

Symbol.keyFor

Symbol.keyFor 方法返回全局注册表中与指定 Symbol 关联的键。

const sym = Symbol.for('shared');
console.log(Symbol.keyFor(sym)); // 输出: 'shared'

注意事项

  • 唯一性:每个 Symbol 值都是唯一的,即使它们的描述相同。
  • 不可变性:Symbol 值是不可变的,一旦创建就不能修改。
  • 无法与字符串自动转换:Symbol 值不能自动转换为字符串,必须显式调用 .toString() 方法。
const sym = Symbol('mySymbol');
console.log(sym.toString()); // 输出: 'Symbol(mySymbol)'

Symbol.prototype.description

Symbol.prototype.description 是 ES2019(ECMAScript 2019)引入的特性,它提供了一种访问 Symbol 描述(description)属性的方式。每个 Symbol 可以在创建时附带一个可选的字符串描述,这个描述有助于调试和输出。

基本概念

  • Symbol:一种基本数据类型,用于创建唯一且不可变的标识符。
  • 描述(description):一个可选的字符串,用于描述Symbol,主要用于调试和日志输出。

Symbol.prototype.description 的基本语法

Symbol.prototype.description 是一个只读属性,它返回创建 Symbol 时提供的描述字符串(如果有的话),否则返回 undefined。

const sym1 = Symbol('mySymbol');
console.log(sym1.description); // 输出: 'mySymbol'

const sym2 = Symbol();
console.log(sym2.description); // 输出: undefined

示例

带描述的 Symbol

创建一个带描述的 Symbol,并访问其描述属性。

const sym = Symbol('example');
console.log(sym.description); // 输出: 'example'

不带描述的 Symbol

创建一个不带描述的 Symbol,并访问其描述属性。

const sym = Symbol();
console.log(sym.description); // 输出: undefined

全局注册的 Symbol

使用 Symbol.for 创建一个全局注册的 Symbol,并访问其描述属性。

const globalSym = Symbol.for('globalExample');
console.log(globalSym.description); // 输出: 'globalExample'

实际应用场景

调试和日志输出

Symbol 的描述可以在调试和日志输出时提供有用的信息,帮助开发者更容易地理解代码的运行情况。

const sym = Symbol('debugSymbol');
console.log(`Symbol description: ${sym.description}`); // 输出: Symbol description: debugSymbol

区分不同的 Symbol

在复杂的应用中,可能会创建多个 Symbol。使用描述可以帮助区分这些 Symbol 的用途。

const sym1 = Symbol('userID');
const sym2 = Symbol('orderID');

console.log(sym1.description); // 输出: 'userID'
console.log(sym2.description); // 输出: 'orderID'

与toString 的区别

在 Symbol.prototype.description 引入之前,获取 Symbol 描述的常用方法是使用 Symbol.prototype.toString。然而,toString 返回的字符串包含了 Symbol 类型的前缀,而 description 只返回描述部分。

const sym = Symbol('example');

console.log(sym.toString()); // 输出: 'Symbol(example)'
console.log(sym.description); // 输出: 'example'

复杂示例:使用 Symbol 作为对象的键

结合 Symbol 的唯一性和描述属性,我们可以创建更易于调试和维护的对象属性。

const sym1 = Symbol('property1');
const sym2 = Symbol('property2');

const obj = {
  [sym1]: 'value1',
  [sym2]: 'value2'
};

console.log(obj[sym1]); // 输出: 'value1'
console.log(obj[sym2]); // 输出: 'value2'

// 打印 Symbol 描述
for (const key of Object.getOwnPropertySymbols(obj)) {
  console.log(`${key.description}: ${obj[key]}`);
}
// 输出:
// property1: value1
// property2: value2

在这个示例中,我们使用 Symbol 作为对象的键,并通过 description 属性帮助理解和调试对象中的 Symbol 键。

注意事项

  • 只读属性:prototype.description是一个只读属性,无法修改。
  • 旧版浏览器支持:prototype.description是 ES2019 引入的特性,请确保目标浏览器或 JavaScript 环境支持。如果需要在不支持的环境中使用,可以使用 polyfill。

异步编程Promise

JavaScript 的 Promise 是一种用于异步编程的对象,它代表了一个异步操作的最终完成(或失败)及其结果值。Promise 提供了一种更为简洁和直观的方式来处理异步操作,避免了回调地狱(callback hell)的问题。Promise 是 ES6(ECMAScript 2015)引入的,现已成为现代 JavaScript 异步编程的核心。

Promise 的基础

创建 Promise

要创建一个 Promise 对象,可以使用 new Promise 构造函数。构造函数接收一个函数(执行函数)作为参数,该函数有两个参数:resolve 和 reject。resolve 用于在异步操作成功时返回结果,reject 用于在异步操作失败时返回错误。

const promise = new Promise((resolve, reject) => {
  // 一些异步操作
  const success = true; // 假设这是异步操作的结果

  if (success) {
    resolve('Operation succeeded');
  } else {
    reject('Operation failed');
  }
});

Promise 的状态

一个 Promise 对象有三种状态:

  • Pending(待定):初始状态,既没有被兑现,也没有被拒绝。
  • Fulfilled(已兑现):操作成功完成。
  • Rejected(已拒绝):操作失败。

状态一旦从 Pending 转变为 Fulfilled 或 Rejected,就不能再变化。

使用 then、catch 和 finally

then 方法

then 方法用于指定在 Promise 被兑现或拒绝后要执行的回调函数。它接收两个参数:第一个是 Fulfilled 状态的回调,第二个是 Rejected 状态的回调。

promise.then(
  (result) => {
    console.log(result); // 输出: 'Operation succeeded'
  },
  (error) => {
    console.log(error);
  }
);

catch 方法

catch 方法用于指定在 Promise 被拒绝后要执行的回调函数。它是 then 方法的简写形式,只接收一个参数,即 Rejected 状态的回调。

promise.catch((error) => {
  console.log(error); // 输出: 'Operation failed'
});

finally 方法

finally 方法用于指定无论 Promise 最终状态如何(兑现或拒绝),都要执行的回调函数。

promise.finally(() => {
  console.log('Operation completed');
});

链式调用

Promise 的 then 方法返回一个新的 Promise,因此可以进行链式调用。链式调用使得处理一系列异步操作变得更为简洁和直观。

new Promise((resolve) => {
  setTimeout(() => resolve(1), 1000);
})
  .then((result) => {
    console.log(result); // 输出: 1
    return result * 2;
  })
  .then((result) => {
    console.log(result); // 输出: 2
    return result * 3;
  })
  .then((result) => {
    console.log(result); // 输出: 6
  });

Promise 的静态方法

Promise.resolve

Promise.resolve 方法返回一个以给定值解析后的 Promise。

Promise.resolve('Success').then((result) => {
  console.log(result); // 输出: 'Success'
});

Promise.reject

Promise.reject 方法返回一个以给定错误理由拒绝的 Promise。

Promise.reject('Error').catch((error) => {
  console.log(error); // 输出: 'Error'
});

Promise.all

Promise.all 方法接收一个 Promise 可迭代对象,返回一个新的 Promise。该 Promise 在所有输入的 Promise 都成功时解析,并带有一个数组包含这些 Promise 的结果;如果有任一 Promise 失败,则以第一个失败的 Promise 的错误原因拒绝。

const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3]).then((results) => {
  console.log(results); // 输出: [1, 2, 3]
});

Promise.race

Promise.race 方法接收一个 Promise 可迭代对象,返回一个新的 Promise。该 Promise 在输入的任一 Promise 解决或拒绝时就会解决或拒绝,并带有那个 Promise 的值或错误原因。

const promise1 = new Promise((resolve) => setTimeout(() => resolve('First'), 500));
const promise2 = new Promise((resolve) => setTimeout(() => resolve('Second'), 100));

Promise.race([promise1, promise2]).then((result) => {
  console.log(result); // 输出: 'Second'
});

Promise.allSettled

Promise.allSettled 是 JavaScript 中处理多个异步操作的一个强大工具。它接收一个可迭代对象(例如数组)作为参数,该对象包含多个 Promise,并返回一个新的 Promise。新的 Promise 会在所有输入的 Promise 都已经解决(fulfilled)或拒绝(rejected)后解析,并解析成一个数组对象。数组中的每个对象表示对应的 Promise 的结果,包括其状态和值。

const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('Error');
const promise3 = Promise.resolve(3);

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
  console.log(results);
  // 输出:
  // [
  //   { status: 'fulfilled', value: 1 },
  //   { status: 'rejected', reason: 'Error' },
  //   { status: 'fulfilled', value: 3 }
  // ]
});

Promise.prototype.finally

Promise.prototype.finally 是 ES2018(ES9)引入的一个方法,添加到 Promise 对象原型上,用于在 Promise 对象完成(无论是成功还是失败)时执行指定的回调函数。这个方法非常适合用于那些无论 Promise 成功还是失败都需要执行的清理操作,例如关闭加载动画、清理资源等。

基本语法

promise.finally(onFinally);
  • onFinally: 一个在Promise 结束时(无论是 fulfilled 还是 rejected)执行的回调函数。该函数不接受任何参数。

用法示例

基本用法

finally 方法可以用于执行一些清理操作,无论 Promise 是成功还是失败。

const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    resolve('Success'); // 或者 reject('Error');
  }, 1000);
});

myPromise
  .then(result => {
    console.log(result); // 输出: 'Success'
  })
  .catch(error => {
    console.error(error);
  })
  .finally(() => {
    console.log('Completed'); // 无论成功或失败都输出: 'Completed'
  });

用于清理操作

假设我们有一个异步操作需要显示加载动画,当操作完成时,无论成功还是失败,都需要关闭动画。

function showLoading() {
  console.log('Loading...');
}

function hideLoading() {
  console.log('Loading finished.');
}

const fetchData = new Promise((resolve, reject) => {
  showLoading();
  setTimeout(() => {
    resolve('Data fetched'); // 或者 reject('Error fetching data');
  }, 2000);
});

fetchData
  .then(result => {
    console.log(result); // 输出: 'Data fetched'
  })
  .catch(error => {
    console.error(error);
  })
  .finally(() => {
    hideLoading(); // 无论成功或失败都输出: 'Loading finished.'
  });

处理链式 Promise

finally 方法不会改变 Promise 的状态和返回值,它会将相同的值传递给后面的 then 或 catch 方法。

const promise = new Promise((resolve, reject) => {
  resolve('Initial success');
});

promise
  .then(result => {
    console.log(result); // 输出: 'Initial success'
    return 'Next success';
  })
  .finally(() => {
    console.log('First finally');
  })
  .then(result => {
    console.log(result); // 输出: 'Next success'
  })
  .finally(() => {
    console.log('Second finally');
  });

在上面的示例中,finally 方法只是执行了一些操作,但不会改变 Promise 链中传递的值。

处理错误的示例

finally 方法适用于在 Promise 链中处理错误时执行一些清理操作,而不会影响错误的传递。

const promise = new Promise((resolve, reject) => {
  reject('Initial error');
});

promise
  .catch(error => {
    console.error(error); // 输出: 'Initial error'
    throw new Error('Next error');
  })
  .finally(() => {
    console.log('First finally');
  })
  .catch(error => {
    console.error(error); // 输出: 'Next error'
  })
  .finally(() => {
    console.log('Second finally');
  });

在上面的示例中,finally 方法在每个 catch 之后执行,但不会终止错误的传递。

与 then 和 catch 的不同之处

  • then方法用于处理 fulfilled 状态,并可以对状态进行转换或传递。
  • catch方法用于处理 rejected 状态,并可以对状态进行转换或传递。
  • finally方法用于处理无论 Promise 的状态是 fulfilled 还是 rejected,都需要执行的清理操作,不会对状态进行转换或传递。

注意事项

  • 不改变Promise 的状态:finally 方法不会改变 Promise 的状态或返回值,只会执行指定的回调函数。
  • 跨浏览器兼容性:finally方法是 ES2018 引入的,需要确保目标环境支持。如果需要在不支持 finally 的环境中使用,可以使用 polyfill。

复杂示例:文件操作

假设我们有一个文件操作,需要在操作完成后关闭文件描述符,无论操作是成功还是失败。

const fs = require('fs').promises;

async function readFile(filePath) {
  let fileHandle;
  try {
    fileHandle = await fs.open(filePath, 'r');
    const content = await fileHandle.readFile('utf-8');
    console.log(content);
  } catch (error) {
    console.error('Error reading file:', error);
  } finally {
    if (fileHandle) {
      await fileHandle.close();
      console.log('File closed');
    }
  }
}

readFile('./example.txt');

在上面的示例中,我们使用 finally 方法确保无论文件读取操作成功还是失败,都能正确关闭文件描述符。

Promise.allSettled

Promise.prototype.finally 是 ES2018(ES9)引入的一个方法,添加到 Promise 对象原型上,用于在 Promise 对象完成(无论是成功还是失败)时执行指定的回调函数。这个方法非常适合用于那些无论 Promise 成功还是失败都需要执行的清理操作,例如关闭加载动画、清理资源等。

基本语法

promise.finally(onFinally);
  • onFinally: 一个在Promise 结束时(无论是 fulfilled 还是 rejected)执行的回调函数。该函数不接受任何参数。

用法示例

基本用法

finally 方法可以用于执行一些清理操作,无论 Promise 是成功还是失败。

const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    resolve('Success'); // 或者 reject('Error');
  }, 1000);
});

myPromise
  .then(result => {
    console.log(result); // 输出: 'Success'
  })
  .catch(error => {
    console.error(error);
  })
  .finally(() => {
    console.log('Completed'); // 无论成功或失败都输出: 'Completed'
  });

用于清理操作

假设我们有一个异步操作需要显示加载动画,当操作完成时,无论成功还是失败,都需要关闭动画。

function showLoading() {
  console.log('Loading...');
}

function hideLoading() {
  console.log('Loading finished.');
}

const fetchData = new Promise((resolve, reject) => {
  showLoading();
  setTimeout(() => {
    resolve('Data fetched'); // 或者 reject('Error fetching data');
  }, 2000);
});

fetchData
  .then(result => {
    console.log(result); // 输出: 'Data fetched'
  })
  .catch(error => {
    console.error(error);
  })
  .finally(() => {
    hideLoading(); // 无论成功或失败都输出: 'Loading finished.'
  });

处理链式 Promise

finally 方法不会改变 Promise 的状态和返回值,它会将相同的值传递给后面的 then 或 catch 方法。

const promise = new Promise((resolve, reject) => {
  resolve('Initial success');
});

promise
  .then(result => {
    console.log(result); // 输出: 'Initial success'
    return 'Next success';
  })
  .finally(() => {
    console.log('First finally');
  })
  .then(result => {
    console.log(result); // 输出: 'Next success'
  })
  .finally(() => {
    console.log('Second finally');
  });

在上面的示例中,finally 方法只是执行了一些操作,但不会改变 Promise 链中传递的值。

处理错误的示例

finally 方法适用于在 Promise 链中处理错误时执行一些清理操作,而不会影响错误的传递。

const promise = new Promise((resolve, reject) => {
  reject('Initial error');
});

promise
  .catch(error => {
    console.error(error); // 输出: 'Initial error'
    throw new Error('Next error');
  })
  .finally(() => {
    console.log('First finally');
  })
  .catch(error => {
    console.error(error); // 输出: 'Next error'
  })
  .finally(() => {
    console.log('Second finally');
  });

在上面的示例中,finally 方法在每个 catch 之后执行,但不会终止错误的传递。

与 then 和 catch 的不同之处

  • then方法用于处理 fulfilled 状态,并可以对状态进行转换或传递。
  • catch方法用于处理 rejected 状态,并可以对状态进行转换或传递。
  • finally方法用于处理无论 Promise 的状态是 fulfilled 还是 rejected,都需要执行的清理操作,不会对状态进行转换或传递。

注意事项

  • 不改变Promise 的状态:finally 方法不会改变 Promise 的状态或返回值,只会执行指定的回调函数。
  • 跨浏览器兼容性:finally方法是 ES2018 引入的,需要确保目标环境支持。如果需要在不支持 finally 的环境中使用,可以使用 polyfill。

复杂示例:文件操作

假设我们有一个文件操作,需要在操作完成后关闭文件描述符,无论操作是成功还是失败。

const fs = require('fs').promises;

async function readFile(filePath) {
  let fileHandle;
  try {
    fileHandle = await fs.open(filePath, 'r');
    const content = await fileHandle.readFile('utf-8');
    console.log(content);
  } catch (error) {
    console.error('Error reading file:', error);
  } finally {
    if (fileHandle) {
      await fileHandle.close();
      console.log('File closed');
    }
  }
}

readFile('./example.txt');

在上面的示例中,我们使用 finally 方法确保无论文件读取操作成功还是失败,都能正确关闭文件描述符。

Promise.any

Promise.any 是 ES2021(ECMAScript 2021)引入的一种新的 Promise 组合方法,用于处理一组 Promise。它返回一个 Promise,当任意一个 Promise 成功(即状态变为 fulfilled)时,就会返回这个成功的值。如果所有的 Promise 都被拒绝(即状态变为 rejected),则返回一个包含所有拒绝原因的 AggregateError。

基本语法

Promise.any(iterable);
  • iterable: 一个可迭代对象(例如数组),其中包含多个Promise 实例。

Promise.any 返回一个新的 Promise,该 Promise 在任意一个传入的 Promise 成功时解析,解析值是第一个成功的 Promise 的值。如果所有传入的 Promise 都被拒绝,则返回一个 AggregateError 对象,包含所有拒绝的原因。

用法示例

基本示例

const promise1 = Promise.reject('Error 1');
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success 2'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 200, 'Success 3'));

Promise.any([promise1, promise2, promise3])
  .then((value) => {
    console.log('Resolved with value:', value); // 输出: Resolved with value: Success 2
  })
  .catch((error) => {
    console.error('Rejected with error:', error);
  });

在这个示例中,promise2 是第一个成功的 Promise,所以 Promise.any 返回 promise2 的值,即 Success 2。

处理所有 Promise 都被拒绝的情况

如果所有传入的 Promise 都被拒绝,Promise.any 返回一个 AggregateError,包含所有拒绝的原因。

const promise1 = Promise.reject('Error 1');
const promise2 = Promise.reject('Error 2');
const promise3 = Promise.reject('Error 3');

Promise.any([promise1, promise2, promise3])
  .then((value) => {
    console.log('Resolved with value:', value);
  })
  .catch((error) => {
    console.error('Rejected with error:', error);
    console.error('All errors:', error.errors);
  });

输出:

Rejected with error: AggregateError: All promises were rejected
All errors: [ 'Error 1', 'Error 2', 'Error 3' ]

实际应用场景

快速响应策略

在 Web 开发中,可能会有多个备选 API 服务提供相同的数据。可以使用 Promise.any 来快速获取第一个成功响应的数据,从而提高用户体验。

const fetchFromService1 = fetch('https://api.service1.com/data');
const fetchFromService2 = fetch('https://api.service2.com/data');
const fetchFromService3 = fetch('https://api.service3.com/data');

Promise.any([fetchFromService1, fetchFromService2, fetchFromService3])
  .then((response) => response.json())
  .then((data) => {
    console.log('Received data:', data);
  })
  .catch((error) => {
    console.error('All services failed:', error);
  });

备用任务

在任务系统中,可以有多个备用任务来完成同一个目标。使用 Promise.any 可以确保至少有一个任务成功完成。

function task1() {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, 'Task 1 failed');
  });
}

function task2() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 200, 'Task 2 succeeded');
  });
}

function task3() {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 300, 'Task 3 succeeded');
  });
}

Promise.any([task1(), task2(), task3()])
  .then((result) => {
    console.log('First successful task:', result); // 输出: First successful task: Task 2 succeeded
  })
  .catch((error) => {
    console.error('All tasks failed:', error);
  });

与其他 Promise 方法的区别

  • all: 等待所有Promise 全部成功,如果有一个失败,则立即返回失败。
  • allSettled: 等待所有Promise 全部完成,无论成功还是失败,返回每个 Promise 的结果。
  • race: 返回第一个完成的Promise,无论是成功还是失败。
  • any: 返回第一个成功的Promise,如果所有 Promise 都失败,则返回 AggregateError。

复杂示例:搜索引擎聚合

假设我们要实现一个搜索引擎聚合器,从多个搜索引擎获取搜索结果,并返回第一个成功的结果。

function searchEngine1(query) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject('Engine 1 failed'), 100);
  });
}

function searchEngine2(query) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Engine 2 results for "${query}"`), 200);
  });
}

function searchEngine3(query) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Engine 3 results for "${query}"`), 300);
  });
}

function search(query) {
  return Promise.any([
    searchEngine1(query),
    searchEngine2(query),
    searchEngine3(query)
  ]);
}

search('JavaScript Promises')
  .then((result) => {
    console.log('Search result:', result); // 输出: Search result: Engine 2 results for "JavaScript Promises"
  })
  .catch((error) => {
    console.error('All search engines failed:', error);
  });

在这个示例中,我们定义了三个模拟的搜索引擎函数 searchEngine1、searchEngine2 和 searchEngine3,并通过 Promise.any 聚合搜索结果,返回第一个成功的结果。

注意事项

  • 浏览器支持:any是 ES2021 引入的特性,确保目标浏览器或 JavaScript 环境支持。如果需要在不支持的环境中使用,可以使用 polyfill。
  • 错误处理:当所有Promise 都被拒绝时,any 返回一个 AggregateError,需要处理该错误类型。
  • 性能考虑:any会并行执行所有传入的 Promise,因此传入的 Promise 数量过多可能会对性能产生影响。

异步函数(async/await)

async 和 await 是 JavaScript 中用于处理异步操作的语法糖,它们基于 Promise,但提供了一种更为直观和简洁的方式来编写异步代码。async/await 是 ES2017(ES8)引入的特性,使得异步代码看起来更像是同步代码,极大地提高了代码的可读性和可维护性。

async 关键字

async 关键字用于定义一个异步函数。一个异步函数总是返回一个 Promise。如果函数显式返回一个值,这个值会被自动包装成 Promise.resolve(value)。如果函数抛出异常,这个异常会被包装成 Promise.reject(error)。

定义异步函数

async function fetchData() {
  return 'Data';
}

fetchData().then((result) => {
  console.log(result); // 输出: 'Data'
});

在上面的示例中,fetchData 函数总是返回一个 Promise,即使函数体内返回的是一个普通值。

await 关键字

await 关键字只能在 async 函数内部使用。它用于等待一个 Promise 解决,并返回 Promise 的结果。如果 Promise 被拒绝,await 会抛出错误。

基本用法

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function asyncFunction() {
  console.log('Waiting...');
  await delay(2000);
  console.log('Done!');
}

asyncFunction();

在上面的示例中,await 关键字用于等待 delay 函数返回的 Promise 被解决,然后再继续执行后面的代码。

错误处理

使用 async/await 语法时,可以使用 try/catch 块来捕获异步操作中的错误。

async function fetchData() {
  try {
    const result = await Promise.reject('Error');
    console.log(result); // 不会执行
  } catch (error) {
    console.log(error); // 输出: 'Error'
  }
}

fetchData();

在上面的示例中,await 关键字等待的 Promise 被拒绝,因此程序会跳转到 catch 块,并处理错误。

串行执行多个异步操作

async/await 使得串行执行多个异步操作变得更加简单和直观。

async function fetchData() {
  const result1 = await Promise.resolve('Data 1');
  console.log(result1); // 输出: 'Data 1'

  const result2 = await Promise.resolve('Data 2');
  console.log(result2); // 输出: 'Data 2'
}

fetchData();

在上面的示例中,result1 和 result2 将按顺序依次输出。

并行执行多个异步操作

有时你可能希望并行执行多个异步操作,而不是一个接一个地执行。这时可以使用 Promise.all。

async function fetchData() {
  const [result1, result2] = await Promise.all([
    Promise.resolve('Data 1'),
    Promise.resolve('Data 2')
  ]);

  console.log(result1); // 输出: 'Data 1'
  console.log(result2); // 输出: 'Data 2'
}

fetchData();

在上面的示例中,result1 和 result2 是并行执行的,所有 Promise 都解决后才继续执行后续代码。

顶级 await

顶级 await 是 ECMAScript 2022 (ES2022) 引入的一项新特性,它允许在模块的顶级作用域中使用 await 关键字。这使得处理异步操作变得更加方便,无需再将所有逻辑包装在异步函数中。

顶级 await 基本概念

在传统的 JavaScript 脚本(非模块)中,await 关键字只能在 async 函数中使用。然而,在模块环境下(通过 <script type=”module”> 引入的脚本文件,或者使用 import/export 语法的文件),可以在顶级作用域直接使用 await,这被称为顶级 await。顶级 await 在某些情况下可以简化代码结构,尤其是在模块初始化时需要进行异步操作时非常有用。

使用场景

异步数据加载

在模块加载时直接获取数据,而无需包裹在 async 函数中。

// module.js
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);

在上面的示例中,fetch 和 response.json 都是异步操作,但我们可以在顶级作用域中直接使用 await,从而简化代码结构。

动态导入

在模块中直接进行动态导入并等待其结果。

// module.js
const moduleA = await import('./moduleA.js');
moduleA.doSomething();

初始化逻辑

一些初始化逻辑可能需要异步操作,顶级 await 使得这些逻辑可以直接在模块中进行,而不需要包裹在函数中。

// module.js
const dbConnection = await initializeDatabase();
console.log('Database initialized:', dbConnection);

与传统方式对比

在引入顶级 await 之前,必须将异步代码包裹在函数中:

// before-top-level-await.js
async function initialize() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
}

initialize();

引入顶级 await 后,代码变得更加简洁:

// with-top-level-await.js
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);

顶级 await 的影响

模块依赖的顺序

顶级 await 会影响模块的依赖顺序。如果一个模块使用了顶级 await,它的依赖模块将等待该模块的顶级 await 完成后再开始执行。

// moduleA.js
console.log('Module A start');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Module A end');

// moduleB.js
import './moduleA.js';
console.log('Module B');

在上面的示例中,moduleB 会等待 moduleA 中的顶级 await 操作完成后再执行。

错误处理

使用顶级 await 时,错误会在顶级作用域中抛出,需要确保在模块加载时处理这些错误。

// module.js
try {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error('Failed to fetch data:', error);
}

浏览器支持

顶级 await 是相对较新的特性,并不是所有浏览器都支持。因此在使用之前需要确认目标环境的兼容性。

实际应用示例

配置加载

在应用启动时加载配置文件:

// config.js
export const config = await fetch('/config.json').then((response) => response.json());

在其他模块中直接使用加载的配置:

// app.js
import { config } from './config.js';
console.log('Configuration:', config);

动态模块加载

根据某些条件动态导入模块:

// dynamic-import.js
const moduleName = someCondition ? './moduleA.js' : './moduleB.js';
const module = await import(moduleName);
module.doSomething();

注意事项

顶级 await 的引入增加了加载的复杂性

顶级 await 会影响模块的加载顺序,可能会导致复杂性增加,尤其是在多个模块相互依赖的情况下。

兼容性问题

虽然顶级 await 提供了很大的便利,但需要确保目标环境支持这一特性。

实际应用场景

async/await 在处理复杂的异步逻辑时非常有用,以下是一些实际应用场景:

处理异步请求

async function getUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

getUserData(123).then((data) => {
  console.log(data);
});

顺序执行多个异步操作

async function processMultipleSteps() {
  try {
    const step1 = await performStep1();
    console.log('Step 1 completed:', step1);

    const step2 = await performStep2(step1);
    console.log('Step 2 completed:', step2);

    const step3 = await performStep3(step2);
    console.log('Step 3 completed:', step3);
  } catch (error) {
    console.error('Error encountered:', error);
  }
}

processMultipleSteps();

处理并发操作

async function fetchAllData() {
  try {
    const [data1, data2, data3] = await Promise.all([
      fetchData1(),
      fetchData2(),
      fetchData3()
    ]);
    console.log('Data 1:', data1);
    console.log('Data 2:', data2);
    console.log('Data 3:', data3);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

fetchAllData();

生成器(Generators)

JavaScript 的生成器(Generator)是 ES6 引入的一种特殊的函数,它可以暂停执行并在之后恢复执行,从而使得函数的执行可以被控制。生成器函数返回一个生成器对象,这个对象可以用于逐步迭代产生的值。生成器在异步编程、迭代器实现等场景中非常有用。

基本概念

定义生成器函数

生成器函数使用 function* 关键字定义,函数体内部使用 yield 关键字来暂停函数执行并返回一个值。

function* generatorFunction() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = generatorFunction();

生成器对象

生成器函数返回一个生成器对象,这个对象实现了迭代器协议。可以使用 next 方法逐步迭代生成器产生的值。

const generator = generatorFunction();

console.log(generator.next()); // 输出: { value: 1, done: false }
console.log(generator.next()); // 输出: { value: 2, done: false }
console.log(generator.next()); // 输出: { value: 3, done: false }
console.log(generator.next()); // 输出: { value: undefined, done: true }

生成器的特性

yield 表达式

yield 关键字用于暂停生成器函数的执行,并返回一个值。每次调用 next 方法时,生成器函数从上次暂停的地方继续执行。

function* generatorFunction() {
  console.log('Start');
  yield 1;
  console.log('After first yield');
  yield 2;
  console.log('After second yield');
  return 3;
}

const generator = generatorFunction();

console.log(generator.next()); // 输出: "Start" 和 { value: 1, done: false }
console.log(generator.next()); // 输出: "After first yield" 和 { value: 2, done: false }
console.log(generator.next()); // 输出: "After second yield" 和 { value: 3, done: true }

传值给生成器

可以在 next 方法中传入一个参数,这个参数会作为上一个 yield 表达式的返回值。

function* generatorFunction() {
  const value1 = yield 1;
  console.log('value1:', value1);

  const value2 = yield 2;
  console.log('value2:', value2);
}

const generator = generatorFunction();

console.log(generator.next()); // 输出: { value: 1, done: false }
console.log(generator.next('a')); // 输出: "value1: a" 和 { value: 2, done: false }
console.log(generator.next('b')); // 输出: "value2: b" 和 { value: undefined, done: true }

生成器的返回值

生成器函数可以显式返回一个值,这个值会在 done 为 true 时作为 value 返回。

function* generatorFunction() {
  yield 1;
  yield 2;
  return 3;
}

const generator = generatorFunction();

console.log(generator.next()); // 输出: { value: 1, done: false }
console.log(generator.next()); // 输出: { value: 2, done: false }
console.log(generator.next()); // 输出: { value: 3, done: true }

异步编程与生成器

生成器可以与 Promise 结合使用,以线性的方式处理异步代码。这种方式常常被称为“生成器驱动的协程”。

function* asyncGenerator() {
  const data1 = yield fetchData1();
  console.log('data1:', data1);

  const data2 = yield fetchData2();
  console.log('data2:', data2);
}

function fetchData1() {
  return new Promise((resolve) => setTimeout(() => resolve('Data 1'), 1000));
}

function fetchData2() {
  return new Promise((resolve) => setTimeout(() => resolve('Data 2'), 1000));
}

function runGenerator(gen) {
  const generator = gen();

  function handle(result) {
    if (result.done) return;

    const value = result.value;
    if (value instanceof Promise) {
      value.then((res) => handle(generator.next(res)));
    } else {
      handle(generator.next(value));
    }
  }

  handle(generator.next());
}

runGenerator(asyncGenerator);

在上面的示例中,runGenerator 函数用于执行生成器驱动的协程,通过递归处理 Promise 来逐步执行生成器。

生成器与迭代器

生成器函数返回的生成器对象实现了迭代器协议,因此生成器可以用于 for…of 循环等语法中。

function* generatorFunction() {
  yield 1;
  yield 2;
  yield 3;
}

for (const value of generatorFunction()) {
  console.log(value); // 输出: 1, 2, 3
}

生成器与 Symbol.iterator

生成器函数也可以用于实现可迭代对象,因为生成器对象实现了 Symbol.iterator 方法。

const iterableObject = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};

for (const value of iterableObject) {
  console.log(value); // 输出: 1, 2, 3
}

生成器方法

生成器对象除了有 next 方法外,还有 return 和 throw 方法。

return 方法

return 方法用于提前终止生成器,并返回一个指定的值。

function* generatorFunction() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = generatorFunction();

console.log(generator.next()); // 输出: { value: 1, done: false }
console.log(generator.return('End')); // 输出: { value: 'End', done: true }

throw 方法

throw 方法用于在生成器函数内部抛出错误,并可以在生成器函数中捕获。

function* generatorFunction() {
  try {
    yield 1;
  } catch (error) {
    console.log('Caught:', error);
  }
  yield 2;
}

const generator = generatorFunction();

console.log(generator.next()); // 输出: { value: 1, done: false }
console.log(generator.throw('Error')); // 输出: "Caught: Error" 和 { value: 2, done: false }

实际应用场景

数据流处理

生成器可以用于处理流式数据,如文件读取、网络请求等。

function* dataStream() {
  yield 'chunk1';
  yield 'chunk2';
  yield 'chunk3';
}

for (const chunk of dataStream()) {
  console.log(chunk); // 输出: chunk1, chunk2, chunk3
}

无限序列

生成器可以用于创建无限序列,如斐波那契数列。

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 输出: 0
console.log(fib.next().value); // 输出: 1
console.log(fib.next().value); // 输出: 1
console.log(fib.next().value); // 输出: 2
console.log(fib.next().value); // 输出: 3

异步控制流

结合 Promise 和生成器,可以实现异步代码的线性控制流,从而提高代码的可读性。

for…of 循环

for…of 循环是 ES6(ECMAScript 2015)引入的一种遍历迭代对象的结构,它提供了一种简洁且易于理解的方式来遍历所有可迭代对象(如数组、字符串、Map、Set 等)。与 for…in 循环不同,for…of 直接获取对象的值而不是键。

基本语法

for…of 循环的基本语法如下:

for (const variable of iterable) {
  // 循环体
}
  • variable:每次迭代时,variable会被赋值为下一个迭代值。
  • iterable:一个可迭代对象(一个实现Iterable 接口的对象,如数组、字符串、Map、Set 等)。

示例

遍历数组

const array = [1, 2, 3, 4, 5];

for (const value of array) {
  console.log(value); // 输出: 1, 2, 3, 4, 5
}

遍历字符串

const string = 'hello';

for (const char of string) {
  console.log(char); // 输出: h, e, l, l, o
}

遍历 Map

const map = new Map([['a', 1], ['b', 2], ['c', 3]]);

for (const [key, value] of map) {
  console.log(`${key}: ${value}`); // 输出: a: 1, b: 2, c: 3
}

遍历 Set

const set = new Set([1, 2, 3]);

for (const value of set) {
  console.log(value); // 输出: 1, 2, 3
}

与 for…in 的区别

  • ..in遍历对象的可枚举属性,包括继承的属性。
  • ..of遍历可迭代对象的值,不关心属性名或键。
const obj = { a: 1, b: 2, c: 3 };

for (const key in obj) {
  console.log(key); // 输出: a, b, c
}

const array = [1, 2, 3];

for (const value of array) {
  console.log(value); // 输出: 1, 2, 3
}

可迭代对象

for…of 循环可以用于任何实现了 Iterable 接口的对象。这些对象必须实现 [Symbol.iterator] 方法,该方法返回一个迭代器对象。

常见的可迭代对象包括:

  • 数组
  • 字符串
  • Map
  • Set
  • NodeList
  • arguments对象

自定义迭代器

你可以创建实现了 Iterable 接口的自定义对象,允许其与 for…of 一起使用。

const iterableObject = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
};

for (const value of iterableObject) {
  console.log(value); // 输出: 1, 2, 3
}

在上面的示例中,我们使用生成器函数和 Symbol.iterator 方法创建了一个自定义的可迭代对象。

异常处理

for…of 循环可以与 try…catch 一起使用来处理迭代过程中可能出现的异常。

const array = [1, 2, 3];

try {
  for (const value of array) {
    if (value === 2) {
      throw new Error('An error occurred');
    }
    console.log(value);
  }
} catch (error) {
  console.error(error.message); // 输出: An error occurred
}

跳过与提前退出

for…of 循环支持 break、continue 和 return 语句来控制循环的执行。

const array = [1, 2, 3, 4, 5];

for (const value of array) {
  if (value === 3) {
    continue; // 跳过当前迭代
  } else if (value === 4) {
    break; // 提前退出循环
  }
  console.log(value); // 输出: 1, 2
}

使用 for…of 遍历对象的属性

虽然 for…of 不能直接用于普通对象,但你可以使用 Object.keys、Object.values 或 Object.entries 将对象转换为可迭代对象。

const obj = { a: 1, b: 2, c: 3 };

// 使用 Object.keys
for (const key of Object.keys(obj)) {
  console.log(key); // 输出: a, b, c
}

// 使用 Object.values
for (const value of Object.values(obj)) {
  console.log(value); // 输出: 1, 2, 3
}

// 使用 Object.entries
for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`); // 输出: a: 1, b: 2, c: 3
}

实际应用示例

处理异步迭代

for…of 循环可以与 async/await 结合使用,处理异步迭代。

async function fetchUrls(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
  }
}

const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];
fetchUrls(urls);

遍历 NodeList

在浏览器环境中,NodeList 对象是可迭代的,可以使用 for…of 循环遍历。

const nodeList = document.querySelectorAll('div');

for (const node of nodeList) {
  console.log(node); // 输出每个 div 元素
}

异步迭代器 (for await…of)

在 JavaScript 中,异步迭代器(Asynchronous Iterators)和 for await…of 循环是 ES2018(ES9)引入的新特性,用于处理异步数据流。这些特性使得处理异步操作和流式数据变得更加简单和直观,尤其是在处理需要逐步获取数据的场景中非常有用,例如网络请求、文件读取等。

基本概念

异步迭代器与普通迭代器类似,但它们的 next 方法返回的是一个 Promise,这使得它们能够表示异步操作。在异步迭代器中,for await…of 循环用于迭代异步数据流。

定义异步迭代器

要创建一个异步可迭代对象,需要实现 [Symbol.asyncIterator] 方法,该方法返回一个异步迭代器对象。这个对象必须实现 next 方法,并返回一个 Promise,该 Promise 解析为一个对象,包含 value 和 done 属性。

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let i = 0;
    return {
      next() {
        if (i < 3) {
          return Promise.resolve({ value: i++, done: false });
        }
        return Promise.resolve({ value: undefined, done: true });
      }
    };
  }
};

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // 输出: 0, 1, 2
  }
})();

for await…of 循环

for await…of 循环用于遍历异步可迭代对象。其语法与 for…of 类似,但需要使用 await 关键字。

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // 输出: 0, 1, 2
  }
})();

异步生成器

异步生成器(Asynchronous Generators)是异步迭代器的一种特殊形式,使用 async function* 语法定义。异步生成器函数的 yield 表达式可以等待异步操作,并返回一个 Promise。

async function* asyncGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

(async () => {
  for await (const value of asyncGenerator()) {
    console.log(value); // 输出: 1, 2, 3
  }
})();

实际应用示例

处理异步数据流

假设我们有一个异步数据流源,如从服务器逐步获取数据,可以用异步生成器来处理这些数据。

async function* fetchData() {
  const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];

  for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    yield data;
  }
}

(async () => {
  for await (const data of fetchData()) {
    console.log(data); // 输出每个数据块
  }
})();

异步文件读取

在 Node.js 环境中,可以使用异步迭代器来逐步读取文件内容。

const fs = require('fs');
const readline = require('readline');

async function* readLines(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

(async () => {
  for await (const line of readLines('./example.txt')) {
    console.log(line); // 输出每一行内容
  }
})();

异步迭代器的错误处理

可以使用 try…catch 块来捕获异步迭代器中的错误。

async function* errorProneGenerator() {
  yield 1;
  throw new Error('Something went wrong');
  yield 2;
}

(async () => {
  try {
    for await (const value of errorProneGenerator()) {
      console.log(value);
    }
  } catch (error) {
    console.error('Error caught:', error.message); // 输出: Error caught: Something went wrong
  }
})();

复杂示例:多源数据流合并

假设我们有多个异步数据源,并希望将它们的结果合并到一个异步迭代器中,可以使用异步生成器来实现。

async function* mergeAsyncIterables(...iterables) {
  const promises = iterables.map(iterable => iterable[Symbol.asyncIterator]().next());

  while (promises.length > 0) {
    const { value, done, index } = await Promise.race(
      promises.map((p, i) => p.then(result => ({ ...result, index: i })))
    );

    if (done) {
      promises.splice(index, 1);
    } else {
      promises[index] = iterables[index][Symbol.asyncIterator]().next();
      yield value;
    }
  }
}

async function* asyncGenerator1() {
  yield 'a1';
  await new Promise(resolve => setTimeout(resolve, 1000));
  yield 'a2';
}

async function* asyncGenerator2() {
  yield 'b1';
  await new Promise(resolve => setTimeout(resolve, 500));
  yield 'b2';
}

(async () => {
  for await (const value of mergeAsyncIterables(asyncGenerator1(), asyncGenerator2())) {
    console.log(value); // 可能输出: b1, a1, b2, a2
  }
})();

注意事项

  • 环境支持:for await…of循环是 ES2018 引入的特性,需要确保目标环境支持。
  • 性能注意:异步迭代器在处理大量数据时需要注意性能,尤其是在频繁进行异步操作的情况下。
  • 错误处理:异步迭代器中可能会抛出错误,需要使用..catch 块进行适当的错误处理。

 

Rest/Spread 属性

JavaScript 的 Rest 和 Spread 属性是 ES2018(ECMAScript 2018)引入的特性,用于对象和数组的解构、合并和拷贝。它们使得处理对象和数组的操作更加简洁和直观。

Rest 属性

Rest 属性用于将剩余的未解构属性收集到一个新对象中。它在对象解构赋值中使用,用于提取对象中的剩余属性。

基本语法

const { 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 }

在上面的示例中,{ a, b, …rest } 用于解构对象,其中 a 和 b 获取对象中的对应属性,剩余的属性 c 和 d 被收集到新的对象 rest 中。

示例

提取对象中的部分属性

const user = {
  id: 1,
  name: 'Alice',
  age: 25,
  email: 'alice@example.com'
};

const { id, ...userInfo } = user;
console.log(id);        // 输出: 1
console.log(userInfo);  // 输出: { name: 'Alice', age: 25, email: 'alice@example.com' }

函数参数中的 Rest 属性

Rest 属性还可以用于函数参数,用于收集剩余的参数。

function logUser({ id, ...userInfo }) {
  console.log(id);        // 输出: 1
  console.log(userInfo);  // 输出: { name: 'Alice', age: 25, email: 'alice@example.com' }
}

const user = {
  id: 1,
  name: 'Alice',
  age: 25,
  email: 'alice@example.com'
};

logUser(user);

Spread 属性

Spread 属性用于将一个对象或数组展开为一系列的键值对或元素。它在对象和数组的字面量中使用,用于合并、拷贝对象或数组。

基本语法

对象中的 Spread 属性

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };

console.log(obj2); // 输出: { a: 1, b: 2, c: 3 }

数组中的 Spread 属性

const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];

console.log(arr2); // 输出: [1, 2, 3, 4, 5]

示例

对象合并

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const mergedObj = { ...obj1, ...obj2 };

console.log(mergedObj); // 输出: { a: 1, b: 2, c: 3, d: 4 }

对象拷贝

const original = { a: 1, b: 2 };
const copy = { ...original };

console.log(copy); // 输出: { a: 1, b: 2 }

数组合并

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const mergedArr = [...arr1, ...arr2];

console.log(mergedArr); // 输出: [1, 2, 3, 4, 5, 6]

数组拷贝

const original = [1, 2, 3];
const copy = [...original];

console.log(copy); // 输出: [1, 2, 3]

复杂示例:使用 Rest 和 Spread 属性构建对象和数组

构建复杂对象

假设我们有一个用户对象,需要根据不同的条件添加或修改属性。

const baseUser = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
};

const extendedUser = {
  ...baseUser,
  age: 25,
  address: '123 Main St'
};

console.log(extendedUser);
// 输出: { id: 1, name: 'Alice', email: 'alice@example.com', age: 25, address: '123 Main St' }

数组去重

使用 Spread 属性和 Set 进行数组去重。

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [...new Set(numbers)];

console.log(uniqueNumbers); // 输出: [1, 2, 3, 4, 5]

函数参数解构和重构

假设我们有一个函数需要接收用户信息并进行处理。

function processUser({ id, name, ...rest }) {
  console.log(id);   // 输出: 1
  console.log(name); // 输出: 'Alice'
  console.log(rest); // 输出: { email: 'alice@example.com', age: 25 }
}

const user = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  age: 25
};

processUser(user);

动态构建对象

假设我们需要根据条件动态构建对象。

const baseConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

const env = 'production';
const config = {
  ...baseConfig,
  ...(env === 'production' ? { cache: true } : { debug: true })
};

console.log(config);
// 输出(如果 env 是 'production'): { apiUrl: 'https://api.example.com', timeout: 5000, cache: true }
// 输出(如果 env 不是 'production'): { apiUrl: 'https://api.example.com', timeout: 5000, debug: true }

注意事项

浅拷贝

Spread 属性进行的是浅拷贝,对于嵌套对象或数组,内部的引用仍然指向相同的对象或数组。

const original = { a: 1, b: { c: 2 } };
const copy = { ...original };

copy.b.c = 3;
console.log(original.b.c); // 输出: 3

顺序

对于对象的 Spread 属性,后面的属性会覆盖前面的属性。

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 };

console.log(merged); // 输出: { a: 1, b: 3, c: 4 }

globalThis

globalThis 是 JavaScript 在 ES2020(ECMAScript 2020)中引入的一个新特性,提供了一种标准化的方法来访问全局对象。全局对象是 JavaScript 环境中的顶级对象,不同环境中的全局对象名称不同,例如在浏览器中是 window,在 Node.js 中是 global,在 Web Workers 中是 self。

为什么需要 globalThis

由于不同的 JavaScript 环境有不同的全局对象名称,在编写跨平台或跨环境的代码时,需要区分这些不同的全局对象。这使得代码复杂化,增加了维护成本。globalThis 解决了这一问题,它为所有环境提供了一个统一的访问全局对象的方式。

基本语法

console.log(globalThis);

在任何 JavaScript 环境中,globalThis 都指向全局对象。

示例

在浏览器中

在浏览器环境中,globalThis 与 window 对象相同。

console.log(globalThis === window); // 输出: true

在 Node.js 中

在 Node.js 环境中,globalThis 与 global 对象相同。

console.log(globalThis === global); // 输出: true

在 Web Workers 中

在 Web Workers 中,globalThis 与 self 对象相同。

console.log(globalThis === self); // 输出: true

实际应用场景

跨环境的全局对象访问

在编写需要在多种环境中运行的代码时,使用 globalThis 可以避免对不同环境中的全局对象名称进行判断。

// 以前的做法
const globalObj = typeof window !== 'undefined' ? window :
                  typeof global !== 'undefined' ? global :
                  typeof self !== 'undefined' ? self :
                  {};

// 使用 globalThis
const globalObj = globalThis;

console.log(globalObj);

设置和获取全局变量

使用 globalThis 可以方便地设置和获取全局变量,而不需要考虑当前运行环境。

// 设置全局变量
globalThis.myGlobalVar = 'Hello, world!';

// 获取全局变量
console.log(globalThis.myGlobalVar); // 输出: 'Hello, world!'

函数和模块的跨环境共享

在编写函数或模块时,可以使用 globalThis 来实现跨环境的共享。

// 定义一个函数并将其挂载到全局对象上
globalThis.myGlobalFunction = function() {
  console.log('This is a global function');
};

// 在任何环境中调用该函数
myGlobalFunction(); // 输出: 'This is a global function'

Polyfill

如果需要在不支持 globalThis 的旧环境中使用,可以使用以下 polyfill:

if (typeof globalThis === 'undefined') {
  (function() {
    if (typeof self !== 'undefined') {
      self.globalThis = self;
    } else if (typeof window !== 'undefined') {
      window.globalThis = window;
    } else if (typeof global !== 'undefined') {
      global.globalThis = global;
    } else {
      // Fallback if no global object is found
      this.globalThis = this;
    }
  })();
}

复杂示例:跨环境的库

假设我们要编写一个跨环境的库,可以在浏览器、Node.js 和 Web Workers 中运行。我们可以使用 globalThis 来访问全局对象,并在不同环境中共享一些公共方法。

// 定义公共方法
function isNodeEnvironment() {
  return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
}

function isBrowserEnvironment() {
  return typeof window !== 'undefined' && typeof window.document !== 'undefined';
}

function isWorkerEnvironment() {
  return typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
}

// 将公共方法挂载到全局对象上
globalThis.myLibrary = {
  isNodeEnvironment,
  isBrowserEnvironment,
  isWorkerEnvironment
};

// 在任何环境中使用库
console.log(globalThis.myLibrary.isNodeEnvironment());
console.log(globalThis.myLibrary.isBrowserEnvironment());
console.log(globalThis.myLibrary.isWorkerEnvironment());

注意事项

  • 避免命名冲突:在全局对象上设置变量时,要注意避免与已有属性或方法冲突。可以考虑使用命名空间来组织全局变量。
  • 浏览器兼容性:globalThis是 ES2020 引入的特性,需要确保目标环境支持。如果需要在不支持的环境中使用,可以使用 polyfill。

Set、WeakSet和 Map、 WeakMap

在JavaScript中,集合(Set)和映射(Map)是常用的数据结构,用于存储唯一值的集合和键值对的集合。两个弱版本(WeakSet 和 WeakMap)也存在,主要用于内存管理和垃圾回收优化。下面详细介绍这些数据结构及其用法。

Set

Set 是一种集合数据结构,用于存储唯一的值,无论是原始值还是对象引用。

基本用法

const mySet = new Set([1, 2, 3, 4, 4]); // 初始化Set,重复的4只会存储一次
console.log(mySet); // 输出: Set { 1, 2, 3, 4 }

mySet.add(5);
console.log(mySet); // 输出: Set { 1, 2, 3, 4, 5 }

mySet.delete(2);
console.log(mySet); // 输出: Set { 1, 3, 4, 5 }

console.log(mySet.has(3)); // 输出: true
console.log(mySet.size);   // 输出: 4

mySet.clear();
console.log(mySet.size);   // 输出: 0

迭代

Set 支持多种迭代方法:

const mySet = new Set(['a', 'b', 'c']);

for (const value of mySet) {
  console.log(value);
}

mySet.forEach((value) => {
  console.log(value);
});

常用方法

  • add(value): 向Set 添加一个值。
  • delete(value): 移除Set 中的指定值。
  • has(value): 检查Set 是否包含指定值。
  • clear(): 移除Set 中的所有值。
  • size: 返回Set 中值的数量。

Map

Map 是一种集合数据结构,用于存储键值对(key-value pairs),任何值(对象或原始值)都可以作为键。

基本用法

const myMap = new Map([
  ['name', 'Alice'],
  ['age', 25]
]);

console.log(myMap); // 输出: Map { 'name' => 'Alice', 'age' => 25 }

myMap.set('occupation', 'Engineer');
console.log(myMap.get('occupation')); // 输出: 'Engineer'

myMap.delete('age');
console.log(myMap.has('age')); // 输出: false

console.log(myMap.size); // 输出: 2

myMap.clear();
console.log(myMap.size); // 输出: 0

迭代

Map 支持多种迭代方法:

const myMap = new Map([
  ['name', 'Alice'],
  ['age', 25]
]);

for (const [key, value] of myMap) {
  console.log(`${key}: ${value}`);
}

myMap.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

常用方法

  • set(key, value): 向Map 添加或更新指定键的值。
  • get(key): 获取Map 中指定键的值。
  • delete(key): 移除Map 中的指定键。
  • has(key): 检查Map 是否包含指定键。
  • clear(): 移除Map 中的所有键值对。
  • size: 返回Map 中键值对的数量。

WeakSet

WeakSet 是一种特殊的 Set,其成员只能是对象,且这些对象是弱引用的。弱引用意味着如果没有其他引用指向该对象,它可以被垃圾回收机制回收。

基本用法

const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };

const myWeakSet = new WeakSet([obj1, obj2]);

console.log(myWeakSet.has(obj1)); // 输出: true

myWeakSet.delete(obj2);
console.log(myWeakSet.has(obj2)); // 输出: false

常用方法

  • add(value): 向WeakSet 添加一个对象。
  • delete(value): 移除WeakSet 中的指定对象。
  • has(value): 检查WeakSet 是否包含指定对象。

WeakMap

WeakMap 是一种特殊的 Map,其键只能是对象,且这些对象是弱引用的。弱引用的键意味着如果没有其他引用指向该对象,它可以被垃圾回收机制回收。

基本用法

const obj1 = { name: 'Alice' };
const obj2 = { name: 'Bob' };

const myWeakMap = new WeakMap([
  [obj1, 'Engineer']
]);

console.log(myWeakMap.get(obj1)); // 输出: 'Engineer'

myWeakMap.set(obj2, 'Doctor');
console.log(myWeakMap.get(obj2)); // 输出: 'Doctor'

myWeakMap.delete(obj1);
console.log(myWeakMap.has(obj1)); // 输出: false

常用方法

  • set(key, value): 向WeakMap 添加或更新指定键的值。
  • get(key): 获取WeakMap 中指定键的值。
  • delete(key): 移除WeakMap 中的指定键。
  • has(key): 检查WeakMap 是否包含指定键。

复杂示例:使用 Set 和 Map

假设我们要实现一个简单的社交网络模型,用户可以有朋友和消息。

class User {
  constructor(name) {
    this.name = name;
    this.friends = new Set();
    this.messages = [];
  }

  addFriend(friend) {
    this.friends.add(friend);
  }

  sendMessage(to, message) {
    to.messages.push({ from: this.name, message });
  }

  readMessages() {
    this.messages.forEach(msg => {
      console.log(`${msg.from}: ${msg.message}`);
    });
  }
}

const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');

alice.addFriend(bob);
alice.addFriend(charlie);

alice.sendMessage(bob, 'Hello, Bob!');
alice.sendMessage(charlie, 'Hi, Charlie!');

bob.readMessages(); // 输出: Alice: Hello, Bob!
charlie.readMessages(); // 输出: Alice: Hi, Charlie!

复杂示例:使用 WeakSet 和 WeakMap

假设我们要管理一个缓存系统,缓存的键是用户对象,值是用户的相关数据。

const userCache = new WeakMap();

function cacheUser(user) {
  if (!userCache.has(user)) {
    userCache.set(user, { data: `Data for ${user.name}` });
  }
  return userCache.get(user);
}

const alice = { name: 'Alice' };
const bob = { name: 'Bob' };

console.log(cacheUser(alice)); // 输出: { data: 'Data for Alice' }
console.log(cacheUser(bob));   // 输出: { data: 'Data for Bob' }

// 当 alice 和 bob 不再有其他引用时,它们的缓存会被自动垃圾回收

注意事项

  • 引用类型:WeakSet 和 WeakMap 只能存储对象引用,不能存储原始值(如字符串、数字等)。
  • 垃圾回收:WeakSet 和 WeakMap 中的对象是弱引用,当没有其他引用指向这些对象时,它们会被垃圾回收。
  • 没有迭代方法:WeakSet 和 WeakMap 没有迭代方法(如forEach 或 values),因为它们的成员可能在任何时候被垃圾回收。

WeakRef 和 FinalizationRegistry

在 JavaScript 中,WeakRef 和 FinalizationRegistry 是在 ES2021(ECMAScript 2021)中引入的两个新特性,用于更灵活和安全地处理对象的内存管理。它们主要用于处理与垃圾回收相关的高级场景。

WeakRef

WeakRef 提供了一种创建弱引用(weak reference)的方法,即对一个对象的引用,但不会阻止该对象被垃圾回收。

基本概念

  • 弱引用:与强引用不同,弱引用不会阻止垃圾回收器回收该对象。
  • 垃圾回收:如果没有其他强引用指向一个对象,即使存在弱引用,垃圾回收器也会回收该对象。

基本语法

const weakRef = new WeakRef(target);

示例

class Example {
  constructor(value) {
    this.value = value;
  }
}

let obj = new Example('Hello, World!');
const weakRef = new WeakRef(obj);

console.log(weakRef.deref()); // 输出: Example { value: 'Hello, World!' }

// 删除强引用
obj = null;

// 尝试获取弱引用
console.log(weakRef.deref()); // 可能输出: undefined(取决于垃圾回收是否已发生)

用途

  • 缓存:在某些缓存实现中,使用WeakRef 可以避免缓存对象阻止垃圾回收,从而减少内存使用。
  • 避免内存泄漏:在需要临时对象引用的场景中使用WeakRef 可以防止内存泄漏。

FinalizationRegistry

FinalizationRegistry 提供了一种注册回调的方法,当注册的对象被垃圾回收时,这些回调会被执行。

基本概念

  • 回调函数:当对象被垃圾回收时,FinalizationRegistry会调用注册的回调函数,通知用户对象已被回收。
  • 注册对象:通过register 方法注册对象,以便在对象被回收时触发回调。

基本语法

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object with held value: ${heldValue} has been collected`);
});

示例

class Example {
  constructor(value) {
    this.value = value;
  }
}

const registry = new FinalizationRegistry((heldValue) => {
  console.log(`Object with value: ${heldValue} has been collected`);
});

let obj = new Example('Hello, World!');
registry.register(obj, 'Example Object');

// 删除强引用
obj = null;

// 垃圾回收器将会在回收 obj 时调用注册的回调函数

用途

  • 资源清理:在对象被回收时自动清理相关资源,如关闭文件、清理网络连接等。
  • 调试:帮助调试和监控对象的生命周期,了解何时对象被回收。

注意事项

  • 不可预测性:JavaScript 中的垃圾回收行为是不可预测的,因此依赖于WeakRef 的对象可能随时被回收,而 FinalizationRegistry 的回调可能不及时执行。
  • 避免滥用:这些特性应慎重使用,主要用于特定场景下的内存管理优化,不应过度依赖它们来控制程序逻辑。

其他更新内容

Object.entries() 和 Object.values()

Object.entries() 返回一个给定对象自身可枚举属性的键值对数组,而 Object.values() 返回一个给定对象自身可枚举属性值的数组。

示例:

const obj = { a: 1, b: 2, c: 3 };

console.log(Object.entries(obj)); // [ ['a', 1], ['b', 2], ['c', 3] ]
console.log(Object.values(obj)); // [ 1, 2, 3 ]

Object.fromEntries()

Object.fromEntries 是 JavaScript 中用于将键值对列表转换为对象的方法。它在 ES2019(ECMAScript 2019)中引入,提供了一种简单且便捷的方式从可迭代对象(如数组或 Map)创建对象。

基本语法

Object.fromEntries(iterable);
  • iterable:一个可迭代的对象,其每个元素是一个具有两个元素的数组,前者表示键,后者表示值。

示例

从数组创建对象

const entries = [
  ['name', 'Alice'],
  ['age', 25],
  ['occupation', 'Engineer']
];

const obj = Object.fromEntries(entries);
console.log(obj);
// 输出: { name: 'Alice', age: 25, occupation: 'Engineer' }

从 Map 创建对象

const map = new Map([
  ['name', 'Bob'],
  ['age', 30],
  ['occupation', 'Designer']
]);

const obj = Object.fromEntries(map);
console.log(obj);
// 输出: { name: 'Bob', age: 30, occupation: 'Designer' }

常见应用场景

URL 查询参数解析,将 URL 查询字符串转换为对象,方便访问和操作查询参数。

const query = 'name=Alice&age=25&occupation=Engineer';
const params = new URLSearchParams(query);

const obj = Object.fromEntries(params);
console.log(obj);
// 输出: { name: 'Alice', age: '25', occupation: 'Engineer' }

Object.entries 的逆操作,Object.fromEntries 是 Object.entries 的逆操作,后者将对象转换为键值对数组。

const obj = { name: 'Charlie', age: 28, occupation: 'Developer' };

// 转换为键值对数组
const entries = Object.entries(obj);
console.log(entries);
// 输出: [['name', 'Charlie'], ['age', 28], ['occupation', 'Developer']]

// 使用 Object.fromEntries 还原对象
const newObj = Object.fromEntries(entries);
console.log(newObj);
// 输出: { name: 'Charlie', age: 28, occupation: 'Developer' }

复杂示例

假设我们有一个数组,包含一些键值对,我们希望根据某些条件过滤并转换为对象。

const entries = [
  ['name', 'Alice'],
  ['age', 25],
  ['occupation', 'Engineer'],
  ['country', 'USA']
];

// 过滤掉年龄小于30的项,并转换为对象
const filteredObj = Object.fromEntries(
  entries.filter(([key, value]) => key !== 'age' || value >= 30)
);

console.log(filteredObj);
// 输出: { name: 'Alice', occupation: 'Engineer', country: 'USA' }

Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() 方法返回一个对象所有自身属性的描述符。

示例:

const obj = {
  prop1: 42,
  get prop2() {
    return this.prop1 * 2;
  }
};

const descriptors = Object.getOwnPropertyDescriptors(obj);
console.log(descriptors);

// 输出:
// {
//   prop1: { value: 42, writable: true, enumerable: true, configurable: true },
//   prop2: {
//     get: [Function: get prop2],
//     set: undefined,
//     enumerable: true,
//     configurable: true
//   }
// }

Object.hasOwn 方法

Object.hasOwn 是 JavaScript 中一个用于检查对象自身属性的新方法。它是在 ES2022(ECMAScript 2022)中引入的,用来取代常用的 Object.prototype.hasOwnProperty,提供了一种更简洁和安全的方式来检查对象自身属性。

基本用法

const obj = {
  name: 'Alice',
  age: 30
};

console.log(Object.hasOwn(obj, 'name')); // 输出: true
console.log(Object.hasOwn(obj, 'gender')); // 输出: false

检查继承属性

Object.hasOwn 只检查对象自身的属性,而不会检查继承自原型链的属性:

const parent = { inheritedProp: 'inherited' };
const child = Object.create(parent);
child.ownProp = 'own';

console.log(Object.hasOwn(child, 'ownProp')); // 输出: true
console.log(Object.hasOwn(child, 'inheritedProp')); // 输出: false

 Object.prototype.hasOwnProperty 的比较

Object.hasOwn 提供了一种更简洁和安全的方式来检查对象自身属性,避免了可能的原型污染问题。传统方法 obj.hasOwnProperty 可能会因为对象本身定义了 hasOwnProperty 方法而导致潜在的问题:

const obj = {
  hasOwnProperty: function() {
    return false;
  },
  name: 'Alice'
};

console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); // 输出: true
console.log(Object.hasOwn(obj, 'name')); // 输出: true

在上面的例子中,如果直接调用 obj.hasOwnProperty(‘name’),会导致错误的结果,因为对象自身重写了 hasOwnProperty 方法。而 Object.hasOwn 则不会遇到这个问题。

适用场景

  • 对象属性检查:在需要检查对象是否拥有某个自身属性的场景中。
  • 防止原型污染:避免因为对象自身定义了hasOwnProperty 方法而导致的错误检查。
  • 简化代码:提供了更简洁和直观的语法来进行属性检查。

String.prototype.padStart() 和 String.prototype.padEnd()

padStart() 和 padEnd() 用于在字符串的开头或结尾补全指定的字符,以达到指定的长度。

示例:

const str = "Hello";

console.log(str.padStart(10)); // "     Hello"
console.log(str.padStart(10, '*')); // "*****Hello"

console.log(str.padEnd(10)); // "Hello     "
console.log(str.padEnd(10, '*')); // "Hello*****"

String.prototype.trimStart() 和 String.prototype.trimEnd()

trimStart 和 trimEnd 是 JavaScript 字符串对象的两个方法,用于修剪字符串开头或结尾的空白字符(包括空格、制表符等)。这些方法在 ES2019(ECMAScript 2019)中引入,提供了更精确的字符串修剪操作。

String.prototype.trimStart(也称为 trimLeft)

trimStart 方法用于从字符串的开头移除空白字符。

示例:

const str = '   Hello, world!   ';

const trimmedStr = str.trimStart();
console.log(trimmedStr); // 输出: 'Hello, world!   '

String.prototype.trimEnd(也称为 trimRight)

trimEnd 方法用于从字符串的结尾移除空白字符。

示例:

const str = '   Hello, world!   ';

const trimmedStr = str.trimEnd();
console.log(trimmedStr); // 输出: '   Hello, world!'

String.prototype.replaceAll

String.prototype.replaceAll 是 JavaScript 中的一个字符串方法,用于替换字符串中所有匹配的子字符串。它在 ES2021(ECMAScript 2021)中引入,提供了一种简便的方法来处理字符串的全局替换操作。

替换所有匹配的子字符串

const str = 'The quick brown fox jumps over the lazy dog. The dog barked.';

// 使用字符串作为 searchValue
const newStr1 = str.replaceAll('dog', 'cat');
console.log(newStr1);
// 输出: 'The quick brown fox jumps over the lazy cat. The cat barked.'

// 使用正则表达式作为 searchValue
const newStr2 = str.replaceAll(/dog/g, 'cat');
console.log(newStr2);
// 输出: 'The quick brown fox jumps over the lazy cat. The cat barked.'

特殊字符替换

当替换包含特殊字符的子字符串时,可以直接使用 replaceAll 方法:

const str = 'Hello, world! Hello, universe!';

// 替换所有 "Hello"
const newStr = str.replaceAll('Hello', 'Hi');
console.log(newStr);
// 输出: 'Hi, world! Hi, universe!'

区分大小写

replaceAll 方法是区分大小写的。如果需要忽略大小写,可以使用带有 i 标志的正则表达式。

const str = 'Hello, World! hello, world!';
const newStr = str.replaceAll(/hello/gi, 'Hi');
console.log(newStr);
// 输出: 'Hi, World! Hi, world!'

正则表达式标志

如果 searchValue 是正则表达式,且不带全局标志 g,则 replaceAll 方法会抛出一个错误。

const str = 'Hello, world!';
try {
  str.replaceAll(/Hello/i, 'Hi');
} catch (e) {
  console.error(e);
  // 输出: TypeError: Only global regexps are allowed with String.prototype.replaceAll
}

使用场景

  • 文本替换:在处理大段文本时,替换特定的单词或短语。
  • 数据清理:在数据清理和格式化过程中,批量替换不符合要求的字符串。
  • 模板替换:在模板字符串中替换占位符。

Array.prototype.includes

Array.prototype.includes 方法用于判断一个数组是否包含某个特定的值,根据判断结果返回 true 或 false。它比 indexOf 方法更加直观和易读。

语法:

arr.includes(valueToFind[, fromIndex])
  • valueToFind:需要在数组中查找的值。
  • fromIndex(可选):从该索引开始查找,默认为0。如果 fromIndex 为负值,表示从数组末尾开始的偏移量。

示例:

const arr = [1, 2, 3, 4, 5];

console.log(arr.includes(3)); // true
console.log(arr.includes(6)); // false

// 带 fromIndex 参数
console.log(arr.includes(3, 3)); // false,从索引 3 开始查找
console.log(arr.includes(3, -2)); // false,从倒数第二个元素开始查找

与 indexOf 方法相比,includes 更加直观。不需要检查返回值是否等于 -1 来判断元素是否存在。

const strArray = ['apple', 'banana', 'cherry'];

console.log(strArray.indexOf('banana') !== -1); // true
console.log(strArray.includes('banana')); // true

Array.prototype.flat 和 Array.prototype.flatMap

Array.prototype.flat 和 Array.prototype.flatMap 是 JavaScript 中用于操作数组的两个常用方法,它们在处理嵌套数组和映射数组方面非常有用。下面是它们的简单介绍和用法示例。

Array.prototype.flat

flat 方法用于将嵌套的数组“拉平”成一个单一的数组。可以指定展开的深度,如果不指定,默认深度为 1。

基本语法

arr.flat([depth]);
  • depth(可选):指定要展开到的深度。默认值是 1。

示例:

const arr1 = [1, 2, [3, 4, [5, 6]]];

// 默认深度 1
console.log(arr1.flat()); 
// 输出: [1, 2, 3, 4, [5, 6]]

// 深度 2
console.log(arr1.flat(2));
// 输出: [1, 2, 3, 4, 5, 6]

// 深度无限(拉平所有层级)
console.log(arr1.flat(Infinity));
// 输出: [1, 2, 3, 4, 5, 6]

// 去除数组中的空项
const arr2 = [1, 2, , 4, 5];
console.log(arr2.flat());
// 输出: [1, 2, 4, 5]

Array.prototype.flatMap

flatMap 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它是 map 和 flat 的组合操作,深度为 1。

基本语法:

arr.flatMap(callback(currentValue[, index[, array]])[, thisArg]);
  • callback:映射函数,用于映射每个元素。
  • currentValue:当前处理的元素。
  • index(可选):当前处理元素的索引。
  • array(可选):调用 flatMap 的数组。
  • thisArg(可选):执行 callback 函数时 this 的值。

示例:

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

// 映射并拉平
const flatMappedArr = arr.flatMap(x => [x, x * 2]);

console.log(flatMappedArr);
// 输出: [1, 2, 2, 4, 3, 6, 4, 8]

// 使用 map 和 flat 的组合来实现相同的效果
const flatMappedArrWithMapAndFlat = arr.map(x => [x, x * 2]).flat();

console.log(flatMappedArrWithMapAndFlat);
// 输出: [1, 2, 2, 4, 3, 6, 4, 8]

复杂示例:结合 flat 和 flatMap

假设我们有一个包含嵌套数组的数组,每个嵌套数组包含字符串,我们希望将其拉平成一个字符串数组,并在每个字符串后面添加一个标记。

const nestedArr = [
  ['apple', 'banana'],
  ['cherry', 'date'],
  ['elderberry', 'fig', ['grape', 'honeydew']]
];

// 使用 flat 拉平数组
const flattenedArr = nestedArr.flat(2);
console.log(flattenedArr);
// 输出: ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig', 'grape', 'honeydew']

// 使用 flatMap 拉平并映射
const flatMappedArr = nestedArr.flatMap(subArr =>
  Array.isArray(subArr) ? subArr.flatMap(item => `${item} (fruit)`) : `${subArr} (fruit)`
);
console.log(flatMappedArr);
// 输出: ['apple (fruit)', 'banana (fruit)', 'cherry (fruit)', 'date (fruit)', 'elderberry (fruit)', 'fig (fruit)', 'grape (fruit)', 'honeydew (fruit)']

Array.prototype.at 和 TypedArray.prototype.at

在 JavaScript 中,Array 和 TypedArray 原型上的 at 方法是 ES2022(ECMAScript 2022)中引入的一项新特性。这个方法提供了一种更简洁和直观的方式来访问数组中的元素,尤其是支持负索引,从数组末尾开始访问元素。

Array.prototype.at

Array.prototype.at 方法允许你通过索引访问数组中的元素,并且支持负索引。

示例:

const array = [10, 20, 30, 40, 50];

// 正索引
console.log(array.at(0)); // 输出: 10
console.log(array.at(2)); // 输出: 30

// 负索引
console.log(array.at(-1)); // 输出: 50
console.log(array.at(-3)); // 输出: 30

// 超出索引范围
console.log(array.at(10)); // 输出: undefined
console.log(array.at(-10)); // 输出: undefined

TypedArray.prototype.at

TypedArray.prototype.at 方法与 Array.prototype.at 的行为类似,但它是用于处理类型化数组(TypedArray)的。

示例:

const typedArray = new Int16Array([10, 20, 30, 40, 50]);

// 正索引
console.log(typedArray.at(0)); // 输出: 10
console.log(typedArray.at(2)); // 输出: 30

// 负索引
console.log(typedArray.at(-1)); // 输出: 50
console.log(typedArray.at(-3)); // 输出: 30

// 超出索引范围
console.log(typedArray.at(10)); // 输出: undefined
console.log(typedArray.at(-10)); // 输出: undefined

适用场景

  • 简化代码:在需要从数组末尾访问元素时,at 方法使代码更加简洁和直观,不再需要使用length 进行计算。
  • 提高可读性:使用负索引访问元素时,at 方法比传统的索引计算方法更具可读性。

可选 catch 绑定

可选 catch 绑定是 JavaScript 在 ES2019(ECMAScript 2019)中引入的一项语法改进,它简化了 try…catch 语句中的 catch 部分,使其在不需要访问捕获的错误对象时变得更加简洁。

在传统的 try…catch 语句中,catch 子句必须包含一个错误参数,即使不使用它也要声明:

try {
  // 可能会抛出错误的代码
} catch (error) {
  // 错误处理
  console.error(error); // 即使不使用 error,也必须声明
}

有了可选 catch 绑定后,可以省略 catch 子句中的错误参数:

try {
  // 可能会抛出错误的代码
} catch {
  // 错误处理
  console.error('An error occurred');
}

可选 catch 绑定主要用于以下场景:

  • 不需要访问错误对象:当你不需要对捕获的错误对象进行操作时,可以使用可选 catch 绑定来简化代码。
  • 一般错误处理:对于一些通用的错误处理逻辑,例如记录错误或显示错误提示,可以使用可选 catch 绑定。

复杂示例

假设我们在一个应用中有多个不同的操作,每个操作可能会抛出错误,但对于这些错误,我们只需要简单地记录它们,而不需要访问具体的错误对象。

const operations = [
  () => JSON.parse('invalid json 1'),
  () => { throw new Error('Some error'); },
  () => JSON.parse('invalid json 2')
];

operations.forEach(operation => {
  try {
    operation();
  } catch {
    console.error('An error occurred during operation');
  }
});

在上面的示例中,每个 operation 都可能抛出错误,我们只需要统一地记录错误信息,而不需要具体的错误对象。

错误原因捕获

错误原因捕获(Error Cause)是 JavaScript 在 ES2022(ECMAScript 2022)中引入的一项新特性,它允许在创建错误对象时,向其添加一个称为“原因”(cause)的属性。这个属性可以用来存储引发当前错误的底层错误信息,帮助开发者更好地调试和理解错误链。

基本用法

假设我们在一个函数中捕获到一个错误,并希望在抛出新的错误时保留原始错误信息:

function parseJson(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (originalError) {
    throw new Error('Failed to parse JSON', { cause: originalError });
  }
}

try {
  parseJson('invalid json');
} catch (error) {
  console.error(error.message); // 输出: "Failed to parse JSON"
  console.error(error.cause);   // 输出: SyntaxError: Unexpected token i in JSON at position 0
}

复杂示例

在一个更复杂的场景中,我们可能会有多个层级的错误,每个层级都捕获并重新抛出错误,同时保留原始错误信息:

function fetchData() {
  try {
    throw new Error('Network error');
  } catch (networkError) {
    throw new Error('Failed to fetch data', { cause: networkError });
  }
}

function processData() {
  try {
    fetchData();
  } catch (fetchError) {
    throw new Error('Failed to process data', { cause: fetchError });
  }
}

try {
  processData();
} catch (error) {
  console.error(error.message); // 输出: "Failed to process data"
  console.error(error.cause);   // 输出: Error: Failed to fetch data
  console.error(error.cause.cause); // 输出: Error: Network error
}

适用场景

  • 错误链跟踪:在复杂的应用中,捕获并重新抛出错误时,保留原始错误信息能够帮助更好地理解错误链。
  • 调试和日志记录:将底层错误信息包含在新的错误对象中,可以提供更多的上下文信息,有助于调试和日志记录。

可选链操作符 (?.)

可选链操作符(Optional Chaining Operator,?.)是 JavaScript 中的一种语法特性,用于安全地访问对象的嵌套属性。当属性不存在或值为 null 或 undefined 时,不会抛出错误,而是返回 undefined。这在处理复杂对象结构时非常有用,能够避免手动检查每个嵌套层级。

访问对象属性

假设我们有一个嵌套的对象:

const user = {
  name: 'Alice',
  address: {
    city: 'Wonderland'
  }
};

// 使用可选链操作符访问嵌套属性
const city = user?.address?.city;
console.log(city); // 输出: 'Wonderland'

// 访问不存在的嵌套属性
const zipCode = user?.address?.zipCode;
console.log(zipCode); // 输出: undefined

访问数组元素

const arr = [1, 2, 3];

// 使用可选链操作符访问数组元素
const value = arr?.[1];
console.log(value); // 输出: 2

// 访问不存在的数组元素
const undefinedValue = arr?.[5];
console.log(undefinedValue); // 输出: undefined

调用方法

const person = {
  greet: () => 'Hello!'
};

// 使用可选链操作符调用方法
const greeting = person?.greet?.();
console.log(greeting); // 输出: 'Hello!'

// 调用不存在的方法
const nonExistentGreeting = person?.sayBye?.();
console.log(nonExistentGreeting); // 输出: undefined

适用场景

  • 安全访问嵌套属性:在处理可能未定义的嵌套对象属性时,避免手动检查每个层级。
  • 数组元素访问:在访问数组元素时,如果数组可能为空或未定义,可以避免抛出错误。
  • 方法调用:在调用对象方法时,如果方法可能不存在,可以避免抛出错误。

注意事项

  • 链条中的非对象属性,如果链条中的某个属性不是对象或数组(例如字符串和数字),但你继续访问其下一级属性,将得到undefined。
  • 不改变原始类型,可选链操作符不会改变对象或数组的原始类型,它只会返回undefined 或调用链条中的下一级。

BigInt

BigInt 是 JavaScript 中的一种新的原始数据类型,用于表示任意精度的整数。这种类型在 ES2020(ECMAScript 2020)中引入,弥补了 JavaScript 中 Number 类型在表示超大整数时的局限性。

有两种方法可以创建 BigInt:

  • 使用BigInt 构造函数。
  • 使用带有n 后缀的整数字面量。

使用 BigInt 构造函数

const bigIntFromNumber = BigInt(123456789012345678901234567890);
console.log(bigIntFromNumber); // 输出: 123456789012345678901234567890n

使用带有 n 后缀的整数字面量

const bigIntLiteral = 123456789012345678901234567890n;
console.log(bigIntLiteral); // 输出: 123456789012345678901234567890n

算术操作

BigInt 支持基本的算术操作,如加法、减法、乘法、除法和取模:

const a = 123456789012345678901234567890n;
const b = 987654321098765432109876543210n;

console.log(a + b); // 输出: 1111111110111111111011111111100n
console.log(b - a); // 输出: 864197532086419753208641975320n
console.log(a * b); // 输出: 121932631137021795226185032733622923332237463801111263526900n
console.log(b / a); // 输出: 8n
console.log(b % a); // 输出: 9000000000900000000090n

比较操作

BigInt 也支持比较操作:

const a = 123456789012345678901234567890n;
const b = 987654321098765432109876543210n;

console.log(a < b);  // 输出: true
console.log(a > b);  // 输出: false
console.log(a === b); // 输出: false
console.log(a !== b); // 输出: true

与 Number 的混合使用

注意,BigInt 和 Number 之间不能直接进行混合运算。如果需要混合使用,可以先进行类型转换:

const bigIntValue = 123456789012345678901234567890n;
const numberValue = 20;

try {
  console.log(bigIntValue + numberValue); // 会抛出 TypeError
} catch (e) {
  console.error(e); // 输出: TypeError: Cannot mix BigInt and other types, use explicit conversions
}

// 显式转换
console.log(bigIntValue + BigInt(numberValue)); // 输出: 123456789012345678901234567910n
console.log(Number(bigIntValue) + numberValue); // 可能会导致精度丢失,输出: 1.2345678901234568e+29

适用场景

  • 处理超大整数:当需要处理超过Number 类型最大安全整数(2^53 – 1,即 9007199254740991)的数值时,可以使用 BigInt。
  • 精度要求高的计算:在需要高精度整数计算的场景中,如金融计算、密码学等。

空值合并操作符 (??)

空值合并操作符 (??) 是 JavaScript 中在 ES2020(ECMAScript 2020)中引入的一种新操作符,用于提供一种更简洁、直观的方法来处理可能为 null 或 undefined 的变量。它通常用于为变量提供默认值。

基本用法

let name = null;
let defaultName = 'Guest';

let displayName = name ?? defaultName;
console.log(displayName); // 输出: 'Guest'

在这个例子中,因为 name 的值为 null,所以 displayName 会取 defaultName 的值。

与逻辑或操作符(||)的比较

在很多情况下,我们可能会使用逻辑或操作符(||)来提供默认值,但它也会对其他假值(例如 0、”、false)进行短路。空值合并操作符则只关注 null 和 undefined。

let num = 0;
let defaultNum = 10;

let result1 = num || defaultNum;
console.log(result1); // 输出: 10,因为 0 被视为假值

let result2 = num ?? defaultNum;
console.log(result2); // 输出: 0,因为 0 不为 null 或 undefined

嵌套使用

空值合并操作符可以与其他逻辑操作符嵌套使用,以实现更复杂的逻辑:

let userSettings = { theme: null };
let defaultSettings = { theme: 'light' };

let theme = userSettings.theme ?? defaultSettings.theme ?? 'dark';
console.log(theme); // 输出: 'light'

在这个示例中,userSettings.theme 为 null,所以取 defaultSettings.theme 的值,如果 defaultSettings.theme 也为 null 或 undefined,则会最终取 ‘dark’ 的值。

使用场景

  • 默认值处理:在变量可能为null 或 undefined 时,提供一个默认值。
  • 配置合并:在应用中处理用户配置和默认配置的合并。
  • 简化代码:减少嵌套的三元运算符或逻辑或操作符(||)的使用,使代码更简洁易读。

注意事项

  • 优先级,空值合并操作符的优先级低于大多数其他操作符,但高于赋值操作符(=)。因此,在一些复杂表达式中,可能需要使用括号来明确操作顺序。
  • 禁止与链式运算符混用,在同一表达式中不能同时使用空值合并操作符(??)与逻辑与(&&)或逻辑或(||)运算符,否则会抛出语法错误。

正则表达式命名捕获组

命名捕获组是正则表达式中的一种功能,允许你为捕获组指定一个名称,从而使正则表达式的可读性更强,并使你在匹配结果中更方便地访问捕获的内容。这一功能在 ES2018(ECMAScript 2018)中引入。

命名捕获组的语法为:(?<name>pattern),其中 name 是捕获组的名称,pattern 是要匹配的正则表达式模式。

基本用法

假设我们有一个包含日期的字符串,并希望提取其中的年、月、日:

const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = regex.exec('2023-10-05');

console.log(result.groups.year);  // 输出: '2023'
console.log(result.groups.month); // 输出: '10'
console.log(result.groups.day);   // 输出: '05'

在这个示例中,命名捕获组 year、month 和 day 使得正则表达式更加清晰,并且我们可以通过 result.groups 对象方便地访问捕获的内容。

使用 match 方法

命名捕获组同样适用于 String.prototype.match 方法:

const str = '2023-10-05';
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = str.match(regex);

console.log(result.groups.year);  // 输出: '2023'
console.log(result.groups.month); // 输出: '10'
console.log(result.groups.day);   // 输出: '05'

使用 replace 方法

命名捕获组在 String.prototype.replace 方法中也很有用,可以使替换操作更加简洁:

const str = '2023-10-05';
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const newStr = str.replace(regex, '$<month>/$<day>/$<year>');
console.log(newStr); // 输出: '10/05/2023'

在这个例子中,我们使用命名捕获组来重新排列日期格式。

适用场景

  • 提高可读性:通过为捕获组指定名称,使正则表达式更具可读性。
  • 方便访问捕获的内容:通过groups 对象,可以更方便地访问捕获的内容,而不需要记住捕获组的索引。
  • 模板替换:在使用replace 方法进行字符串替换时,使替换操作更加直观和易于理解。

正则表达式 s 标志(dotAll 模式)

在 JavaScript 中,正则表达式的 s 标志(也称为 dotAll 模式)引入了一种新功能,使得 . 元字符可以匹配换行符。本文将简单介绍这个标志的作用、用法以及一些示例。

默认情况下,在正则表达式中,. 元字符匹配除换行符(如 \n 和 \r)之外的任何单个字符:

const regex = /.*/;
const str = 'Hello\nWorld';

console.log(str.match(regex)); // 输出: ["Hello"]

在上面的示例中,. 只匹配到 Hello,因为换行符 \n 阻断了匹配。

引入 s 标志

s 标志(dotAll 模式)允许 . 元字符匹配包括换行符在内的任何字符:

const regex = /.*./s;
const str = 'Hello\nWorld';

console.log(str.match(regex)); // 输出: ["Hello\nWorld"]

在这个示例中,使用 s 标志后,. 可以匹配换行符,因此整个字符串 Hello\nWorld 都被匹配到了。

在正则表达式中使用 s 标志的语法非常简单,只需要在正则表达式的末尾添加 s:

const regex = /pattern/s;

单行字符串

const regex = /Hello.World/s;
const str = 'Hello\nWorld';

console.log(regex.test(str)); // 输出: true

在这个示例中,Hello.World 在开启 s 标志后能够匹配包含换行符的字符串 Hello\nWorld。

多行字符串

const multilineStr = `
Line 1
Line 2
Line 3
`;

const regex = /Line.*Line/s;

console.log(regex.test(multilineStr)); // 输出: true

在这个示例中,正则表达式 Line.*Line 在开启 s 标志后能够匹配多行字符串 multilineStr 中的所有内容。

适用场景

  • 多行文本匹配:当需要匹配跨多行的文本时,使用s 标志可以简化正则表达式的编写。
  • 提升匹配能力:通过允许. 元字符匹配换行符,可以提升正则表达式的匹配能力,适用于处理复杂文本数据。

注意事项

  • 性能考虑:启用s 标志后,由于 . 可以匹配所有字符(包括换行符),因此可能会对性能有一定影响,特别是在处理大文本时需要谨慎。
  • 与m 标志的区别:m 标志(多行模式)与 s 标志不同,m 标志改变了 ^ 和 $ 的行为,使它们可以匹配行首和行尾,而 s 标志改变了 . 的行为,使它可以匹配换行符。

正则表达式 d 标志(匹配索引)

在 JavaScript 中,d 标志是提议中的一个正则表达式新特性,用于启用匹配索引(match indices)。

以下是一些示例来展示其用法:

查找匹配的索引

const regex = /(foo)(bar)/d; // 使用 d 标志
const str = 'foobar';
const result = regex.exec(str);

console.log(result.indices); 
// 假设输出: [ [ 0, 6 ], [ 0, 3 ], [ 3, 6 ] ]
// [ 0, 6 ] 是整个匹配的索引范围
// [ 0, 3 ] 是第一个捕获组 (foo) 的索引范围
// [ 3, 6 ] 是第二个捕获组 (bar) 的索引范围

在这个示例中,result.indices 将包含每个匹配和捕获组的起始和结束索引。

多重匹配

const regex = /(a+)(b+)/d;
const str = 'aaabbb';
const result = regex.exec(str);

console.log(result.indices);
// 假设输出: [ [ 0, 6 ], [ 0, 3 ], [ 3, 6 ] ]
// [ 0, 6 ] 是整个匹配的索引范围
// [ 0, 3 ] 是第一个捕获组 (aaa) 的索引范围
// [ 3, 6 ] 是第二个捕获组 (bbb) 的索引范围

适用场景

  • 代码编辑器和语法高亮:可以更精确地定位匹配位置,从而实现更复杂的语法高亮和编辑功能。
  • 文本处理:在文本替换、提取和分析中更精确地定位匹配位置,使操作更高效。
  • 调试和日志记录:在调试正则表达式时,了解确切的匹配位置有助于更快地发现问题。

指数运算符 **

指数运算符(**)用于计算一个数的指数(幂次),它与 Math.pow 方法功能相同,但语法更简洁。

语法:

base ** exponent
  • base:基数。
  • exponent:指数。

示例:

console.log(2 ** 3); // 8,相当于 Math.pow(2, 3)
console.log(5 ** 2); // 25,相当于 Math.pow(5, 2)
console.log(7 ** 0); // 1,任何数的 0 次幂都为 1

与 Math.pow 对比,指数运算符语法更简洁,更易读。

console.log(Math.pow(2, 3)); // 8
console.log(2 ** 3); // 8

逻辑赋值运算符

逻辑赋值运算符是 JavaScript 中在 ES2021(ECMAScript 2021)中引入的一组新的运算符,用于简化常见的赋值逻辑操作。这些运算符把逻辑操作符(如 ||, &&, ??)与赋值操作符 = 结合起来,以更简洁的方式实现一些常见的操作。

三种逻辑赋值运算符

  • 逻辑或赋值运算符(||=)
  • 逻辑与赋值运算符(&&=)
  • 空值合并赋值运算符(??=)

数值分隔符

数值分隔符(Numeric Separators)是 JavaScript 中在 ES2021(ECMAScript 2021)中引入的一项新特性,用于提高代码中数值的可读性。通过使用下划线(_)作为分隔符,可以更直观地表示大数值或分组数字,使代码更易于理解。

数值分隔符使用下划线(_)来分隔数值中的一部分。数值分隔符可以在不同类型的数值中使用,包括整数、小数、二进制、八进制和十六进制。

整数

let largeNumber = 1_000_000;
console.log(largeNumber); // 输出: 1000000

小数

let preciseNumber = 3.141_592_653_589_793;
console.log(preciseNumber); // 输出: 3.141592653589793

二进制数

let binaryNumber = 0b1010_0001_1000_0101;
console.log(binaryNumber); // 输出: 41349

八进制数

let octalNumber = 0o1234_5670;
console.log(octalNumber); // 输出: 3426864

十六进制数

let hexNumber = 0xA0_B0_C0_D0;
console.log(hexNumber); // 输出: 2694881360

注意事项

  • 仅用于提高可读性:数值分隔符仅用于提高代码可读性,不影响数值的实际值。
  • 不能出现在数值的开头或结尾:下划线不能出现在数值的开头或结尾,且不能连续使用。
  • 与数值类型无关:数值分隔符可以与任何数值类型(整数、小数、二进制、八进制、十六进制)一起使用。

适用场景

  • 大数值:在表示大数值时,使用数值分隔符可以帮助快速识别数字的大小,特别是在财务、科学计算等场景中。
  • 分组数字:在表示长的数字序列(如银行卡号、电话号码等)时,使用数值分隔符可以提高可读性。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注