ES6/ES2015 - ES16/ES2025
ECMAScript(简称ES)是JavaScript的官方标准,从2015年开始每年发布一个新版本。
版本一览表
年份 | 版本 | 主要新特性 |
---|---|---|
2015 | ES6/ES2015 | let/const、箭头函数、Class、模板字符串、解构赋值、模块、Promise |
2016 | ES7/ES2016 | 指数运算符、Array.includes() |
2017 | ES8/ES2017 | async/await、Object.entries/values、padStart/padEnd |
2018 | ES9/ES2018 | 异步迭代、Promise.finally()、扩展运算符增强 |
2019 | ES10/ES2019 | Array.flat()、Object.fromEntries()、可选catch |
2020 | ES11/ES2020 | BigInt、空值合并??、可选链?. |
2021 | ES12/ES2021 | Promise.any()、replaceAll()、逻辑赋值操作符 |
2022 | ES13/ES2022 | 私有字段、顶层await、Error.cause |
2023 | ES14/ES2023 | 数组不可变方法、Hashbang、Symbols做WeakMap键 |
2024 | ES15/ES2024 | 分组API、Promise.withResolvers()、Temporal API |
2025 | ES16/ES2025 | Iterator helpers、Promise.try()(提案中) |
ES2015 (ES6) - JavaScript的重大革新
ES6是JavaScript历史上最重要的更新,引入了大量现代编程特性。
1. let 和 const - 块级作用域
问题背景: var
存在变量提升和函数作用域问题。
// var 的问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出三个3
}
// let 解决方案
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出0, 1, 2
}
// const 常量声明
const PI = 3.14159;
// PI = 3.14; // TypeError: Assignment to constant variable
// const 对象可以修改内容
const user = { name: 'Alice' };
user.name = 'Bob'; // 允许
// user = {}; // 错误:不能重新赋值
2. 箭头函数 - 简洁的函数语法
// 传统函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
// 单参数可省略括号
const square = x => x * x;
// 多行函数体需要大括号和return
const greet = name => {
const message = `Hello, ${name}!`;
return message.toUpperCase();
};
// this绑定的区别
class Timer {
constructor() {
this.seconds = 0;
}
// 传统函数:this指向调用者
start1() {
setInterval(function() {
this.seconds++; // this指向window/global
}, 1000);
}
// 箭头函数:this绑定到定义时的上下文
start2() {
setInterval(() => {
this.seconds++; // this指向Timer实例
}, 1000);
}
}
3. Class类 - 面向对象编程
// ES6类语法
class Animal {
// 构造函数
constructor(name, species) {
this.name = name;
this.species = species;
}
// 实例方法
speak() {
return `${this.name} makes a sound`;
}
// 静态方法
static getSpeciesCount() {
return Animal.count || 0;
}
}
// 继承
class Dog extends Animal {
constructor(name, breed) {
super(name, 'Canine'); // 调用父类构造函数
this.breed = breed;
}
// 重写父类方法
speak() {
return `${this.name} barks`;
}
// 新方法
wagTail() {
return `${this.name} wags tail happily`;
}
}
const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.speak()); // "Buddy barks"
console.log(myDog.wagTail()); // "Buddy wags tail happily"
4. 模板字符串 - 字符串插值
const name = 'Alice';
const age = 30;
// 传统字符串拼接
const oldWay = 'Hello, my name is ' + name + ' and I am ' + age + ' years old.';
// 模板字符串
const newWay = `Hello, my name is ${name} and I am ${age} years old.`;
// 多行字符串
const multiLine = `
这是第一行
这是第二行
当前时间:${new Date().toISOString()}
`;
// 表达式和函数调用
const price = 99.99;
const message = `商品价格:$${price.toFixed(2)},折扣后:$${(price * 0.8).toFixed(2)}`;
// 标签模板
function highlight(strings, ...values) {
return strings.reduce((result, string, i) => {
return result + string + (values[i] ? `<mark>${values[i]}</mark>` : '');
}, '');
}
const product = 'iPhone';
const count = 3;
const html = highlight`您有 ${count} 个 ${product} 在购物车中`;
// "您有 <mark>3</mark> 个 <mark>iPhone</mark> 在购物车中"
5. 解构赋值 - 提取数据的便捷方式
// 数组解构
const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
console.log(first); // 'red'
// 跳过元素
const [, , blue] = colors;
console.log(blue); // 'blue'
// 剩余元素
const [primary, ...secondary] = colors;
console.log(secondary); // ['green', 'blue']
// 对象解构
const person = {
name: 'Alice',
age: 30,
city: 'New York',
country: 'USA'
};
const { name, age } = person;
console.log(name, age); // 'Alice' 30
// 重命名变量
const { name: userName, city: location } = person;
console.log(userName, location); // 'Alice' 'New York'
// 默认值
const { height = 170 } = person;
console.log(height); // 170
// 嵌套解构
const user = {
id: 1,
profile: {
name: 'Bob',
settings: {
theme: 'dark'
}
}
};
const { profile: { name: profileName, settings: { theme } } } = user;
console.log(profileName, theme); // 'Bob' 'dark'
// 函数参数解构
function greetUser({ name, age = 18 }) {
return `Hello ${name}, you are ${age} years old`;
}
greetUser({ name: 'Charlie', age: 25 }); // "Hello Charlie, you are 25 years old"
6. 模块系统 - import/export
// math.js - 导出模块
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// 默认导出
export default function divide(a, b) {
return a / b;
}
// main.js - 导入模块
import divide, { PI, add, multiply } from './math.js';
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
console.log(divide(10, 2)); // 5
// 重命名导入
import { add as sum } from './math.js';
// 导入全部
import * as MathUtils from './math.js';
console.log(MathUtils.add(1, 2)); // 3
7. Promise - 异步编程
// 创建Promise
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve({ data: 'Hello World' });
} else {
reject(new Error('获取数据失败'));
}
}, 1000);
});
};
// 使用Promise
fetchData()
.then(result => {
console.log('成功:', result.data);
return result.data.toUpperCase();
})
.then(upperData => {
console.log('转换后:', upperData);
})
.catch(error => {
console.log('错误:', error.message);
})
.finally(() => {
console.log('请求完成');
});
// Promise链式调用
Promise.resolve(1)
.then(x => x + 1)
.then(x => x * 2)
.then(x => console.log(x)); // 4
// Promise.all - 并行执行
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then(values => {
console.log(values); // [1, 2, 3]
});
// Promise.race - 竞态
const slow = new Promise(resolve => setTimeout(() => resolve('慢'), 1000));
const fast = new Promise(resolve => setTimeout(() => resolve('快'), 100));
Promise.race([slow, fast])
.then(result => {
console.log(result); // '快'
});
ES2016 (ES7) - 小而精的更新
1. 指数运算符 (**)
// 传统方式
Math.pow(2, 3); // 8
// ES2016 指数运算符
2 ** 3; // 8
2 ** 0.5; // 1.4142135623730951 (平方根)
// 支持复合赋值
let x = 2;
x **= 3; // 相当于 x = x ** 3
console.log(x); // 8
// 优先级高于一元运算符
-2 ** 2; // -4 (相当于 -(2 ** 2))
(-2) ** 2; // 4
2. Array.prototype.includes()
const fruits = ['apple', 'banana', 'orange'];
// ES2016之前
fruits.indexOf('banana') !== -1; // true
fruits.indexOf('grape') !== -1; // false
// ES2016 includes方法
fruits.includes('banana'); // true
fruits.includes('grape'); // false
// 处理NaN的区别
const numbers = [1, 2, NaN, 4];
numbers.indexOf(NaN); // -1 (找不到NaN)
numbers.includes(NaN); // true (正确找到NaN)
// 从指定位置开始查找
const letters = ['a', 'b', 'c', 'a'];
letters.includes('a', 2); // false (从索引2开始找,没找到'a')
letters.includes('a', 3); // true (从索引3开始找,找到了'a')
ES2017 (ES8) - 异步编程的革命
1. async/await - 异步代码同步写法
// Promise方式
function fetchUserData() {
return fetch('/api/user')
.then(response => response.json())
.then(user => {
console.log('用户:', user);
return fetch(`/api/posts/${user.id}`);
})
.then(response => response.json())
.then(posts => {
console.log('帖子:', posts);
return posts;
})
.catch(error => {
console.log('错误:', error);
});
}
// async/await方式
async function fetchUserDataAsync() {
try {
const userResponse = await fetch('/api/user');
const user = await userResponse.json();
console.log('用户:', user);
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
console.log('帖子:', posts);
return posts;
} catch (error) {
console.log('错误:', error);
}
}
// 并行执行多个异步操作
async function fetchMultipleData() {
try {
// 并行执行
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
} catch (error) {
console.log('获取数据失败:', error);
}
}
// 循环中使用await
async function processItems(items) {
// 串行处理
for (const item of items) {
await processItem(item);
}
// 并行处理
await Promise.all(items.map(item => processItem(item)));
}
2. Object.entries() 和 Object.values()
const person = {
name: 'Alice',
age: 30,
city: 'New York'
};
// Object.keys() (ES5已有)
Object.keys(person); // ['name', 'age', 'city']
// Object.values() (ES2017新增)
Object.values(person); // ['Alice', 30, 'New York']
// Object.entries() (ES2017新增)
Object.entries(person); // [['name', 'Alice'], ['age', 30], ['city', 'New York']]
// 实际应用场景
// 遍历对象
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}
// 对象转Map
const personMap = new Map(Object.entries(person));
// 过滤对象属性
const filteredEntries = Object.entries(person)
.filter(([key, value]) => typeof value === 'string');
const filteredObject = Object.fromEntries(filteredEntries);
console.log(filteredObject); // { name: 'Alice', city: 'New York' }
// 计算对象属性数量
const counts = { apple: 5, banana: 3, orange: 8 };
const total = Object.values(counts).reduce((sum, count) => sum + count, 0);
console.log(total); // 16
3. String.prototype.padStart() 和 padEnd()
// padStart - 在开头填充
'5'.padStart(3, '0'); // '005'
'hello'.padStart(10, '*'); // '*****hello'
// padEnd - 在结尾填充
'5'.padEnd(3, '0'); // '500'
'hello'.padEnd(10, '*'); // 'hello*****'
// 实际应用场景
// 格式化数字
const formatNumber = (num, length = 4) => {
return String(num).padStart(length, '0');
};
formatNumber(5); // '0005'
formatNumber(123); // '0123'
// 格式化时间
const formatTime = (hours, minutes, seconds) => {
const h = String(hours).padStart(2, '0');
const m = String(minutes).padStart(2, '0');
const s = String(seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
};
formatTime(9, 5, 3); // '09:05:03'
// 创建简单的表格对齐
const data = [
{ name: 'Alice', score: 95 },
{ name: 'Bob', score: 87 },
{ name: 'Charlie', score: 92 }
];
data.forEach(({ name, score }) => {
const formattedName = name.padEnd(10, ' ');
const formattedScore = String(score).padStart(3, ' ');
console.log(`${formattedName} ${formattedScore}`);
});
// Alice 95
// Bob 87
// Charlie 92
ES2018 (ES9) - 异步迭代和增强功能
1. 异步迭代 (for await…of)
// 创建异步迭代器
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
// 使用 for await...of
async function processAsyncData() {
for await (const value of asyncGenerator()) {
console.log(value); // 1, 2, 3 (依次输出)
}
}
// 处理异步数据流
async function fetchUserPosts(userIds) {
async function* postGenerator() {
for (const userId of userIds) {
const response = await fetch(`/api/users/${userId}/posts`);
const posts = await response.json();
yield posts;
}
}
for await (const posts of postGenerator()) {
console.log('处理用户帖子:', posts);
}
}
// 读取文件流 (Node.js环境)
const fs = require('fs');
async function readLargeFile() {
const stream = fs.createReadStream('large-file.txt', { encoding: 'utf8' });
for await (const chunk of stream) {
console.log('读取到数据块:', chunk.length);
// 处理数据块
}
}
2. Promise.prototype.finally()
let isLoading = true;
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('网络请求失败');
}
return response.json();
})
.then(data => {
console.log('数据获取成功:', data);
// 处理成功逻辑
})
.catch(error => {
console.error('请求失败:', error);
// 处理错误逻辑
})
.finally(() => {
isLoading = false; // 无论成功失败都会执行
console.log('请求完成,隐藏加载状态');
});
// 实际应用:资源清理
async function processFile(filename) {
let fileHandle;
try {
fileHandle = await openFile(filename);
const data = await fileHandle.read();
return processData(data);
} catch (error) {
console.error('文件处理失败:', error);
throw error;
} finally {
// 确保文件句柄被关闭
if (fileHandle) {
await fileHandle.close();
}
}
}
3. 对象扩展运算符
// 对象扩展运算符
const baseConfig = {
host: 'localhost',
port: 3000,
debug: false
};
const devConfig = {
...baseConfig,
debug: true,
hot: true
};
console.log(devConfig);
// {
// host: 'localhost',
// port: 3000,
// debug: true,
// hot: true
// }
// 合并多个对象
const user = { name: 'Alice', age: 30 };
const preferences = { theme: 'dark', language: 'en' };
const permissions = { admin: false, editor: true };
const fullProfile = { ...user, ...preferences, ...permissions };
// 对象的浅拷贝
const originalUser = { name: 'Bob', hobbies: ['reading', 'swimming'] };
const clonedUser = { ...originalUser };
// 注意:扩展运算符是浅拷贝
clonedUser.hobbies.push('cooking'); // 会影响原对象的hobbies数组
// 深拷贝需要其他方法
const deepClone = JSON.parse(JSON.stringify(originalUser));
// 函数参数中的对象扩展
function createUser(defaults, overrides) {
return {
id: Math.random(),
createdAt: new Date(),
...defaults,
...overrides
};
}
const newUser = createUser(
{ name: 'Unknown', role: 'user' },
{ name: 'Charlie', email: 'charlie@example.com' }
);
4. 正则表达式增强
// 命名捕获组
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2023-12-25'.match(dateRegex);
console.log(match.groups.year); // '2023'
console.log(match.groups.month); // '12'
console.log(match.groups.day); // '25'
// replace中使用命名捕获组
const formatted = '2023-12-25'.replace(dateRegex, '$<day>/$<month>/$<year>');
console.log(formatted); // '25/12/2023'
// s 标志 (dotAll) - 让.匹配任何字符包括换行符
const multilineText = `第一行
第二行
第三行`;
const regex1 = /第一行.第二行/; // 不匹配,因为.不匹配换行符
const regex2 = /第一行.第二行/s; // 匹配,s标志让.匹配换行符
console.log(regex1.test(multilineText)); // false
console.log(regex2.test(multilineText)); // true
// 正向否定断言和正向肯定断言
const password = 'myPassword123';
// 正向肯定断言:必须包含数字
const hasNumber = /(?=.*\d)/;
console.log(hasNumber.test(password)); // true
// 正向否定断言:不能包含特殊字符
const noSpecialChar = /^(?!.*[!@#$%^&*]).*$/;
console.log(noSpecialChar.test(password)); // true
ES2019 (ES10) - 数组和对象处理增强
1. Array.prototype.flat() 和 flatMap()
// 数组扁平化
const nestedArray = [1, [2, 3], [4, [5, 6]]];
// flat() - 默认深度为1
nestedArray.flat(); // [1, 2, 3, 4, [5, 6]]
// 指定扁平化深度
nestedArray.flat(2); // [1, 2, 3, 4, 5, 6]
// 完全扁平化
const deepNested = [1, [2, [3, [4, [5]]]]];
deepNested.flat(Infinity); // [1, 2, 3, 4, 5]
// flatMap() = map + flat
const sentences = ['Hello world', 'How are you', 'Nice day'];
// 传统方式
sentences.map(s => s.split(' ')).flat();
// [['Hello', 'world'], ['How', 'are', 'you'], ['Nice', 'day']] ->
// ['Hello', 'world', 'How', 'are', 'you', 'Nice', 'day']
// flatMap方式
sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'How', 'are', 'you', 'Nice', 'day']
// 实际应用:处理用户评论
const users = [
{ name: 'Alice', comments: ['Great post!', 'Thanks for sharing'] },
{ name: 'Bob', comments: ['Interesting', 'I agree'] },
{ name: 'Charlie', comments: ['Nice work'] }
];
// 获取所有评论
const allComments = users.flatMap(user => user.comments);
console.log(allComments);
// ['Great post!', 'Thanks for sharing', 'Interesting', 'I agree', 'Nice work']
// 复杂的flatMap用例:数据处理管道
const orders = [
{ id: 1, items: [{ name: 'laptop', price: 1000 }, { name: 'mouse', price: 25 }] },
{ id: 2, items: [{ name: 'keyboard', price: 100 }] }
];
const expensiveItems = orders
.flatMap(order => order.items)
.filter(item => item.price > 50)
.map(item => item.name);
console.log(expensiveItems); // ['laptop', 'keyboard']
2. Object.fromEntries()
// 将键值对数组转换为对象
const entries = [['name', 'Alice'], ['age', 30], ['city', 'New York']];
const person = Object.fromEntries(entries);
console.log(person); // { name: 'Alice', age: 30, city: 'New York' }
// 与Object.entries()配合使用
const originalObj = { a: 1, b: 2, c: 3 };
// 对对象的值进行转换
const doubledObj = Object.fromEntries(
Object.entries(originalObj).map(([key, value]) => [key, value * 2])
);
console.log(doubledObj); // { a: 2, b: 4, c: 6 }
// 过滤对象属性
const user = {
name: 'Bob',
age: 25,
email: 'bob@example.com',
password: 'secret123',
isAdmin: false
};
// 创建公开用户信息(移除敏感信息)
const publicUser = Object.fromEntries(
Object.entries(user).filter(([key]) => key !== 'password')
);
// Map转对象
const userMap = new Map([
['id', 123],
['name', 'Charlie'],
['email', 'charlie@example.com']
]);
const userObj = Object.fromEntries(userMap);
console.log(userObj); // { id: 123, name: 'Charlie', email: 'charlie@example.com' }
// 查询参数处理
const searchParams = new URLSearchParams('?name=Alice&age=30&city=NYC');
const paramsObj = Object.fromEntries(searchParams);
console.log(paramsObj); // { name: 'Alice', age: '30', city: 'NYC' }
3. String.prototype.trimStart() 和 trimEnd()
const messyString = ' Hello World ';
// ES5方法
messyString.trim(); // 'Hello World' (移除两端空格)
// ES2019新方法
messyString.trimStart(); // 'Hello World ' (只移除开头空格)
messyString.trimEnd(); // ' Hello World' (只移除结尾空格)
// 别名方法(为了兼容性)
messyString.trimLeft(); // 等同于trimStart()
messyString.trimRight(); // 等同于trimEnd()
// 实际应用:处理用户输入
function processUserInput(input) {
// 移除开头空格,但保留结尾的换行符或空格(可能有格式意义)
return input.trimStart();
}
// 处理代码格式化
const codeLines = [
' function example() {',
' console.log("hello");',
' }'
];
// 移除统一的缩进
const minIndent = Math.min(
...codeLines
.filter(line => line.trim())
.map(line => line.length - line.trimStart().length)
);
const formattedCode = codeLines.map(line =>
line.trimStart().padStart(line.trimStart().length + Math.max(0, line.length - line.trimStart().length - minIndent), ' ')
);
4. 可选的 catch 参数
// ES2019之前:必须提供catch参数
try {
JSON.parse(invalidJson);
} catch (error) {
console.log('解析失败'); // 即使不使用error参数也必须声明
}
// ES2019:catch参数可选
try {
JSON.parse(invalidJson);
} catch {
console.log('解析失败'); // 不需要error参数
}
// 实际应用:功能检测
let supportsLocalStorage = false;
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
supportsLocalStorage = true;
} catch {
// 忽略错误,仅用于检测是否支持localStorage
supportsLocalStorage = false;
}
// 另一个场景:忽略特定错误
function loadConfig() {
try {
// 尝试读取本地配置
return JSON.parse(localStorage.getItem('appConfig'));
} catch {
// 配置读取失败或解析错误时,返回默认配置
return { theme: 'light', fontSize: 16 };
}
}
5. Function.prototype.toString() 增强
// ES2019之前:toString()会省略注释和空格
function add(a, b) {
// 这是加法函数
return a + b;
}
// ES2019之前的输出(可能):"function add(a, b) { return a + b; }"
// ES2019的输出:保留原始格式
console.log(add.toString());
// 输出:
// function add(a, b) {
// // 这是加法函数
// return a + b;
// }
// 箭头函数也支持
const multiply = (a, b) => {
/* 乘法函数 */
return a * b;
};
console.log(multiply.toString());
// 输出:
// (a, b) => {
// /* 乘法函数 */
// return a * b;
// }
ES2020 (ES11) - 解决实际开发痛点
1. BigInt - 任意精度整数
// 传统Number的限制(2^53 - 1)
const maxSafeInt = Number.MAX_SAFE_INTEGER;
console.log(maxSafeInt); // 9007199254740991
// 超出安全整数范围的计算会失真
console.log(maxSafeInt + 1); // 9007199254740992
console.log(maxSafeInt + 2); // 9007199254740992(错误结果)
// BigInt解决方案
const bigInt1 = 9007199254740991n; // 后缀n表示BigInt
const bigInt2 = BigInt(9007199254740991); // 构造函数方式
// 正确的大整数计算
console.log(bigInt1 + 1n); // 9007199254740992n
console.log(bigInt1 + 2n); // 9007199254740993n
// 大整数比较(需类型一致)
console.log(10n === 10); // false(类型不同)
console.log(10n === BigInt(10)); // true
// 实际应用:处理超大ID或金额
const transactionId = 123456789012345678901234567890n;
const largeAmount = 999999999999999999999999999n;
// 注意:BigInt不能与Number直接运算
// console.log(10n + 5); // TypeError
console.log(10n + BigInt(5)); // 15n
console.log(Number(10n) + 5); // 15(需确保值在安全范围内)
2. 空值合并运算符 (??)
// 传统逻辑运算符的问题(0、''、false会被误判)
const count = 0;
const displayCount = count || '未知'; // '未知'(错误,0是有效数值)
const username = '';
const displayName = username || '匿名用户'; // '匿名用户'(错误,空字符串是有效名称)
// 空值合并运算符:仅当左侧为null/undefined时才使用右侧值
const displayCount2 = count ?? '未知'; // 0(正确)
const displayName2 = username ?? '匿名用户'; // ''(正确)
const age = null;
const displayAge = age ?? 18; // 18(正确,null使用默认值)
const height = undefined;
const displayHeight = height ?? 170; // 170(正确,undefined使用默认值)
// 实际应用:配置项默认值
function createConfig(options) {
return {
theme: options.theme ?? 'light',
fontSize: options.fontSize ?? 16,
showSidebar: options.showSidebar ?? true // false会被保留
};
}
// 与可选链配合使用
const user = {
name: 'Alice',
address: {
city: 'New York'
// zipCode未定义
}
};
const zipCode = user.address.zipCode ?? '未填写';
console.log(zipCode); // '未填写'
3. 可选链运算符 (?.)
// 传统嵌套属性访问的问题
const user = {
name: 'Alice',
address: {
city: 'New York'
// street未定义
}
};
// 传统方式:需要层层判断
const street = user.address && user.address.street && user.address.street.name;
console.log(street); // undefined(无错误,但代码繁琐)
// 可选链方式:简洁安全
const street2 = user.address?.street?.name;
console.log(street2); // undefined(无错误,代码简洁)
// 数组元素访问
const users = [
{ name: 'Bob', age: 25 },
// 第二个元素未定义
{ name: 'Charlie', age: 30 }
];
const secondUserName = users[1]?.name;
console.log(secondUserName); // undefined(无错误)
const fourthUserName = users[3]?.name;
console.log(fourthUserName); // undefined(无错误)
// 函数调用安全
const utils = {
calculate: (a, b) => a + b
// format未定义
};
// 传统方式:需判断函数是否存在
const result1 = utils.format && utils.format('hello');
// 可选链方式
const result2 = utils.format?.('hello');
console.log(result2); // undefined(无错误)
// 实际应用:API数据处理
async function fetchUserName(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// 安全获取嵌套属性,无需担心数据结构变化
return data?.user?.profile?.name ?? '未知用户';
}
4. String.prototype.matchAll()
const text = 'Hello 123, World 456! Welcome 789';
const regex = /\d+/g; // 匹配所有数字
// 传统方式:多次调用exec()
let match;
const numbers1 = [];
while ((match = regex.exec(text)) !== null) {
numbers1.push(match[0]);
}
console.log(numbers1); // ['123', '456', '789']
// ES2020 matchAll():返回迭代器
const matches = text.matchAll(regex);
// 转换为数组
const numbers2 = [...matches];
console.log(numbers2);
// [
// ['123', index: 6, input: 'Hello 123, World 456! Welcome 789', groups: undefined],
// ['456', index: 15, input: 'Hello 123, World 456! Welcome 789', groups: undefined],
// ['789', index: 28, input: 'Hello 123, World 456! Welcome 789', groups: undefined]
// ]
// 配合for...of循环
const numbers3 = [];
for (const match of text.matchAll(regex)) {
numbers3.push(match[0]);
}
// 命名捕获组场景
const dateText = '今天是2023-10-05,明天是2023-10-06';
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
const dates = [];
for (const match of dateText.matchAll(dateRegex)) {
dates.push({
year: match.groups.year,
month: match.groups.month,
day: match.groups.day
});
}
console.log(dates);
// [
// { year: '2023', month: '10', day: '05' },
// { year: '2023', month: '10', day: '06' }
// ]
5. 动态导入 (import())
// 传统静态导入:无论是否需要都会加载
import { heavyFunction } from './heavy-module.js';
// ES2020动态导入:按需加载
async function loadHeavyModule() {
try {
// 动态导入返回Promise
const heavyModule = await import('./heavy-module.js');
heavyModule.heavyFunction(); // 调用模块中的函数
// 解构导入
const { utilityFunction } = await import('./utils.js');
utilityFunction();
} catch (error) {
console.error('模块加载失败:', error);
}
}
// 实际应用:路由懒加载(React/Vue场景)
// React示例
function Home() {
return <div>首页</div>;
}
// 懒加载其他路由组件
const About = React.lazy(() => import('./About.js'));
const Contact = React.lazy(() => import('./Contact.js'));
function App() {
return (
<Router>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} /> {/* 按需加载 */}
<Route path="/contact" element={<Contact />} /> {/* 按需加载 */}
</Routes>
</Suspense>
</Router>
);
}
// 条件加载
async function loadFeatureModule(feature) {
switch (feature) {
case 'chart':
return import('./chart-module.js');
case 'editor':
return import('./editor-module.js');
default:
throw new Error('未知功能模块');
}
}
6. 顶层 await (Top-level await)
// ES2020之前:await只能在async函数中使用
// 模块顶层使用await会报错
// ES2020:模块顶层支持await
// config.js
const fetchConfig = async () => {
const response = await fetch('/api/config');
return response.json();
};
// 顶层await:模块加载会等待配置获取完成
export const config = await fetchConfig();
// main.js
import { config } from './config.js';
// 导入时config已准备就绪,无需额外等待
console.log('应用配置:', config);
document.title = config.appName;
// 实际应用:依赖初始化
// db.js
import { initDatabase } from './database.js';
// 顶层await初始化数据库连接
export const db = await initDatabase({
host: 'localhost',
port: 5432,
name: 'appdb'
});
// 使用时db已连接
import { db } from './db.js';
async function getUser(id) {
return db.query('SELECT * FROM users WHERE id = ?', [id]);
}
// 注意:顶层await会阻塞模块执行,适用于必要的初始化场景
// 避免在不需要等待的场景滥用
ES2021 (ES12) - 提升开发效率
1. Promise.any()
// Promise.race() vs Promise.any()
// Promise.race(): 只要有一个Promise settle(成功/失败)就返回
// Promise.any(): 只要有一个Promise成功就返回,全部失败才返回失败
// 示例:多个API请求,取第一个成功的结果
const fetchFromServer1 = () => fetch('/api/data1');
const fetchFromServer2 = () => fetch('/api/data2');
const fetchFromServer3 = () => fetch('/api/data3');
// 使用Promise.any()
Promise.any([fetchFromServer1(), fetchFromServer2(), fetchFromServer3()])
.then(response => {
console.log('第一个成功的响应:', response);
return response.json();
})
.then(data => console.log('获取到的数据:', data))
.catch(error => {
console.error('所有请求都失败了:', error);
console.log('失败原因数组:', error.errors); // AggregateError包含所有失败原因
});
// 实际应用:图片加载容错
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = url;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`加载图片失败: ${url}`));
});
}
// 尝试从多个CDN加载图片,取第一个成功的
const imageUrls = [
'https://cdn1.example.com/image.jpg',
'https://cdn2.example.com/image.jpg',
'https://cdn3.example.com/image.jpg'
];
Promise.any(imageUrls.map(url => loadImage(url)))
.then(img => {
document.body.appendChild(img);
})
.catch(error => {
console.error('所有图片加载失败:', error);
// 显示默认图片
const defaultImg = new Image();
defaultImg.src = '/images/default.jpg';
document.body.appendChild(defaultImg);
});
2. String.prototype.replaceAll()
const text = 'Hello World! World is beautiful. I love World.';
// ES2021之前:替换所有匹配需要使用正则表达式g标志
const newText1 = text.replace(/World/g, 'Earth');
console.log(newText1);
// 'Hello Earth! Earth is beautiful. I love Earth.'
// ES2021 replaceAll(): 无需正则,直接替换所有匹配
const newText2 = text.replaceAll('World', 'Earth');
console.log(newText2);
// 'Hello Earth! Earth is beautiful. I love Earth.'
// 处理特殊字符(无需转义)
const url = 'https://example.com/path?param1=value1¶m2=value2¶m1=value3';
// 替换所有param1为newParam
const newUrl = url.replaceAll('param1', 'newParam');
console.log(newUrl);
// 'https://example.com/path?newParam=value1¶m2=value2&newParam=value3'
// 与正则表达式配合(需g标志,否则报错)
const messyText = 'apple, Apple, APPLE';
// 错误:未使用g标志
// messyText.replaceAll(/apple/i, 'orange'); // TypeError
// 正确:使用g标志
const cleanText = messyText.replaceAll(/apple/gi, 'orange');
console.log(cleanText); // 'orange, orange, orange'
// 实际应用:敏感信息替换
function redactSensitiveData(data, sensitiveKeys) {
let jsonStr = JSON.stringify(data);
sensitiveKeys.forEach(key => {
// 匹配 "key":"value" 格式中的value
const regex = new RegExp(`"${key}":"[^"]+"`, 'g');
jsonStr = jsonStr.replaceAll(regex, `"${key}":"[REDACTED]"`);
});
return JSON.parse(jsonStr);
}
const userData = {
name: 'Alice',
email: 'alice@example.com',
password: 'secret123',
phone: '1234567890'
};
const redactedData = redactSensitiveData(userData, ['password', 'phone']);
console.log(redactedData);
// {
// name: 'Alice',
// email: 'alice@example.com',
// password: '[REDACTED]',
// phone: '[REDACTED]'
// }
3. 逻辑赋值操作符 (&&=, ||=, ??=)
// 1. 逻辑与赋值 (&&=)
// 语法:a &&= b → 等同于 a = a && b
// 只有a为真时,才将b赋值给a
let x = 5;
x &&= 10; // x = 5 && 10 → 10(5为真,赋值10)
console.log(x); // 10
let y = null;
y &&= 10; // y = null && 10 → null(null为假,保持原值)
console.log(y); // null
// 实际应用:安全更新对象属性
let user = { name: 'Alice', age: 30 };
user.address &&= { ...user.address, city: 'New York' };
// 等同于:if (user.address) user.address = { ...user.address, city: 'New York' }
// 2. 逻辑或赋值 (||=)
// 语法:a ||= b → 等同于 a = a || b
// 只有a为假时,才将b赋值给a
let count = 0;
count ||= 10; // count = 0 || 10 → 10(0为假,赋值10)
console.log(count); // 10
let name = 'Bob';
name ||= 'Unknown'; // name = 'Bob' || 'Unknown' → 'Bob'(真,保持原值)
console.log(name); // 'Bob'
// 3. 空值合并赋值 (??=)
// 语法:a ??= b → 等同于 a = a ?? b
// 只有a为null/undefined时,才将b赋值给a(解决||=误判0/''/false的问题)
let score = 0;
score ??= 100; // score = 0 ?? 100 → 0(0不是null/undefined,保持原值)
console.log(score); // 0
let username = '';
username ??= 'Guest'; // username = '' ?? 'Guest' → ''(保持原值)
console.log(username); // ''
let age = null;
age ??= 18; // age = null ?? 18 → 18(null,赋值18)
console.log(age); // 18
// 实际应用:设置默认配置
function setupOptions(options) {
// 仅当options.theme为null/undefined时设置默认值
options.theme ??= 'light';
// 仅当options.fontSize为null/undefined时设置默认值
options.fontSize ??= 16;
// 仅当options.showSidebar为null/undefined时设置默认值
options.showSidebar ??= true;
return options;
}
const userOptions = {
theme: 'dark',
fontSize: 0, // 0是有效配置,不会被覆盖
showSidebar: false // false是有效配置,不会被覆盖
};
const finalOptions = setupOptions(userOptions);
console.log(finalOptions);
// { theme: 'dark', fontSize: 0, showSidebar: false }
4. 数字分隔符 (Numeric Separators)
// 传统大数字:难以阅读
const population = 7800000000; // 78亿,不易快速识别
const budget = 1234567890123; // 1.2万亿,阅读困难
// ES2021数字分隔符:使用_分隔数字,增强可读性
const population2 = 7_800_000_000; // 78亿,清晰可见
const budget2 = 1_234_567_890_123; // 1.2万亿,易于识别
console.log(population2); // 7800000000(输出时自动忽略_)
console.log(budget2 === 1234567890123); // true
// 小数也支持
const pi = 3.141_592_653_5;
const price = 999.99;
const discount = 0.00_5; // 0.005
// 二进制、八进制、十六进制也支持
const binary = 0b1010_1100_1011; // 二进制
const octal = 0o123_456_700; // 八进制
const hex = 0x1a_bc_3d_ef; // 十六进制
// 实际应用:财务数据
const salary = 125_000; // 12.5万
const tax = 28_750; // 2.875万
const netIncome = salary - tax; // 96250
// 科学计数法
const avogadroNumber = 6.022_140_76e23; // 阿伏伽德罗常数
// 注意事项:
// 1. 不能在数字开头或结尾
// const invalid1 = _123; // 错误
// const invalid2 = 123_; // 错误
// 2. 不能在小数点前后
// const invalid3 = 123_.45; // 错误
// const invalid4 = 123._45; // 错误
// 3. 不能在科学计数法的e前后
// const invalid5 = 123e_45; // 错误
// const invalid6 = 123_e45; // 错误
5. WeakRef 和 FinalizationRegistry
// WeakRef:创建对象的弱引用,不阻止垃圾回收
let obj = { data: '重要数据' };
const weakRef = new WeakRef(obj);
// 获取弱引用指向的对象
console.log(weakRef.deref()); // { data: '重要数据' }
// 释放强引用
obj = null;
// 垃圾回收后,deref()返回undefined(时机不确定)
// 注意:垃圾回收行为在不同环境下可能不同
setTimeout(() => {
console.log(weakRef.deref()); // 可能为undefined
}, 1000);
// FinalizationRegistry:对象被垃圾回收后执行回调
const registry = new FinalizationRegistry((value) => {
console.log(`对象被回收了,附加数据:${value}`);
});
// 注册对象:当obj被垃圾回收时,调用回调并传入附加数据
let targetObj = { id: 1 };
registry.register(targetObj, 'targetObj的附加信息', targetObj);
// 释放强引用
targetObj = null;
// 垃圾回收后,会触发FinalizationRegistry的回调
// 输出:"对象被回收了,附加数据:targetObj的附加信息"
// 实际应用:缓存管理
class WeakCache {
constructor() {
this.cache = new Map();
this.registry = new FinalizationRegistry(key => {
this.cache.delete(key);
console.log(`缓存项 ${key} 已清理`);
});
}
set(key, value) {
this.cache.set(key, value);
// 注册:当value被垃圾回收时,删除对应的缓存项
this.registry.register(value, key, value);
}
get(key) {
return this.cache.get(key);
}
delete(key) {
const value = this.cache.get(key);
if (value) {
this.registry.unregister(value); // 取消注册
this.cache.delete(key);
}
}
}
// 使用弱引用缓存
const cache = new WeakCache();
let data = { id: 1, content: '大数据对象' };
cache.set('data1', data);
console.log(cache.get('data1')); // { id: 1, content: '大数据对象' }
// 释放强引用
data = null;
// 当data被垃圾回收后,缓存项会自动清理
// 输出:"缓存项 data1 已清理"
ES2022 (ES13) - 增强安全性和模块化
1. 类的私有字段 (#)
// ES2022之前:模拟私有属性(通过命名约定或闭包,并非真正私有)
class User {
constructor(name, password) {
this.name = name;
// 约定:下划线开头表示私有,但仍可外部访问
this._password = password;
}
checkPassword(password) {
return this._password === password;
}
}
const user1 = new User('Alice', 'secret123');
console.log(user1._password); // 'secret123'(可外部访问,不安全)
// ES2022:真正的私有字段(#开头)
class SecureUser {
// 私有字段声明(可选,也可在构造函数中直接定义)
#password;
#lastLogin;
constructor(name, password) {
this.name = name; // 公有字段
this.#password = password; // 私有字段
this.#lastLogin = new Date(); // 私有字段
}
checkPassword(password) {
// 类内部可访问私有字段
return this.#password === password;
}
getLastLogin() {
// 提供访问私有字段的公有方法
return this.#lastLogin.toISOString();
}
// 私有方法
#updateLastLogin() {
this.#lastLogin = new Date();
}
login(password) {
if (this.checkPassword(password)) {
this.#updateLastLogin(); // 内部调用私有方法
return true;
}
return false;
}
}
const user2 = new SecureUser('Bob', 'password456');
// 公有字段可访问
console.log(user2.name); // 'Bob'
// 私有字段不可外部访问
console.log(user2.#password); // SyntaxError: Private field '#password' must be declared in an enclosing class
console.log(user2.#updateLastLogin()); // SyntaxError
// 只能通过公有方法访问/操作私有成员
console.log(user2.checkPassword('password456')); // true
console.log(user2.getLastLogin()); // 登录前的时间
user2.login('password456');
console.log(user2.getLastLogin()); // 登录后的最新时间
// 私有字段的继承限制
class AdminUser extends SecureUser {
constructor(name, password, role) {
super(name, password);
this.role = role;
}
// 子类无法访问父类的私有字段
getParentPassword() {
return this.#password; // SyntaxError: Private field '#password' is not defined in class 'AdminUser'
}
}
2. 顶层 await 正式标准化(ES2020提案,ES2022正式纳入)
// 模块顶层直接使用await,无需包裹在async函数中
// config.js
try {
// 顶层await加载远程配置
const response = await fetch('https://api.example.com/config');
const config = await response.json();
// 导出加载完成的配置
export const appConfig = {
apiUrl: config.apiUrl || 'https://default-api.example.com',
theme: config.theme || 'light',
features: config.features || []
};
} catch (error) {
// 加载失败时导出默认配置
console.error('配置加载失败,使用默认配置:', error);
export const appConfig = {
apiUrl: 'https://default-api.example.com',
theme: 'light',
features: []
};
}
// db.js
import { appConfig } from './config.js';
import { Database } from './database.js';
// 顶层await初始化数据库连接
export const db = await Database.connect({
host: appConfig.apiUrl,
timeout: 5000
});
// main.js
import { appConfig } from './config.js';
import { db } from './db.js';
// 导入时config和db已初始化完成,可直接使用
console.log('应用启动,使用API地址:', appConfig.apiUrl);
// 直接使用已连接的数据库
async function getUsers() {
const users = await db.query('SELECT * FROM users LIMIT 10');
return users;
}
// 注意事项:
// 1. 顶层await仅在ES模块中支持(需设置type="module")
// 2. 会阻塞模块依赖链,适用于必要的初始化场景
// 3. 避免循环依赖中的顶层await
3. Error.cause - 错误链追踪
// ES2022之前:错误原因难以追踪
function fetchData() {
return fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return response.json();
})
.catch(error => {
// 原始错误信息被覆盖,难以定位根本原因
throw new Error('数据获取失败');
});
}
// ES2022:Error.cause 传递原始错误
function fetchDataWithCause() {
return fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`, {
cause: new Error(`响应状态: ${response.status}, 响应文本: ${response.statusText}`)
});
}
return response.json();
})
.catch(error => {
// 保留原始错误,添加上下文信息
throw new Error('数据获取失败', { cause: error });
});
}
// 使用错误链
fetchDataWithCause()
.catch(error => {
console.error('最终错误:', error.message); // 最终错误: 数据获取失败
// 追踪原始错误链
let cause = error.cause;
let depth = 1;
while (cause) {
console.error(`原因 ${depth}:`, cause.message);
cause = cause.cause;
depth++;
}
// 输出示例:
// 原因 1: HTTP错误: 404
// 原因 2: 响应状态: 404, 响应文本: Not Found
});
// 实际应用:多层级错误处理
async function processOrder(orderId) {
try {
const order = await fetchOrder(orderId);
try {
await validateOrder(order);
} catch (validateError) {
throw new Error(`订单验证失败 (ID: ${orderId})`, { cause: validateError });
}
try {
await processPayment(order);
} catch (paymentError) {
throw new Error(`支付处理失败 (ID: ${orderId})`, { cause: paymentError });
}
return { success: true, orderId };
} catch (error) {
// 记录完整错误链
logErrorWithCause(error);
throw error;
}
}
// 错误日志记录函数
function logErrorWithCause(error) {
const errorChain = [{ message: error.message, stack: error.stack }];
let cause = error.cause;
while (cause) {
errorChain.push({ message: cause.message, stack: cause.stack });
cause = cause.cause;
}
// 发送完整错误链到日志服务
fetch('/api/logs', {
method: 'POST',
body: JSON.stringify({
timestamp: new Date().toISOString(),
errorChain
})
});
}
4. Array.prototype.at()
const arr = ['a', 'b', 'c', 'd', 'e'];
// ES2022之前:访问数组末尾元素
console.log(arr[arr.length - 1]); // 'e'(传统方式)
console.log(arr.slice(-1)[0]); // 'e'(slice方式)
// ES2022 at(): 支持负索引,更简洁
console.log(arr.at(0)); // 'a'(正索引,同arr[0])
console.log(arr.at(2)); // 'c'(正索引)
console.log(arr.at(-1)); // 'e'(负索引,最后一个元素)
console.log(arr.at(-2)); // 'd'(负索引,倒数第二个元素)
console.log(arr.at(-5)); // 'a'(负索引,第一个元素)
console.log(arr.at(-6)); // undefined(超出范围)
// 实际应用:处理用户输入的列表
function getLastSelectedItem(selectedItems) {
// 安全获取最后一个选中项,无需判断数组长度
return selectedItems.at(-1) || '无选中项';
}
const selected = ['item1', 'item2', 'item3'];
console.log(getLastSelectedItem(selected)); // 'item3'
const emptySelected = [];
console.log(getLastSelectedItem(emptySelected)); // '无选中项'
// 字符串也支持at()方法
const str = 'Hello World';
console.log(str.at(-1)); // 'd'(最后一个字符)
console.log(str.at(-5)); // 'W'(倒数第五个字符)
// 类数组对象也支持
const argumentsObj = function() { return arguments; }('a', 'b', 'c');
console.log(Array.prototype.at.call(argumentsObj, -1)); // 'c'
5. Object.hasOwn()
const user = {
name: 'Alice',
age: 30
};
// 继承的属性
Object.prototype.customMethod = function() {};
// ES2022之前:判断自身属性(排除继承属性)
console.log(user.hasOwnProperty('name')); // true(自身属性)
console.log(user.hasOwnProperty('customMethod')); // false(继承属性)
// 问题:如果对象重写了hasOwnProperty方法,会导致错误
const badObj = {
hasOwnProperty: () => false,
value: 'test'
};
console.log(badObj.hasOwnProperty('value')); // false(错误结果,因为hasOwnProperty被重写)
// ES2022 Object.hasOwn(): 更安全的自身属性判断
console.log(Object.hasOwn(user, 'name')); // true(自身属性)
console.log(Object.hasOwn(user, 'customMethod')); // false(继承属性)
// 解决重写hasOwnProperty的问题
console.log(Object.hasOwn(badObj, 'value')); // true(正确结果)
console.log(Object.hasOwn(badObj, 'hasOwnProperty')); // true(自身属性)
// 处理null/undefined(不会报错)
console.log(Object.hasOwn(null, 'any')); // false
console.log(Object.hasOwn(undefined, 'any')); // false
// 实际应用:安全遍历对象自身属性
function getOwnProperties(obj) {
const props = [];
for (const key in obj) {
// 安全判断自身属性
if (Object.hasOwn(obj, key)) {
props.push(key);
}
}
return props;
}
const testObj = {
a: 1,
b: 2
};
// 添加继承属性
testObj.__proto__.c = 3;
console.log(getOwnProperties(testObj)); // ['a', 'b'](正确排除继承属性c)
6. 模块的静态导入和导出增强
// 1. 导出时重命名的增强
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// 导出时重命名
export { add as sum, subtract as difference };
// 2. 导入时重命名和聚合导出
// utils.js
export { sum, difference } from './math.js'; // 聚合导出
export { default as formatDate } from './date-utils.js'; // 导出默认模块并命名
// 3. 动态导入的增强(结合顶层await)
// feature.js
let featureModule;
if (process.env.FEATURE_ENABLED) {
// 条件动态导入
featureModule = await import('./feature-module.js');
} else {
featureModule = await import('./feature-fallback.js');
}
// 导出动态导入的模块
export const feature = featureModule;
// 4. 导入断言(Import Assertions)
// 导入JSON文件(需运行环境支持)
import config from './config.json' assert { type: 'json' };
console.log(config.apiUrl); // 使用JSON数据
// 导入CSS模块(在浏览器或构建工具中)
import styles from './styles.css' assert { type: 'css' };
// 5. 命名空间导出的增强
// components.js
export * as Buttons from './buttons.js';
export * as Inputs from './inputs.js';
export * as Modals from './modals.js';
// 使用时
import { Buttons, Inputs } from './components.js';
const primaryBtn = new Buttons.PrimaryButton();
const textInput = new Inputs.TextInput();
ES2023 (ES14) - 数组操作和性能优化
1. 数组不可变方法 (toReversed(), toSorted(), toSpliced(), with())
const arr = [3, 1, 2];
// 1. toReversed() - 反转数组(不改变原数组)
const reversed = arr.toReversed();
console.log(reversed); // [2, 1, 3]
console.log(arr); // [3, 1, 2](原数组不变)
// 对比传统reverse()(改变原数组)
const arr2 = [3, 1, 2];
arr2.reverse();
console.log(arr2); // [2, 1, 3](原数组改变)
// 2. toSorted() - 排序数组(不改变原数组)
const sorted = arr.toSorted();
console.log(sorted); // [1, 2, 3]
console.log(arr); // [3, 1, 2](原数组不变)
// 自定义排序
const users = [
{ name: 'Bob', age: 25 },
{ name: 'Alice', age: 30 },
{ name: 'Charlie', age: 20 }
];
const sortedByAge = users.toSorted((a, b) => a.age - b.age);
console.log(sortedByAge);
// [
// { name: 'Charlie', age: 20 },
// { name: 'Bob', age: 25 },
// { name: 'Alice', age: 30 }
// ]
console.log(users); // 原数组不变
// 3. toSpliced() - 删除/插入元素(不改变原数组)
const arr3 = [1, 2, 3, 4, 5];
// 删除:从索引1开始删除2个元素
const spliced1 = arr3.toSpliced(1, 2);
console.log(spliced1); // [1, 4, 5]
console.log(arr3); // [1, 2, 3, 4, 5](原数组不变)
// 插入:从索引2开始删除0个元素,插入6,7
const spliced2 = arr3.toSpliced(2, 0, 6, 7);
console.log(spliced2); // [1, 2, 6, 7, 3, 4, 5]
// 替换:从索引3开始删除1个元素,插入8
const spliced3 = arr3.toSpliced(3, 1, 8);
console.log(spliced3); // [1, 2, 3, 8, 5]
// 4. with () - 替换数组元素(不改变原数组)
const arr = [10, 20, 30, 40];
// 替换索引1的元素为25
const newArr = arr.with(1, 25);
console.log(newArr); // [10, 25, 30, 40]
console.log(arr); // [10, 20, 30, 40](原数组未改变)
// 替换最后一个元素(可结合负索引)
const arr2 = ['a', 'b', 'c'];
const updatedArr = arr2.with(-1, 'd');
console.log(updatedArr); // ['a', 'b', 'd']
// 超出数组长度的索引:自动扩展数组(填补 empty 空位)
const arr3 = [1, 2];
const extendedArr = arr3.with(5, 6);
console.log(extendedArr); // [1, 2, empty × 3, 6]
console.log(extendedArr.length); // 6(数组长度自动调整)
2. Symbols 作为 WeakMap 键
在 ES2023 之前,WeakMap
的键只能是对象类型(Object
、Array
、Function
等),无法使用 Symbol
。ES2023 扩展了这一限制,允许 Symbol
作为 WeakMap
的键,解决了“需要唯一标识且不影响垃圾回收”的场景需求。
核心特性
Symbol
作为键时,仍保持WeakMap
的“弱引用”特性:若Symbol
没有其他强引用,对应的WeakMap
条目会被垃圾回收。Symbol
的唯一性保证:即使两个Symbol
描述相同(如Symbol('key')
),也会被视为不同的键,避免键冲突。
// ES2023 之前:WeakMap 键只能是对象
const weakMapOld = new WeakMap();
const objKey = {};
weakMapOld.set(objKey, '对象作为键'); // 合法
// weakMapOld.set(Symbol('key'), 'Symbol作为键'); // 报错(ES2023前不支持)
// ES2023:Symbol 可作为 WeakMap 键
const weakMap = new WeakMap();
// 1. 基本使用:Symbol 作为键
const symbolKey1 = Symbol('userData');
const symbolKey2 = Symbol('config');
// 设置值
weakMap.set(symbolKey1, { name: 'Alice', age: 30 });
weakMap.set(symbolKey2, { theme: 'dark', fontSize: 16 });
// 获取值
console.log(weakMap.get(symbolKey1)); // { name: 'Alice', age: 30 }
console.log(weakMap.has(symbolKey2)); // true
// 删除值
weakMap.delete(symbolKey2);
console.log(weakMap.has(symbolKey2)); // false
// 2. 唯一性保证:描述相同的 Symbol 是不同的键
const symA = Symbol('sameDesc');
const symB = Symbol('sameDesc');
weakMap.set(symA, '值A');
weakMap.set(symB, '值B');
console.log(weakMap.get(symA)); // '值A'(与symB不冲突)
console.log(weakMap.get(symB)); // '值B'
// 3. 弱引用特性:Symbol 无强引用时,条目会被垃圾回收
let tempSymbol = Symbol('temp');
weakMap.set(tempSymbol, '临时数据');
console.log(weakMap.has(tempSymbol)); // true
// 释放强引用:tempSymbol 不再指向该 Symbol
tempSymbol = null;
// 垃圾回收后:weakMap 中该条目会被自动清理(时机由JS引擎决定)
setTimeout(() => {
console.log(weakMap.has(tempSymbol)); // 可能为 false(已回收)
}, 1000);
// 实际应用:模块私有状态管理
// 场景:模块内需要存储私有状态,且不希望暴露给外部,同时支持垃圾回收
const moduleWeakMap = new WeakMap();
// 私有 Symbol 键(模块内隐藏,外部无法访问)
const privateStateKey = Symbol('privateState');
// 公开方法:初始化模块状态
export function initModule() {
const state = { count: 0, logs: [] };
moduleWeakMap.set(privateStateKey, state);
}
// 公开方法:更新模块状态(外部无法直接访问 state)
export function incrementCount() {
const state = moduleWeakMap.get(privateStateKey);
if (state) {
state.count++;
state.logs.push(`更新时间:${new Date().toISOString()}`);
}
}
// 公开方法:获取状态(仅暴露必要信息)
export function getModuleState() {
const state = moduleWeakMap.get(privateStateKey);
return state ? { count: state.count, logCount: state.logs.length } : null;
}
3. Hashbang 语法(#!)支持
Hashbang(也叫 Shebang)是 Unix-like 系统中用于指定脚本解释器的语法(如 #!/usr/bin/env node
)。ES2023 正式将其纳入标准,允许 JavaScript 脚本文件开头使用 Hashbang,且 JS 引擎会自动忽略这一行(无需手动处理)。
核心作用
- 让 JS 脚本可直接在终端执行(如
./script.js
),无需显式调用node script.js
。 - 标准化 Hashbang 处理:避免不同 JS 引擎对 Hashbang 的解析差异。
// script.js(ES2023 支持 Hashbang)
#!/usr/bin/env node
// 脚本逻辑(Hashbang 行被 JS 引擎自动忽略)
console.log('Hello, Hashbang!');
// 命令行执行(无需 node 命令)
// 1. 给脚本添加可执行权限:chmod +x script.js
// 2. 直接执行:./script.js
// 输出:Hello, Hashbang!
// 注意事项:
// 1. Hashbang 必须在文件第一行,且以 #! 开头
// 2. 仅在 Unix-like 系统(Linux、macOS)生效,Windows 需通过 WSL 或 Git Bash 等环境支持
// 3. 若脚本在浏览器中运行,Hashbang 行仍会被忽略(不影响前端代码)
// 实际应用:CLI 工具脚本
#!/usr/bin/env node
// 简单的 CLI 工具:接收命令行参数并输出
const args = process.argv.slice(2); // 获取命令行参数(排除 node 和脚本路径)
if (args.length === 0) {
console.log('请输入参数!用法:./cli.js <消息>');
process.exit(1);
}
console.log(`你输入的消息:${args.join(' ')}`);
console.log(`当前时间:${new Date().toLocaleString()}`);
// 执行示例:
// ./cli.js Hello ES2023
// 输出:
// 你输入的消息:Hello ES2023
// 当前时间:2024/5/20 14:30:00
4. 其他小优化
(1)Array.prototype.findLast()
和 Array.prototype.findLastIndex()
ES2022 已引入这两个方法,但 ES2023 进一步优化了其兼容性和性能。它们从数组末尾开始查找元素,避免了传统“反转数组后查找”的额外开销。
const numbers = [10, 20, 30, 40, 50];
// 从末尾查找第一个大于 30 的元素
const lastLargeNum = numbers.findLast(num => num > 30);
console.log(lastLargeNum); // 50
// 从末尾查找第一个大于 30 的元素的索引
const lastLargeIndex = numbers.findLastIndex(num => num > 30);
console.log(lastLargeIndex); // 4(索引从 0 开始)
// 实际应用:查找最新的有效数据
const logs = [
{ id: 1, status: 'failed' },
{ id: 2, status: 'success' },
{ id: 3, status: 'failed' },
{ id: 4, status: 'success' }
];
// 查找最后一次成功的日志
const lastSuccessLog = logs.findLast(log => log.status === 'success');
console.log(lastSuccessLog); // { id: 4, status: 'success' }
(2)TypedArray
方法扩展
ES2023 为 TypedArray
(如 Uint8Array
、Float64Array
等)添加了与普通数组一致的方法,如 toReversed()
、toSorted()
、toSpliced()
、with()
,确保类型化数组也能支持不可变操作。
// 创建一个 TypedArray(无符号8位整数数组)
const typedArr = new Uint8Array([3, 1, 2]);
// 不可变排序
const sortedTypedArr = typedArr.toSorted();
console.log(sortedTypedArr); // Uint8Array [1, 2, 3]
console.log(typedArr); // Uint8Array [3, 1, 2](原数组不变)
// 不可变替换
const updatedTypedArr = typedArr.with(1, 5);
console.log(updatedTypedArr); // Uint8Array [3, 5, 2]
ES2024 (ES15) - 高效数据处理与异步增强
1. 数组分组 API(Array.prototype.group()
、Array.prototype.groupToMap()
)
ES2024 引入了原生的数组分组方法,解决了传统“手动循环+条件判断”分组的繁琐问题,支持直接按条件将数组分为多个子集。
(1)Array.prototype.group(callback)
- 接收一个回调函数,回调返回字符串/符号(Symbol)类型的分组键。
- 返回一个普通对象,键为分组键,值为该组对应的数组元素。
const products = [
{ name: 'iPhone', category: 'electronics', price: 999 },
{ name: 'Shirt', category: 'clothing', price: 29 },
{ name: 'Laptop', category: 'electronics', price: 1299 },
{ name: 'Pants', category: 'clothing', price: 49 },
{ name: 'Headphones', category: 'electronics', price: 199 }
];
// 按 category 分组
const groupedByCategory = products.group(product => product.category);
console.log(groupedByCategory);
// {
// electronics: [
// { name: 'iPhone', category: 'electronics', price: 999 },
// { name: 'Laptop', category: 'electronics', price: 1299 },
// { name: 'Headphones', category: 'electronics', price: 199 }
// ],
// clothing: [
// { name: 'Shirt', category: 'clothing', price: 29 },
// { name: 'Pants', category: 'clothing', price: 49 }
// ]
// }
// 按价格区间分组(自定义分组键)
const groupedByPrice = products.group(product => {
if (product.price < 100) return 'cheap';
if (product.price < 1000) return 'mid';
return 'expensive';
});
console.log(groupedByPrice.cheap); // [Shirt, Pants]
console.log(groupedByPrice.expensive); // [Laptop]
(2)Array.prototype.groupToMap(callback)
- 与
group()
逻辑类似,但返回一个Map
对象(而非普通对象)。 - 支持任意类型的分组键(如对象、数组),解决了普通对象键只能是字符串/Symbol 的限制。
const users = [
{ name: 'Alice', age: 25, team: { id: 1, name: 'Team A' } },
{ name: 'Bob', age: 30, team: { id: 2, name: 'Team B' } },
{ name: 'Charlie', age: 28, team: { id: 1, name: 'Team A' } },
{ name: 'David', age: 32, team: { id: 2, name: 'Team B' } }
];
// 按 team 对象分组(普通 group() 无法实现,因为对象键会被转为 "[object Object]")
const groupedByTeam = users.groupToMap(user => user.team);
// Map 的键是 team 对象,值是该团队的用户数组
const teamAUsers = groupedByTeam.get(users[0].team);
console.log(teamAUsers); // [Alice, Charlie]
// 遍历 Map 分组结果
groupedByTeam.forEach((usersInTeam, team) => {
console.log(`团队 ${team.name} 成员:`, usersInTeam.map(u => u.name));
});
// 输出:
// 团队 Team A 成员: ["Alice", "Charlie"]
// 团队 Team B 成员: ["Bob", "David"]
2. Promise.withResolvers()
- 简化 Promise 创建
传统创建 Promise
时,需手动定义 resolve
和 reject
函数并封装在 executor 回调中。ES2024 的 Promise.withResolvers()
直接返回包含 promise
、resolve
、reject
的对象,简化了 Promise 初始化代码。
// 传统 Promise 创建方式
function createTimer(ms) {
let resolve;
const promise = new Promise((res) => {
setTimeout(() => res(`延迟 ${ms}ms 完成`), ms);
resolve = res; // 需手动保存 resolve 引用(如需外部触发)
});
return { promise, resolve };
}
// ES2024 Promise.withResolvers()
function createTimerNew(ms) {
const { promise, resolve } = Promise.withResolvers();
setTimeout(() => resolve(`延迟 ${ms}ms 完成`), ms);
return { promise, resolve };
}
// 使用示例:控制 Promise 完成时机
const { promise: timerPromise, resolve: manualResolve } = createTimerNew(1000);
// 监听 Promise 结果
timerPromise.then(message => console.log(message)); // 1秒后输出 "延迟 1000ms 完成"
// 可选:提前手动触发 resolve(覆盖定时器逻辑)
// manualResolve("手动提前完成"); // 若取消注释,会立即输出该消息
// 实际应用:异步资源锁
class AsyncLock {
constructor() {
this.isLocked = false;
this.pending = null; // 存储等待中的 Promise resolver
}
// 加锁
lock() {
if (!this.isLocked) {
this.isLocked = true;
return Promise.resolve(); // 无需等待,直接加锁
}
// 已有锁,返回等待中的 Promise
if (!this.pending) {
this.pending = Promise.withResolvers();
}
return this.pending.promise;
}
// 解锁
unlock() {
this.isLocked = false;
// 若有等待中的请求,触发下一个锁
if (this.pending) {
this.pending.resolve();
this.pending = null; // 清空等待状态
}
}
}
// 使用异步锁:确保异步操作串行执行
const lock = new AsyncLock();
async function safeAsyncOperation(taskName) {
await lock.lock(); // 加锁:若已有操作,等待其完成
try {
console.log(`开始执行任务:${taskName}`);
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟异步操作
console.log(`完成任务:${taskName}`);
} finally {
lock.unlock(); // 确保解锁(即使出错)
}
}
// 并发调用,但会串行执行
safeAsyncOperation('任务1');
safeAsyncOperation('任务2');
safeAsyncOperation('任务3');
// 输出顺序:
// 开始执行任务:任务1 → 完成任务:任务1 → 开始执行任务:任务2 → 完成任务:任务2 → ...
3. Temporal API - 彻底解决日期时间处理痛点
JavaScript 原生的 Date
对象长期存在设计缺陷(如月份从 0 开始、时区处理混乱、无法处理历法等)。ES2024 引入的 Temporal API 是一套全新的日期时间处理标准,提供了清晰、安全、易用的 API,支持时区、历法、时长等复杂场景。
核心概念与常用 API
类型 | 用途 | 示例 |
---|---|---|
Temporal.Instant |
表示“时间点”(UTC 时间,无时区) | Temporal.Instant.from('2024-05-20T12:00:00Z') |
Temporal.ZonedDateTime |
带时区的日期时间(如“北京时间 2024-05-20 20:00:00”) | Temporal.ZonedDateTime.from('2024-05-20T20:00:00+08:00[Asia/Shanghai]') |
Temporal.PlainDate |
无时间、无时区的日期(如“2024-05-20”) | Temporal.PlainDate.from('2024-05-20') |
Temporal.PlainTime |
无日期、无时区的时间(如“14:30:00”) | Temporal.PlainTime.from('14:30:00') |
Temporal.Duration |
表示“时长”(如“2小时30分钟”) | Temporal.Duration.from({ hours: 2, minutes: 30 }) |
代码示例
// 1. 创建带时区的日期时间(解决 Date 时区混乱问题)
// 北京时区的 2024年5月20日 20:00:00
const beijingTime = Temporal.ZonedDateTime.from({
year: 2024,
month: 5,
day: 20,
hour: 20,
minute: 0,
second: 0,
timeZone: 'Asia/Shanghai' // 明确指定时区
});
console.log(beijingTime.toString()); // 2024-05-20T20:00:00+08:00[Asia/Shanghai]
// 转换为纽约时区
const newYorkTime = beijingTime.toTimeZone('America/New_York');
console.log(newYorkTime.toString()); // 2024-05-20T08:00:00-04:00[America/New_York](自动计算时差)
// 2. 日期计算(避免 Date 的月份偏移问题)
const today = Temporal.PlainDate.from('2024-05-20');
// 加 1 个月(自动处理月份天数差异)
const nextMonth = today.add({ months: 1 });
console.log(nextMonth.toString()); // 2024-06-20
// 减 2 周
const twoWeeksAgo = today.subtract({ weeks: 2 });
console.log(twoWeeksAgo.toString()); // 2024-05-06
// 计算两个日期的差值(返回 Duration)
const diff = nextMonth.since(today);
console.log(diff.months); // 1(差值为 1 个月)
// 3. 格式化(内置支持,无需第三方库如 moment.js)
const zonedTime = Temporal.ZonedDateTime.from('2024-05-20T20:00:00+08:00[Asia/Shanghai]');
// 自定义格式
console.log(zonedTime.toLocaleString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'long'
})); // 2024年5月20日 20:00 中国标准时间
// 4. 处理不同历法(如农历、伊斯兰历)
// 注意:部分历法需额外加载插件,核心 API 支持扩展
const lunarDate = Temporal.PlainDate.from({
year: 2024,
month: 4,
day: 13,
calendar: 'chinese' // 农历(需环境支持)
});
console.log(lunarDate.toLocaleString('zh-CN')); // 2024年四月十三(农历)
// 5. 时长处理(精确到纳秒,支持复杂单位)
const duration1 = Temporal.Duration.from({ hours: 2, minutes: 30 });
const duration2 = Temporal.Duration.from({ minutes: 45, seconds: 15 });
// 时长相加
const totalDuration = duration1.add(duration2);
console.log(totalDuration.toString()); // PT3H15M15S(3小时15分15秒)
// 时长转换为总秒数
console.log(totalDuration.total({ unit: 'seconds' })); // 11715(3*3600 + 15*60 +15)
ES2025 (ES16) - 提案中的重要特性
ES2025 的特性目前处于 Stage 3 提案阶段(接近标准化),以下是最受关注的两个特性:
1. Iterator Helpers - 迭代器辅助方法
迭代器(Iterator
)是 JavaScript 中处理序列数据的核心接口(如数组、Map
、生成器函数返回值),但原生缺乏便捷的操作方法。Iterator Helpers 为迭代器添加了类似数组的链式操作方法(如 map()
、filter()
、take()
),支持惰性求值(仅在需要时计算下一个元素),大幅提升迭代器的易用性。
// 1. 基本使用:迭代器链式操作
const numbers = [1, 2, 3, 4, 5];
const iterator = numbers[Symbol.iterator](); // 获取数组的迭代器
// 迭代器操作链:过滤偶数 → 乘以 2 → 取前 2 个元素
const resultIterator = iterator
.filter(num => num % 2 === 0) // 过滤偶数:2,4
.map(num => num * 2) // 乘以2:4,8
.take(2); // 取前2个元素
// 遍历结果(惰性求值:仅在 next() 调用时计算)
console.log(resultIterator.next().value); // 4(第一次计算)
console.log(resultIterator.next().value); // 8(第二次计算)
console.log(resultIterator.next().done); // true(无更多元素)
// 2. 与生成器函数结合(处理无限序列)
// 生成无限递增的整数迭代器
function* infiniteNumbers() {
let num = 1;
while (true) yield num++;
}
// 操作无限迭代器:取偶数 → 乘以 3 → 取前 3 个
const finiteResult = infiniteNumbers()
.filter(num => num % 2 === 0)
.map(num => num * 3)
.take(3);
// 转换为数组(触发迭代器计算)
console.log(Array.from(finiteResult)); // [6, 12, 18](仅计算前3个,避免无限循环)
// 3. 异步迭代器支持(Async Iterator)
async function* asyncDataGenerator() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}
// 异步迭代器操作:过滤大于1的数 → 乘以 10
const asyncResult = asyncDataGenerator()
.filter(async num => (await num) > 1)
.map(async num => (await num) * 10);
// 遍历异步结果
for await (const value of asyncResult) {
console.log(value); // 20 → 30
}
2. Promise.try()
- 简化同步/异步错误捕获
传统场景中,若一个函数可能返回 同步值 或 Promise,捕获其错误需同时处理 try/catch
(同步)和 .catch()
(异步),代码冗余。Promise.try()
统一了这一逻辑:无论函数返回同步值还是 Promise,都能通过 .catch()
捕获所有错误。
// 传统问题:函数可能返回同步值或 Promise,错误捕获繁琐
function unstableFunction(shouldThrow, isAsync) {
if (shouldThrow) {
// 可能抛出同步错误
throw new Error('同步错误');
}
if (isAsync) {
// 可能返回 rejected Promise
return Promise.reject(new Error('异步错误'));
}
// 可能返回同步值
return '成功结果';
}
// 传统错误捕获方式(需同时处理同步和异步)
function handleTraditional() {
try {
const result = unstableFunction(false, true);
// 若返回 Promise,需额外 catch
if (result instanceof Promise) {
result.catch(error => console.error('传统捕获:', error.message));
}
} catch (error) {
console.error('传统捕获:', error.message);
}
}
// ES2025 Promise.try():统一捕获同步/异步错误
function handleWithTry() {
Promise.try(() => unstableFunction(false, true))
.then(result => console.log('成功:', result))
.catch(error => console.error('Promise.try 捕获:', error.message));
}
// 测试不同场景
handleWithTry(false, false); // 成功: 成功结果(同步值)
handleWithTry(true, false); // 捕获: 同步错误(同步抛出)
handleWithTry(false, true); // 捕获: 异步错误(Promise reject)
// 实际应用:统一处理 API 调用(可能有缓存层返回同步值)
function fetchDataWithCache(id) {
// 1. 先查缓存(同步)
const cachedData = getFromCache(id);
if (cachedData) {
return cachedData; // 同步返回缓存值
}
// 2. 缓存未命中,异步请求
return fetch(`/api/data/${id}`).then(res => res.json());
}
// 使用 Promise.try() 统一处理
Promise.try(() => fetchDataWithCache(123))
.then(data => console.log('数据:', data))
.catch(error => {
// 同时捕获:缓存读取错误(同步)和 API 请求错误(异步)
console.error('获取数据失败:', error);
});
总结与学习建议
ECMAScript 从 2015 年开始的“年度更新”模式,让 JavaScript 逐步成为一门更成熟、更强大的语言。每个版本的新特性都围绕“解决实际开发痛点”展开,如:
- ES2015 奠定现代 JS 基础(
let/const
、箭头函数、模块); - ES2017-2020 优化异步编程(
async/await
、可选链、顶层await
); - ES2022-2024 增强安全性与不可变性(私有字段、数组不可变方法);
- ES2024+ 聚焦高效数据处理(分组 API、Temporal)。