第 22 章 ES6+ 新增语法
第 22 章 ES6+ 新增语法
ES2020、ES2021、ES2022、ES2023、ES2024… JavaScript 越来越甜了!
22.1 链式操作
可选链操作符 ?.(ES2020+)
可选链操作符(Optional Chaining)——?.,这是 JavaScript 历史上最受好评的语法更新之一!它让深层属性访问变得安全,再也不用写一堆 && 了。
1
2
3
4
5
6
| // 以前的痛苦写法
const city = user && user.address && user.address.city;
// 现在的优雅写法
const city = user?.address?.city;
console.log('城市:', city); // 城市: undefined(如果路径上任何一环是 null/undefined)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // 可选链的三种用法
const person = {
name: '张三',
address: {
city: '北京',
zip: '100000'
}
};
// 1. 属性访问
console.log('城市:', person?.address?.city); // 城市: 北京
console.log('国家:', person?.address?.country); // 国家: undefined
// 2. 函数调用
const obj = {
greet: function() {
return '你好!';
}
};
const noGreet = null;
console.log('打招呼:', obj?.greet?.()); // 打招呼: 你好!
console.log('不存在的方法:', noGreet?.greet?.()); // 不存在的方法: undefined
|
1
2
3
4
5
| // 3. 数组元素访问
const arr = [1, 2, 3];
console.log('第三个元素:', arr?.[2]); // 第三个元素: 3
console.log('第十个元素:', arr?.[9]); // 第十个元素: undefined
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 可选链与空值合并运算符配合
const user = {
profile: {
name: '小明'
// age 没定义
}
};
// 只用可选链:得到 undefined
console.log('年龄:', user?.profile?.age); // 年龄: undefined
// 配合空值合并:得到默认值
console.log('年龄(带默认值):', user?.profile?.age ?? '未知'); // 年龄(带默认值): 未知
|
1
2
3
4
5
6
7
8
9
10
11
12
| // 可选链的短路特性
// 如果可选链某一步是 null/undefined,后续不再执行
const obj2 = {
getValue: () => {
console.log('这个函数会被调用吗?');
return 'value';
}
};
console.log('结果:', obj2?.nonExistent?.()); // 结果: undefined(函数没被调用)
console.log('结果是 undefined 类型:', typeof obj2?.nonExistent?.()); // undefined
|
1
2
3
4
| // 多次可选链
const deep = {};
console.log('多层可选链:', deep?.a?.b?.c?.d); // undefined(不会报错)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 实际应用:安全地访问 API 响应
const apiResponse = {
status: 200,
data: {
user: {
name: '李四',
contact: {
email: 'li@example.com'
}
}
}
};
const email = apiResponse?.data?.user?.contact?.email;
console.log('邮箱:', email); // 邮箱: li@example.com
// 不存在的路径
const phone = apiResponse?.data?.user?.contact?.phone;
console.log('电话:', phone); // 电话: undefined
|
1
2
| // 容易犯的错误:可选链不能用于赋值
// person?.name = '王五'; // SyntaxError! 可选链不能用于赋值目标
|
1
2
3
4
5
6
7
| // 可选链与算符结合
// 错误写法
// const x = person?.name.length; // 如果 name 是 undefined,会报错
// 正确写法(可选链后使用空值合并)
const nameLength = person?.name?.length ?? 0;
console.log('名字长度:', nameLength); // 名字长度: 2
|
空值合并运算符 ??(ES2020+)
空值合并运算符(Nullish Coalescing)——??,它只在你真正"什么都没有"的时候才使用默认值,而不是在"假值"(falsy)的时候。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // ?? vs || 的区别
console.log('=== ?? vs || ===');
const a = 0;
const b = '';
const c = false;
const d = null;
const e = undefined;
console.log('使用 ||:');
console.log(' a || 10:', a || 10); // 10(0 是 falsy)
console.log(' b || "hi":', b || 'hi'); // hi(空字符串是 falsy)
console.log(' c || true:', c || true); // true(false 是 falsy)
console.log(' d || "null":', d || 'null'); // null(null 是 falsy)
console.log(' e || "undefined":', e || 'undefined'); // undefined(undefined 是 falsy)
console.log('\n使用 ??(只关心 null/undefined):');
console.log(' a ?? 10:', a ?? 10); // 0(0 不是 null/undefined)
console.log(' b ?? "hi":', b ?? 'hi'); // ''(空字符串不是 null/undefined)
console.log(' c ?? true:', c ?? true); // false(false 不是 null/undefined)
console.log(' d ?? "null":', d ?? 'null'); // null(d 是 null)
console.log(' e ?? "undefined":', e ?? 'undefined'); // undefined(e 是 undefined)
|
1
2
3
4
5
6
7
8
9
10
11
12
| // 典型应用:设置默认值
function setWidth(width) {
// || 会把 0 和 '' 也当成需要替换的值
// ?? 只替换 null 和 undefined
const finalWidth = width ?? 300;
console.log('最终宽度:', finalWidth);
}
setWidth(0); // 最终宽度: 0(而不是 300!)
setWidth(500); // 最终宽度: 500
setWidth(null); // 最终宽度: 300
setWidth(undefined); // 最终宽度: 300
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 配合可选链使用
const user = {
name: '小红',
age: 0, // 故意设为0
bio: '' // 故意设为空字符串
};
// 获取年龄,如果没有则默认为 18
const displayAge = user?.age ?? 18;
console.log('显示年龄:', displayAge); // 0(而不是 18!)
// 获取简介,如果没有则默认为 '这个人很懒'
const displayBio = user?.bio ?? '这个人很懒';
console.log('显示简介:', displayBio); // ''(空字符串,而不是默认值)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // ?? 不能与 && 或 || 混用(除非用括号)
// 这会报错
// const x = a || b ?? c;
// 需要加括号(|| 和 ?? 的优先级:|| > ??)
const x = a || (b ?? c);
// 如果 a 是 truthy,x = a
// 如果 a 是 falsy 但不是 null/undefined,x = b ?? c
// 如果 a 是 null/undefined,x = b ?? c
// 实际例子:
let a = 0, b = 1, c = 2;
console.log('a || (b ?? c):', a || (b ?? c)); // 1(因为 a 是 0,|| 会看 b)
// 所以正确的等价写法需要考虑 a 的所有 falsy 值
// 如果想用 ?? 代替 ||,需要这样:
const y = a !== null && a !== undefined ? a : (b ?? c);
|
1
2
3
4
5
6
7
8
9
10
11
| // ?? 的短路特性
function getValue() {
console.log('函数被调用了!');
return 'value';
}
const result = null ?? getValue(); // 函数被调用了!
console.log('结果:', result); // 结果: value
const result2 = 'hello' ?? getValue(); // 函数不会被调用!
console.log('结果2:', result2); // 结果2: hello
|
💡 本章小结(第22章第1节)
可选链 ?. 让深层属性访问变得安全,user?.profile?.address?.city 不会在中间任何一环为 null/undefined 时报错。空值合并运算符 ?? 只在值为 null 或 undefined 时才使用默认值,不会被 0、''、false 这些假值误导。两者配合使用效果最佳:user?.age ?? 18 既安全又有默认值!
22.2 逻辑赋值
||= / &&= / ??=(ES2021+)
逻辑赋值运算符——||=、&&=、??=——它们把逻辑运算和赋值合并成了一步。这语法刚出来的时候很多人觉得奇怪,但用习惯了还真挺方便的。
1
2
3
4
5
6
7
8
9
10
| // 逻辑 OR 赋值 ||=(当值为 falsy 时才赋值)
let a = 0;
let b = 'hello';
let c = null;
a ||= 10; // a 是 0(falsy),所以赋值为 10
b ||= 'world'; // b 是 'hello'(truthy),不变
c ||= 'assigned'; // c 是 null(falsy),所以赋值为 'assigned'
console.log('a:', a, 'b:', b, 'c:', c); // a: 10 b: hello c: assigned
|
1
2
3
4
5
6
7
8
9
10
| // 逻辑 AND 赋值 &&=(当值为 truthy 时才赋值)
let x = 5;
let y = 0;
let z = 'existing';
x &&= 100; // x 是 5(truthy),所以赋值为 100
y &&= 50; // y 是 0(falsy),不变
z &&= 'changed'; // z 是 'existing'(truthy),所以赋值为 'changed'
console.log('x:', x, 'y:', y, 'z:', z); // x: 100 y: 0 z: changed
|
1
2
3
4
5
6
7
8
9
10
11
12
| // 逻辑空值赋值 ??=(当值为 null/undefined 时才赋值)
let m = null;
let n = undefined;
let o = 0; // 不是 null/undefined
let p = '';
m ??= 'm assigned'; // m 是 null,赋值
n ??= 'n assigned'; // n 是 undefined,赋值
o ??= 'o assigned'; // o 是 0,不赋值
p ??= 'p assigned'; // p 是 '',不赋值
console.log('m:', m, 'n:', n, 'o:', o, 'p:', p); // m: m assigned n: n assigned o: 0 p: ''
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // 对比三种逻辑赋值
let value;
console.log('=== 初始值 ===');
value = 0;
console.log('value = 0');
console.log('\n||=:');
value ||= 'default'; // 0 是 falsy,所以赋值为 'default'
console.log('result:', value); // result: default
value = 'hello';
value ||= 'default'; // 'hello' 是 truthy,不变
console.log('result:', value); // result: hello
console.log('\n&&=:');
value = 0;
value &&= 'default'; // 0 是 falsy,不变
console.log('result:', value); // result: 0
value = 'hello';
value &&= 'default'; // 'hello' 是 truthy,赋值为 'default'
console.log('result:', value); // result: default
console.log('\n??=:');
value = 0;
value ??= 'default'; // 0 不是 null/undefined,不变
console.log('result:', value); // result: 0
value = null;
value ??= 'default'; // null,赋值为 'default'
console.log('result:', value); // result: default
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // 实际应用:配置对象合并
function createConfig(baseConfig, overrides) {
return {
theme: baseConfig.theme,
language: baseConfig.language,
debug: baseConfig.debug,
// 用 ||= 来允许用户覆盖
...Object.fromEntries(
Object.entries(overrides).map(([key, value]) => [key, value])
)
};
}
// 用逻辑赋值实现
function createConfigBetter(base) {
const config = {};
config.theme = base.theme;
config.language = base.language ?? 'en'; // 默认英语
config.debug = base.debug ?? false; // 默认关闭
// 允许传入的配置覆盖默认
config.theme = base.theme;
config.language ??= 'en'; // 如果没设置,默认英语
config.debug ??= false;
return config;
}
const defaults = { theme: 'light', language: 'en' };
const userPrefs = { language: 'zh' };
const config = createConfigBetter(defaults);
console.log('配置:', config); // 配置: { theme: 'light', language: 'zh', debug: false }
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 应用:安全地添加属性
const user = { name: '张三' };
// 以前
if (!user.age) {
user.age = 18;
}
// 现在
user.age ??= 18;
console.log('用户年龄:', user.age); // 用户年龄: 18
user.age = 20;
user.age ??= 18; // 已经是 20 了,不会再改
console.log('更新后年龄:', user.age); // 更新后年龄: 20
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 应用:缓存结果
function expensiveCalc() {
console.log('执行耗时计算...');
return 42;
}
const cache = {};
function getCachedResult(key) {
cache[key] ??= expensiveCalc();
return cache[key];
}
console.log(getCachedResult('answer')); // 执行耗时计算... 42
console.log(getCachedResult('answer')); // 不打印,直接返回缓存值 42
console.log(getCachedResult('answer')); // 不打印,直接返回缓存值 42
|
1
2
3
4
5
6
| // 等价转换
// a ||= b 等价于 a || (a = b)
// a &&= b 等价于 a && (a = b)
// a ??= b 等价于 a ?? (a = b)
// 但是逻辑赋值更简洁!
|
💡 本章小结(第22章第2节)
逻辑赋值运算符把逻辑判断和赋值合并成了一步:||= 在值为 falsy 时赋值,&&= 在值为 truthy 时赋值,??= 在值为 null/undefined 时赋值。它们的出现让配置合并、默认值设置、缓存等场景的代码更简洁。记住:||= 是"假值替换",&&= 是"真值替换",??= 是"空值替换"!
22.3 数组新方法
flat / flatMap:扁平化
数组扁平化是前端开发中的常见需求。ES2019 引入了 flat() 和 flatMap(),让这个操作变得异常简单。
1
2
3
4
5
6
| // flat:扁平化数组
const nested = [1, [2, 3], [4, [5, 6]]];
console.log('原数组:', nested);
console.log('扁平一层:', nested.flat()); // [ 1, 2, 3, 4, [ 5, 6 ] ]
console.log('完全扁平:', nested.flat(Infinity)); // [ 1, 2, 3, 4, 5, 6 ]
|
1
2
3
4
| // flat 会自动移除空位
const withHoles = [1, , 2, , [3, , 4]];
console.log('有空洞的数组:', withHoles);
console.log('扁平后:', withHoles.flat()); // [ 1, 2, 3, 4 ] —— 空洞被移除了!
|
1
2
3
4
5
| // flatMap:先映射再扁平
const words = ['hello world', 'good morning'];
const chars = words.flatMap(word => word.split(''));
console.log('字符数组:', chars);
// [ 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', 'g', 'o', 'o', 'd', ' ', 'm', 'o', 'r', 'n', 'i', 'n', 'g' ]
|
1
2
3
4
5
6
7
| // flatMap 的返回值会自动被扁平
// 相当于 words.map(...).flat()
const result1 = words.map(w => w.split(''));
console.log('map后:', result1); // [['h','e','l','l','o',' '...], ['g','o','o','d',' '...]]
const result2 = words.flatMap(w => w.split(''));
console.log('flatMap后:', result2); // ['h','e','l','l','o',' ','w',...]
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| // flatMap 可以返回不同长度的数组
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => [n * 2]); // [[2], [4], [6], [8]]
console.log('map后:', doubled);
const doubledFlat = numbers.flatMap(n => [n * 2]); // [2, 4, 6, 8]
console.log('flatMap后:', doubledFlat);
// 如果想"展开"成单个元素,用 flatMap
// 如果想"展开"成多个元素,也可以
const expanded = numbers.flatMap(n => Array(n).fill(n));
console.log('展开为重复数组:', expanded); // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
|
toSorted / toReversed / toSpliced / with(ES2023+)
ES2023 引入了数组的"非变异"方法——这些方法会返回新的数组/值,而不会修改原数组。在 React 等强调不可变性的框架中,这些方法非常有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // toReversed():返回反转后的新数组(不修改原数组)
const arr1 = [1, 2, 3, 4, 5];
const reversed1 = arr1.toReversed();
console.log('原数组:', arr1); // [ 1, 2, 3, 4, 5 ] —— 不变!
console.log('反转后:', reversed1); // [ 5, 4, 3, 2, 1 ]
// 对比旧的 reverse()
const arr2 = [1, 2, 3, 4, 5];
const reversed2 = arr2.reverse();
console.log('原数组 after reverse():', arr2); // [ 5, 4, 3, 2, 1 ] —— 被修改了!
console.log('反转后:', reversed2); // [ 5, 4, 3, 2, 1 ]
|
1
2
3
4
5
6
| // toSorted():返回排序后的新数组(不修改原数组)
const unsorted = [3, 1, 4, 1, 5, 9, 2, 6];
const sorted = unsorted.toSorted();
console.log('原数组:', unsorted); // [ 3, 1, 4, 1, 5, 9, 2, 6 ] —— 不变!
console.log('排序后:', sorted); // [ 1, 1, 2, 3, 4, 5, 6, 9 ]
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // toSpliced():返回 splice 操作后的新数组(不修改原数组)
const original = ['a', 'b', 'c', 'd', 'e'];
// 删除元素
const deleted = original.toSpliced(1, 2); // 从索引1开始删除2个
console.log('原数组:', original); // ['a', 'b', 'c', 'd', 'e'] —— 不变!
console.log('删除后:', deleted); // ['a', 'd', 'e']
// 插入元素
const inserted = original.toSpliced(2, 0, 'x', 'y'); // 从索引2插入,不删除
console.log('插入后:', inserted); // ['a', 'b', 'x', 'y', 'c', 'd', 'e']
// 替换元素
const replaced = original.toSpliced(2, 2, 'Z');
console.log('替换后:', replaced); // ['a', 'b', 'Z', 'd', 'e']
|
1
2
3
4
5
6
7
8
9
10
| // with():返回指定位置元素被替换后的新数组(不修改原数组)
const arr = [1, 2, 3, 4, 5];
const newArr = arr.with(2, 'three'); // 把索引2的元素替换为'three'
console.log('原数组:', arr); // [ 1, 2, 3, 4, 5 ] —— 不变!
console.log('替换后:', newArr); // [ 1, 2, 'three', 4, 5 ]
// 支持负索引
const lastItem = arr.with(-1, 999);
console.log('替换最后一个:', lastItem); // [ 1, 2, 3, 4, 999 ]
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 这些方法在 React 中的应用
// React 鼓励不可变性,直接修改状态是不推荐的
const state = {
items: [1, 2, 3, 4, 5],
user: { name: '张三', age: 30 }
};
// 以前的写法
const newItems1 = state.items.slice(); // 先拷贝
newItems1.reverse();
const newState1 = { ...state, items: newItems1 };
// 现在的写法(更简洁)
const newState2 = {
...state,
items: state.items.toReversed()
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 对比总结
const test = [3, 1, 4, 1, 5, 9, 2, 6];
// 旧方法(修改原数组)
test.sort(); // 修改 test
test.reverse(); // 修改 test
test.splice(); // 修改 test
// 新方法(返回新数组)
test.toSorted(); // 返回新数组,test 不变
test.toReversed(); // 返回新数组,test 不变
test.toSpliced(); // 返回新数组,test 不变
test.with(); // 返回新数组,test 不变
|
Array.groupBy / Map.groupBy(ES2024+)
ES2024 引入了 groupBy 和 Map.groupBy 方法,让分组变得超级简单!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // groupBy:根据条件分组
const students = [
{ name: '张三', score: 85 },
{ name: '李四', score: 72 },
{ name: '王五', score: 90 },
{ name: '赵六', score: 68 },
{ name: '钱七', score: 95 }
];
// 按及格/不及格分组
const byPass = students.groupBy(s => s.score >= 60 ? 'pass' : 'fail');
console.log('按及格分组:');
console.log(' pass:', byPass.pass.map(s => s.name)); // [ '张三', '李四', '王五', '赵六' ]
console.log(' fail:', byPass.fail.map(s => s.name)); // [ '钱七' ]
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 按分数段分组
const byGrade = students.groupBy(s => {
if (s.score >= 90) return 'A';
if (s.score >= 80) return 'B';
if (s.score >= 70) return 'C';
if (s.score >= 60) return 'D';
return 'F';
});
console.log('按等级分组:', Object.entries(byGrade));
// [ ['A', [{name: '王五', score: 90}, {name: '钱七', score: 95}]],
// ['B', [{name: '张三', score: 85}]],
// ['C', [{name: '李四', score: 72}]],
// ['D', [{name: '赵六', score: 68}]],
// ['F', []] ]
|
1
2
3
4
5
6
7
8
9
10
11
| // groupByToMap:返回 Map 而不是普通对象
const byGradeMap = students.groupByToMap(s => {
if (s.score >= 90) return 'A';
if (s.score >= 80) return 'B';
if (s.score >= 70) return 'C';
if (s.score >= 60) return 'D';
return 'F';
});
console.log('Map分组:', byGradeMap.get('A'));
// [ { name: '王五', score: 90 }, { name: '钱七', score: 95 } ]
|
1
2
3
4
| // groupBy 不会包含空数组
const items = [1, 2, 3, 4, 5, 6];
const grouped = items.groupBy(n => n % 2 === 0 ? 'even' : 'odd');
console.log('分组结果:', Object.keys(grouped)); // ['even', 'odd'] —— 不会包含空分组
|
findLast / findLastIndex
ES2023 还引入了 findLast 和 findLastIndex,从数组末尾开始查找。
1
2
3
4
5
6
7
8
| // findLast:从后往前找第一个满足条件的元素
const fruits = ['苹果', '香蕉', '橙子', '葡萄', '香蕉', '西瓜'];
console.log('最后一个香蕉:', fruits.findLast(f => f === '香蕉')); // 最后一个香蕉: 香蕉(第二个)
console.log('最后一个以"西"开头的:', fruits.findLast(f => f.startsWith('西'))); // 西瓜
// 对比 find:从前往后找
console.log('第一个香蕉:', fruits.find(f => f === '香蕉')); // 第一个香蕉: 香蕉(第一个)
|
1
2
3
4
5
6
| // findLastIndex:返回从后往前第一个满足条件的索引
const numbers = [1, 3, 5, 7, 9, 3, 5];
console.log('最后一个3的位置:', numbers.findLastIndex(n => n === 3)); // 5(索引5)
console.log('第一个3的位置:', numbers.findIndex(n => n === 3)); // 2(索引2)
console.log('最后一个大于5的:', numbers.findLast(n => n > 5)); // 9
|
1
2
3
4
5
6
7
8
9
10
11
| // 实际应用:获取最新添加的数据
const logs = [
{ time: '10:00', level: 'info' },
{ time: '10:05', level: 'warn' },
{ time: '10:10', level: 'error' },
{ time: '10:15', level: 'warn' },
{ time: '10:20', level: 'info' }
];
const latestError = logs.findLast(log => log.level === 'error');
console.log('最新错误:', latestError); // { time: '10:10', level: 'error' }
|
💡 本章小结(第22章第3节)
ES2023+ 为数组带来了很多实用的新方法:flat()/flatMap() 用于扁平化数组;toReversed()/toSorted()/toSpliced()/with() 是非变异版本的方法,不会修改原数组;groupBy()/groupByToMap() 简化了分组操作;findLast()/findLastIndex() 从数组末尾开始查找。在 React 等强调不可变性的框架中,非变异方法尤为重要!
22.4 其他新特性
顶层 await(ES2022+)
顶层 await(Top-level await)让你可以在模块顶层使用 await,而不用包在 async 函数里。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 以前的写法:必须包在 async 函数里
async function loadConfig() {
const response = await fetch('/api/config');
const config = await response.json();
return config;
}
const config = loadConfig(); // 返回的是 Promise,不是 config!
config.then(c => console.log('配置:', c));
// 顶层 await 写法
// const config = await fetch('/api/config').then(r => r.json());
// console.log('配置:', config); // 直接得到配置对象
|
1
2
3
4
5
6
7
8
| // 实际应用:模块初始化
// config.js
// const config = await fetch('/api/config').then(r => r.json());
// export default config;
// anotherModule.js
// import config from './config.js';
// console.log(config); // 已经解析好了
|
1
2
3
4
5
6
7
| // 顶层 await 的风险:阻塞
// 如果在模块顶层使用 await,会阻塞后续代码的执行
// 这在某些场景下很有用,但在另一些场景下可能是灾难
// 例如:
// const data = await fetch('/api/slow-endpoint').then(r => r.json());
// console.log('这条语句要等上面的 fetch 完成才能执行');
|
1
2
3
4
5
6
7
8
9
10
| // 错误处理
try {
const data = await fetch('/api/config').then(r => {
if (!r.ok) throw new Error('请求失败');
return r.json();
});
console.log('数据:', data);
} catch (error) {
console.error('获取数据失败:', error);
}
|
Object.hasOwn()(ES2022+)
Object.hasOwn() 是 Object.prototype.hasOwnProperty() 的更安全替代品。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| const obj = { name: '张三', age: 30 };
// 旧方法
console.log('hasOwnProperty:', obj.hasOwnProperty('name')); // true
console.log('hasOwnProperty:', obj.hasOwnProperty('toString')); // false
// hasOwn 的优势
// 1. 不会被子类重写
class NewObj {
constructor() {
this.name = '李四';
}
hasOwnProperty() {
return false; // 重写了 hasOwnProperty
}
}
const newObj = new NewObj();
console.log('hasOwnProperty 调用:', newObj.hasOwnProperty('name')); // false(被破坏了)
console.log('Object.hasOwn:', Object.hasOwn(newObj, 'name')); // true(不受影响)
|
1
2
3
4
5
6
7
| // 2. 更简洁的语法
const proto = { inherited: 'inherit' };
const child = Object.create(proto);
child.own = 'own';
console.log('Object.hasOwn:', Object.hasOwn(child, 'own')); // true
console.log('Object.hasOwn:', Object.hasOwn(child, 'inherited')); // false
|
1
2
3
4
| // 3. 可以处理原始值
// Object.hasOwn(123, 'toString'); // TypeError! 第一个参数必须是对象
Object.hasOwn({ name: 'test' }, 'name'); // true
|
Promise.withResolvers()(ES2024+)
Promise.withResolvers() 是 ES2024 新增的语法糖,让创建 Promise 和它的 resolve/reject 函数变得更简单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 传统写法
function fetchData() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// 使用 setTimeout 模拟异步操作
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('数据加载成功!');
} else {
reject(new Error('加载失败'));
}
}, 1000);
return { promise, resolve, reject };
}
// 使用
const { promise, resolve, reject } = fetchData();
promise
.then(data => console.log('成功:', data))
.catch(err => console.error('失败:', err));
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Promise.withResolvers() 写法(ES2024+)
function fetchDataNew() {
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('数据加载成功!');
} else {
reject(new Error('加载失败'));
}
}, 1000);
return { promise, resolve, reject };
}
|
import.meta 是一个提供模块元信息的对象,包含当前模块的 URL 等信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // import.meta.url:当前模块的 URL
// 在浏览器中
// console.log(import.meta.url); // file:///path/to/current/module.js
// 在 Node.js 中
// console.log(import.meta.url); // file:///path/to/current/module.js
// 应用:获取当前文件的目录
// import { fileURLToPath } from 'url';
// import { dirname } from 'path';
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
|
1
2
| // import.meta.meta:模块的元数据(实验性)
// console.log(import.meta.meta); // { url: '...', ... }
|
Import Attributes / with { type: ‘json’ }
Import Attributes(之前叫 Import Assertions)允许你在导入时指定资源的类型,比如导入 JSON 文件。
1
2
3
4
5
6
7
8
9
10
| // 导入 JSON 文件
// import config from './config.json' with { type: 'json' };
// 以前需要这样
// const response = await fetch('./config.json');
// const config = await response.json();
// 现在可以直接 import
// import books from './books.json' with { type: 'json' };
// console.log(books); // 直接是解析后的对象
|
1
2
3
4
5
6
7
8
| // 应用:确保导入的是正确类型的资源
// 防止导入错误的文件类型
try {
// import data from './data.txt' with { type: 'json' };
// 这里会报错,因为 data.txt 不是 JSON
} catch (e) {
console.error('导入类型不匹配');
}
|
graph TD
A["ES2020+ 新特性"] --> B["可选链 ?."]
A --> C["空值合并 ??"]
A --> D["逻辑赋值 ||= &&= ??="]
A --> E["ES2023+ 新特性"]
E --> E1["flat / flatMap"]
E --> E2["toReversed / toSorted"]
E --> E3["toSpliced / with"]
E --> E4["groupBy / groupByToMap"]
E --> E5["findLast / findLastIndex"]
A --> F["ES2022+ 新特性"]
F --> F1["顶层 await"]
F --> F2["Object.hasOwn()"]
A --> G["ES2024+ 新特性"]
G --> G1["Promise.withResolvers()"]
G --> G2["Import Attributes"]💡 本章小结(第22章第4节)
ES2020-2024 为 JavaScript 带来了大量实用新语法:顶层 await 简化了模块初始化;Object.hasOwn() 是更安全的 hasOwnProperty 替代品;Promise.withResolvers() 让 Promise + resolve/reject 的模式更简洁;import.meta 提供了模块的元信息;Import Attributes 让你在导入时指定资源类型。这些新特性让 JavaScript 越来越像一门"成熟"的语言了!
本章小结(第22章)
1. 链式操作
- 可选链
?.:安全访问深层属性,为 null/undefined 时不报错 - 空值合并
??:只在 null/undefined 时使用默认值,不被假值误导
2. 逻辑赋值
||=:值为 falsy 时赋值&&=:值为 truthy 时赋值??=:值为 null/undefined 时赋值
3. 数组新方法
flat/flatMap:扁平化数组toReversed/toSorted/toSpliced/with:非变异操作,不修改原数组groupBy/groupByToMap:简化分组操作findLast/findLastIndex:从末尾开始查找
4. 其他新特性
- 顶层 await:模块顶层直接使用 await
- Object.hasOwn():更安全的 hasOwnProperty 替代
- Promise.withResolvers():简化 Promise + resolve/reject 模式
- import.meta:获取模块元信息
- Import Attributes:导入时指定资源类型
记忆口诀
可选链防报错,空值合并不怕假
逻辑赋值三兄弟,||= &&= ??=
数组方法越来越甜,flat groupBy findLast
新特新语法学不完,JavaScript 真能干!