自 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的场景,如事件处理、定时器、异步操作等。
document.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: 'NewYork', zip: '10001' } }; const {name, address: {city, zip}} = user; console.log(name); // 输出: "Alice" console.log(city); // 输出: "NewYork" 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(ECMAScript2015)引入,用于创建唯一且不可变的标识符。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(待定):初始状态,既没有被兑现,也没有被拒绝。
- Rejected(已拒绝):操作失败。
Fulfilled(已兑现):操作成功完成。
状态一旦从 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(ECMAScript2021)引入的一种新的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('Error1'); const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success2')); const promise3 = new Promise((resolve) => setTimeout(resolve, 200, 'Success3')); Promise.any([promise1, promise2, promise3]) .then((value) => { console.log('Resolved with value:', value); // 输出: Resolved with value: Success2 }) .catch((error) => { console.error('Rejected with error:', error); });
在这个示例中,promise2是第一个成功的Promise,所以Promise.any返回promise2的值,即Success2。
处理所有 Promise 都被拒绝的情况
如果所有传入的Promise都被拒绝,Promise.any返回一个AggregateError,包含所有拒绝的原因。
const promise1 = Promise.reject('Error1'); const promise2 = Promise.reject('Error2'); const promise3 = Promise.reject('Error3'); 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: ['Error1', 'Error2', 'Error3']
实际应用场景
快速响应策略
在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, 'Task1 failed'); }); } function task2() { return new Promise((resolve, reject) => { setTimeout(resolve, 200, 'Task2 succeeded'); }); } function task3() { return new Promise((resolve, reject) => { setTimeout(resolve, 300, 'Task3 succeeded'); }); } Promise.any([task1(), task2(), task3()]) .then((result) => { console.log('First successful task:', result); //输出: First successful task: Task2 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('Engine1 failed'), 100); }); } function searchEngine2(query) { return new Promise((resolve) => { setTimeout(() => resolve(`Engine2 results for "${query}"`), 200); }); } function searchEngine3(query) { return new Promise((resolve) => { setTimeout(() => resolve(`Engine3 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: Engine2 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('Data1'); console.log(result1); //输出:'Data1' const result2 = await Promise.resolve('Data2'); console.log(result2); //输出:'Data2' } fetchData();
在上面的示例中,result1和result2将按顺序依次输出。
并行执行多个异步操作
有时你可能希望并行执行多个异步操作,而不是一个接一个地执行。这时可以使用Promise.all。
async function fetchData() { const [result1, result2] = await Promise.all([ Promise.resolve('Data1'), Promise.resolve('Data2') ]); console.log(result1); //输出:'Data1' console.log(result2); //输出:'Data2' } fetchData();
在上面的示例中,result1和result2是并行执行的,所有Promise都解决后才继续执行后续代码。
顶级await
顶级await是ECMAScript2022(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('ModuleA start'); await new Promise((resolve) => setTimeout(resolve, 1000)); console.log('ModuleA end'); //moduleB.js import './moduleA.js'; console.log('ModuleB');
在上面的示例中,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('Data1'), 1000)); } function fetchData2() { return new Promise((resolve) => setTimeout(() => resolve('Data2'), 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 的区别
- for...in 遍历对象的可枚举属性,包括继承的属性。
- for...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 引入的特性,需要确保目标环境支持。
- 性能注意:异步迭代器在处理大量数据时需要注意性能,尤其是在频繁进行异步操作的情况下。
- 错误处理:异步迭代器中可能会抛出错误,需要使用 try...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 json1'), () => { throw new Error('Some error'); }, () => JSON.parse('invalid json2') ]; 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 = /(?\d{4})-(? \d{2})-(? \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 = /(?\d{4})-(? \d{2})-(? \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 = /(?\d{4})-(? \d{2})-(? \d{2})/; const newStr = str.replace(regex, '$ /$ /$ '); 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 = ` Line1 Line2 Line3 `; 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
注意事项
- 仅用于提高可读性:数值分隔符仅用于提高代码可读性,不影响数值的实际值。
- 不能出现在数值的开头或结尾:下划线不能出现在数值的开头或结尾,且不能连续使用。
- 与数值类型无关:数值分隔符可以与任何数值类型(整数、小数、二进制、八进制、十六进制)一起使用。
适用场景
- 大数值:在表示大数值时,使用数值分隔符可以帮助快速识别数字的大小,特别是在财务、科学计算等场景中。
- 分组数字:在表示长的数字序列(如银行卡号、电话号码等)时,使用数值分隔符可以提高可读性。