S03-11 JS-高级-ES6-内置类
[TOC]
Symbol
API-Symbol
构造方法:
- Symbol():
(description?)
,ES2015,原始数据类型,用于创建唯一标识符作为属性键,避免对象属性名冲突。
静态属性:
- Symbol.iterator:
()=>Iterator
,是 JS 中的内置 Symbol,它定义了对象的默认迭代器,是使对象可迭代的关键机制。
属性:
- sym.description:
string|undefined
,ES2020,只读,用于获取创建 Symbol 时传入的可选描述字符串。
静态方法:
- Symbol.for():
(description)
,用于在全局 Symbol 注册表中搜索或创建 Symbol。 - Symbol.keyFor():
(sym)
,用于检索全局 Symbol 注册表中 Symbol 关联键(描述字符串) 的方法。
相关方法:
- Object.getOwnPropertySymbols():
(obj)
,用于获取对象自身的所有 Symbol 属性键的静态方法。Symbol 属性键在常规属性遍历(如Object.keys()
或for...in
)中不可见。
ES5 痛点
ES5 痛点:属性名冲突
在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
示例:添加新属性时覆盖原属性
比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性。
示例:封装方法中覆盖原属性
比如我们前面在讲 apply、call、bind 实现时,我们有给其中添加一个 fn 属性,那么如果它内部原来已经有了 fn 属性了呢?
示例:混入中覆盖原属性
比如开发中我们使用混入,那么混入中出现了同名的属性,必然有一个会被覆盖掉。
基本使用
Symbol(符号):ES2015,第七种原始数据类型,用于创建唯一标识符作为属性键,避免对象属性名冲突。
对象属性名类型:在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值。
创建 Symbol:
局部 Symbol
js// 创建匿名 Symbol const anonymousSymbol = Symbol(); // 创建带描述的 Symbol const idSymbol = Symbol("user_id");
全局 Symbol
jsconst globalSym = Symbol.for('key')
应用场景:
作为对象属性键
js// 作为对象属性键 const idSymbol = Symbol("user_id"); const user = { name: "Alice", [idSymbol]: 12345 // 使用 Symbol 作为键 }; console.log(user[idSymbol]); // 12345
核心特性:
唯一性:每个 Symbol 值都是完全唯一的。
jsconst sym1 = Symbol(); const sym2 = Symbol(); console.log(sym1 === sym2); // false // 相同描述仍产生不同 Symbol const sym3 = Symbol("desc"); const sym4 = Symbol("desc"); console.log(sym3 === sym4); // false
不可变性:Symbol 一旦创建就不能被修改。
非字符串属性键:可用作对象属性名,避免命名冲突。
description:ES2020,可以在创建 Symbol 值的时候传入一个描述文本。
对象属性键
Symbol 作为对象属性键:
我们通常会使用 Symbol 在对象中表示唯一的属性名,它有以下三种写法:
写法一:属性名赋值
jsconst user = {} user[idSymbol] = 12345
写法二:
Object.defienProperty()
jsObject.defineProperty(user, idSymbol, { value: 12345, enumerable: true, configurable: true, writable: true })
写法三:对象字面量
jsconst user = { name: "tom", [idSymbol]: 12345 // 使用 Symbol 作为键 };
遍历 Symbol 属性键
遍历Symbol属性键:
Symbol 作为属性键,具有不可枚举性:
- 不能通过 Object.keys() 或
for...in
遍历。 - 只能通过 Object.getOwnPropertySymbols() 或 Reflect.ownKeys() 遍历。
const sym1 = Symbol("key1");
const sym2 = Symbol("key2");
const obj = {
[sym1]: "value1",
[sym2]: "value2",
name: "Alice"
};
const symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); // [Symbol(key1), Symbol(key2)]
Symbol 分类
局部 Symbol
局部 Symbol:通过 Symbol()
创建。
const localSymbol = Symbol("local");
特性:
- 每次调用创建唯一值。
- 不会添加到全局注册表。
- 最适合模块内部使用。
全局 Symbol
前面我们讲 Symbol 的目的是为了创建一个独一无二的值,那么如果我们现在就是想创建相同的 Symbol应该怎么来做呢?
答案是使用 Symbol.for()
。
全局 Symbol:通过 Symbol.for()
创建或获取。
// 创建或获取全局 Symbol
const globalSymbol1 = Symbol.for("app.id");
const globalSymbol2 = Symbol.for("app.id");
console.log(globalSymbol1 === globalSymbol2); // true
特性:
在全局 Symbol 注册表中存储
跨领域共享(iframe、Web Workers 等)
相同的 key,通过
Symbol.for()
可以生成相同的 Symbol 值jsconst s1 = Symbol.for("ss"); const s2 = Symbol.for("ss"); console.log(s1 === s2); // true
通过
Symbol.keyFor()
可以获取通过 Symbol.for()传入的 keyjsconst s1 = Symbol.for("ss"); const s3 = Symbol.for() console.log(Symbol.keyFor(s1)); // ss console.log(Symbol.keyFor(s3)); // undefined
Set
API-Set
构造方法:
- new Set():
(iterable?)
,用于创建一个 Set 对象的构造函数。暂时没有字面量创建方式。
属性:
- set.size:
number
,只读,用于获取当前 Set 中元素的数量。
方法:
- set.add():
(value)
,修改原集合,用于向 Set 集合中添加新元素。遵循集合的唯一性原则,自动过滤重复值。 - set.delete():
(value)
,修改原集合,用于从集合中移除指定的元素。 - set.has():
(value)
,用于高效检查指定值是否存在于集合中。 - set.clear():
()
,修改原集合,用于一次性移除集合中的所有元素。 - set.forEach():
(callbackFn, thisArg?)
,允许对集合中的每个元素遍历执行指定操作。
基本使用
ES5:在 ES6 之前,我们存储数据的结构主要有两种:数组、对象。
ES6:在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式 WeakSet、WeakMap。
Set:是 ES6 引入的一种集合数据结构,它允许你存储任何类型的唯一值(包括原始类型和对象引用)。
对比数组:类似于数组,但是和数组的区别是元素不能重复。
核心特性:
- 唯一性保证:自动过滤重复值
- 值类型多样性:支持所有 JS 类型
- 插入顺序保留:元素按添加顺序排列
- 高效操作:添加、删除、查找均为 O(1) 时间复杂度
创建 Set:
创建 Set 我们需要通过 Set 构造函数(暂时没有字面量创建的方式):
// 1. 空 Set
const emptySet = new Set();
// 2. 从数组初始化(自动去重)
const numberSet = new Set([1, 2, 2, 3]); // Set(3) {1, 2, 3}
// 3. 从字符串初始化
const charSet = new Set("hello"); // Set(4) {"h", "e", "l", "o"}
// 4. 混合类型
const mixedSet = new Set([42, "42", true, null, undefined, {}, NaN]);
唯一性规则
不同于数组,Set 集合中存储的元素都是不能重复。
可以匹配
NaN
:可以正确识别NaN
(尽管NaN !== NaN
)。jsconst set = new Set(); set.add(NaN); set.add(NaN); // 被忽略 console.log(set.size); // 1
严格类型检查:不同类型但值相同的元素被区分。
jsset.add(5); set.add("5"); // 添加成功,5 和 "5" 视为不同值 console.log(set.size); // 3
对象引用区分:只匹配相同内存地址的对象。
jsconst obj1 = { id: 1 }; const obj2 = { id: 1 }; // 内容相同但引用不同 set.add(obj1); set.add(obj2); // 添加成功 console.log(set.size); // 5
迭代操作
Set 的参数是可迭代对象,它支持以下迭代遍历操作:
forEach 遍历
jsconst techStack = new Set(["JavaScript", "React", "Node.js"]); techStack.forEach((value, key, set) => { console.log(value); // value 和 key 相同 });
for...of 循环
jsfor (const tech of techStack) { console.log(tech); }
转换为数组
jsconst techArray = [...techStack];
应用:数组去重
数组去重:
方式一:利用Set特性实现
我们可以发现 Set 中存放的元素是不会重复的,那么 Set 有一个非常常用的功能就是给数组去重。
// 4. 应用:数组去重
const arr = ["刘备", "关羽", "张飞", "吕布", "关羽", "刘备"];
const set2 = new Set(arr);
console.log(set2); // Set(4) {'刘备', '关羽', '张飞', '吕布'}
const set3 = Array.from(set2);
console.log(set3); // (4) ['刘备', '关羽', '张飞', '吕布']
// 简单写法一
console.log(Array.from(new Set(arr))); // (4) ['刘备', '关羽', '张飞', '吕布']
// 或者写法二
console.log([...new Set(arr)]); // (4) ['刘备', '关羽', '张飞', '吕布']
方式二:遍历数组,判断元素是否已存在
判断 Set@
判断Set类型有以下2种方法:
方法一:instanceof
const s = new Set([1,2,3])
console.log(s instanceof Set) // true
方法二:Object.prototype.toString.call()
const s = new Set([1,2,3])
console.log(Object.prototype.toString.call(s)) // [object Set]
WeakSet
API-WeakSet
构造方法:
- new WeakSet():
(iterable?)
,用于创建一个 WeakSet 对象的构造函数。暂时没有字面量创建方式。
方法:
- weakSet.add():
(value)
,用于向弱集合中添加对象引用。 - weakSet.delete():
(value)
,用于从弱集合中移除特定对象的引用。 - weakSet.has():
(value)
,用于确定特定对象是否存在于弱集合中。
基本使用
WeakSet:是 ES6 引入的一种特殊的集合类型,它专门用于存储对象的弱引用,这意味着它们不会阻止垃圾回收机制回收这些对象。
核心特性:
不同于 Set:
仅存储对象:只能添加对象引用(不能存储原始值)
jsnew WeakSet().add("string"); // TypeError
弱引用机制:如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收,无法控制何时从 WeakSet 中移除对象。
jslet obj = { data: "important" }; const weakSet = new WeakSet(); weakSet.add(obj); // 解除对象引用 obj = null; // 垃圾回收后,对象自动从 WeakSet 中移除 // 无法验证,但引擎会在适当时机回收
不可迭代:没有遍历方法(如 forEach、keys、values)
原理: WeakSet 只是对对象的弱引用,如果遍历获取到其中的元素,有可能会造成对象不能正常的销毁。
js// 以下操作都会导致错误 weakSet.forEach(() => {}); console.log(weakSet.size); for (const item of weakSet) {}
无 size 属性:无法获取元素数量
自动清理:当对象被回收时自动从集合中移除
类似 Set:
- 唯一性:内部元素不能重复
对比 Set:
特性 | WeakSet | Set |
---|---|---|
元素类型 | 仅对象 | 任意类型 |
引用强度 | 弱引用 | 强引用 |
垃圾回收影响 | 对象可被回收 | 对象不会被回收 |
可迭代性 | ❌ 不可迭代 | ✅ 可迭代 |
size 属性 | ❌ 无 | ✅ 有 |
clear() 方法 | ❌ 无 | ✅ 有 |
内存管理 | 自动清理 | 需手动管理 |
使用场景 | 关联对象元数据、临时跟踪对象 | 通用唯一值集合 |
创建 WeakSet:
创建 WeakSet 我们需要通过 WeakSet 构造函数:
// 空 WeakSet
const weakSet = new WeakSet();
// 使用对象初始化
const obj1 = { id: 1 };
const obj2 = { id: 2 };
const weakSet = new WeakSet([obj1, obj2]);
强引用/弱引用@
强引用
强引用:普通对象的内存图
说明:
- 普通对象被重新赋值为 null 时,就断开了和内存中对象的联系。
- 但是由于之前已经将对象的内存地址赋值给了数组 arr,赋值为 null 后这些对象依然被数组 arr 所引用,所以它们并不会被销毁。
弱引用
弱引用:WeakSet 内存图
说明: 添加到 WeakSet 中的对象都是弱引用,可能会被 GC 随时回收。
应用场景
WeakSet 应用:限制类中方法的调用者
事实上这个问题并不好回答,我们来使用一个 Stack Overflow 上的答案;
需求:默认情况下,类中的running()
方法有多种方式可以调用,我们希望可以限制它的这种任意性:只有通过类的实例对象才能调用类中的方法。
思路:
每次new一个实例时,将该实例保存到WeakSet中。
之后调用类中方式时,判断是否是通过保存的实例调用。
注意:此处用 WeakSet 的好处:
- 使用 WeakSet 想要销毁实例对象 p 的时候,可以直接通过
p = null
销毁。 - 如果使用 Set 的话,由于实例对象一直被 Set 引用,所以无法销毁。
Map
API-Map
构造方法:
- new Map():
(iterable?)
,用于创建一个新的 Map 对象的构造函数。暂时没有字面量创建方式。
方法:
- map.set():
(key, value)
,修改原集合,用于添加或更新键值对。 - map.get():
(key)
,用于根据键获取对应的值。 - map.has():
(key)
,用于检查指定键是否存在于 Map 中。 - map.delete():
(key)
,修改原集合,用于从 Map 中删除指定的键值对。 - map.clear():
()
,修改原集合,用于完全清空 Map 中的所有键值对。 - map.forEach():
(callbackFn, thisArg?)
,用于按插入顺序遍历 Map 的所有键值对并执行回调函数。
基本使用
对象类型的局限性:
之前我们一直使用对象来存储映射关系,对象存储有什么局限呢:
对象存储映射关系只能用字符串或 Symbol(ES6新增) 作为属性名。
某些情况下我们可能希望使用其他类型(如对象)作为属性名,这个时候会自动将对象转成字符串来作为key,这并不能达成我们的需求。
此时我们就可以使用Map:
Map:是 JS 于 ES6 引入的一种用于存储键值对(key-value pairs,映射关系)的集合数据结构。
核心特性:
任意类型的键:
- 对象的键:只能是
String
或Symbol
。 - Map 的键:可以是任意类型:函数、对象、数字、布尔值、
NaN
,甚至另一个Map
。
jsconst map = new Map(); map.set({ id: 1 }, "对象作为键"); // ✅ 对象作为键 map.set(42, "数字作为键"); // ✅ 数字作为键 map.set(() => {}, "函数作为键"); // ✅ 函数作为键
- 对象的键:只能是
Map中不能存储包含相同键的元素,可以存储包含相同值得元素。
jsconst m4 = new Map([ [1, "tom"], [1, "jack"], // 覆盖前项 ["name", "jerry"], ["nick", "jerry"], // OK ]); console.log("m4: ", m4); // => {1 => 'jack', 'name' => 'jerry', 'nick' => 'jerry'}
遍历顺序:保持插入顺序
- 遍历 Map 时:元素严格按插入顺序返回。
- 变量普通对象时:属性顺序不保证稳定(尤其在数字键上)。
高性能的增删查改:
对频繁增删键值对的场景进行了优化,性能优于普通对象。
对比 普通对象:
特性 | Map | Object |
---|---|---|
键的类型 | 任意值(函数、对象等) | 仅 String / Symbol |
顺序保证 | 插入顺序 | 不保证顺序(尤其数字键) |
大小获取 | map.size 直接获取 | 需手动计算 Object.keys() |
默认键 | 无默认键(纯净) | 有原型链上的继承属性 |
性能 | 高频增删场景更优 | 静态键值对场景更优 |
序列化 | 不能直接 JSON 序列化 | 支持 JSON 序列化 |
创建 Map:
创建 Map 我们需要通过 Map 构造函数(暂时没有字面量创建的方式):
// 1. 空 Map
const emptyMap = new Map()
// 2. 从数组初始化(二维数组)
const map = new Map([
[{name: 'tom'}, 18],
[{name: 'jack'}, 30]
])
WeakMap
API-WeakMap
构造方法:
- new WeakMap():
(iterable?)
,用于创建一个 WeakMap 对象的构造函数。暂时没有字面量创建方式。
方法:
- weakMap.set():
(key, value)
,修改原集合,用于添加或更新键值对,主要围绕弱引用机制设计。 - weakMap.get():
(key)
,用于根据键对象获取关联的值,其行为受 弱引用机制 约束。 - weakMap.has():
(key)
,用于检查指定的键对象是否存在于 WeakMap 中,其行为受 弱引用机制 约束。 - weakMap.delete():
(key)
,修改原集合,用于从集合中移除指定的键值对。
基本使用
WeakMap:是 ES6 引入的一种特殊的键值对集合,与 Map 类似但有一些关键区别,主要围绕弱引用机制设计。
核心特性:
不同于 Map:
键必须是对象:
WeakMap 的键只能是对象(包括数组、函数等引用类型),不能使用原始值(字符串、数字等)作为键:
jsconst weakMap = new WeakMap(); const objKey = { id: 1 }; weakMap.set(objKey, "关联数据"); // ✅ 有效 weakMap.set("stringKey", "值"); // ❌ TypeError: Invalid value used as weak map key
弱引用机制:
- 键是弱引用:WeakMap 对键的引用是"弱"的
- 不影响垃圾回收:当键对象没有其他引用时,会被垃圾回收器回收
- 自动移除条目:键被回收后,对应的键值对会自动从 WeakMap 中移除
jslet user = { name: "Alice" }; const weakMap = new WeakMap(); weakMap.set(user, "敏感数据"); user = null; // 移除对对象的唯一引用 // 垃圾回收后,user 对象及关联数据会被自动清除
不可枚举性:
WeakMap 没有遍历能力:
- 没有
keys()
、values()
、entries()
方法 - 没有
forEach()
方法 - 没有
size
属性
jsconsole.log(weakMap.size); // undefined weakMap.forEach(...); // TypeError: weakMap.forEach is not a function
- 没有
WeakMap 的局限性:
- 无法清空:没有
clear()
方法 - 无法查看内容:调试时无法检查内部数据
- 依赖垃圾回收:清除时机不确定
- 不支持原始值键:只能用对象作为键
对比 Map:
特性 | WeakMap | Map |
---|---|---|
键类型 | 仅对象 | 任意类型 |
垃圾回收 | 键无引用时自动回收 | 阻止键被回收 |
可枚举性 | 不可枚举 | 可枚举(keys/values/entries) |
大小查询 | 无 size 属性 | 有 size 属性 |
遍历能力 | 无遍历方法 | 有 forEach 等方法 |
内存管理 | 自动内存管理 | 需手动删除避免内存泄漏 |
使用场景 | 元数据/私有数据/缓存 | 通用键值存储 |
创建 WeakMap:
创建 WeakSet 我们需要通过 WeakSet 构造函数,暂时没有字面量创建方式:
// 空 WeakMap
const emptyWM = new WeakMap();
// 从数组初始化(二维数组)
const key1 = { name: 'key1' };
const key2 = [1, 2, 3];
const weakMap = new WeakMap([
[key1, '值1'],
[key2, { data: '值2' }]
]);
// 从Map初始化
const sourceMap = new Map([
[document.body, '页面主体'],
[new Date(), '创建时间']
]);
const domWM = new WeakMap(sourceMap);
应用场景
WeakMap 应用:实现响应式
WeakMap 有什么作用呢?(后续专门讲解)
FinalizationRegistry
API-FinalizationRegistry
构造方法:
- new FinalizationRegistry():
(cleanupCallback)
,ES2021,允许开发者注册一个回调函数,当对象被垃圾回收时自动执行清理操作。
方法:
- registry.register():
(target, heldValue, unregisterToken?)
,ES2021,用于注册需要垃圾回收监视的对象。 - registry.unregister():
(unregisterToken)
,ES2021,用于取消之前通过register()
方法设置的垃圾回收监听。
基本使用
FinalizationRegistry:ES2021,允许开发者注册一个回调函数,当对象被垃圾回收时自动执行清理操作。它在管理外部资源(如文件句柄、网络连接等)时非常有用。
垃圾回收关联:
当一个对象被注册到
FinalizationRegistry
后,当该对象被垃圾回收器回收时,注册的回调函数会被触发(具体时机由 JS 引擎决定)。资源清理:
主要用途是释放对象关联的外部资源(如内存、文件描述符等),这些资源无法被 JS 的垃圾回收器自动管理。
注意事项:
执行时机不确定
回调函数可能在对象回收后很久才执行,甚至可能永不执行(如页面快速关闭时)。不应依赖它处理关键逻辑。
避免引用目标对象
回调中不能直接引用被回收的对象(它已不存在)。应通过
heldValue
传递必要信息。性能影响
过度使用可能影响垃圾回收效率,仅在必要时使用。
优先显式清理
对于重要资源(如数据库连接),永远优先使用显式清理方法(如
.close()
),FinalizationRegistry
仅作为最后保障。
使用方法:
创建注册表:
new FinalizationRegistry():
(cleanupCallback)
,ES2021,允许开发者注册一个回调函数,当对象被垃圾回收时自动执行清理操作。jsconst registry = new FinalizationRegistry(heldValue => { console.log(`清理资源: ${heldValue}`); })
注册对象:
registry.register():
(target, heldValue, unregisterToken?)
,ES2021,用于注册需要垃圾回收监视的对象。jsconst obj = { /* 某个对象 */ }; const externalResource = { id: "file#123" }; // 关联的外部资源 // 注册对象 registry.register(obj, "资源标识", externalResource); // 或(推荐):将 externalResource 作为 heldValue registry.register(obj, externalResource);
取消注册(可选)
registry.unregister():
(unregisterToken)
,ES2021,用于取消之前通过register()
方法设置的垃圾回收监听。jsregistry.unregister(externalResource); // 传入注册时的 heldValue/token
完整示例:
jsclass FileWrapper { constructor(filename) { this.resource = { fd: openFile(filename) }; // 模拟外部资源 this.registry = new FinalizationRegistry(fd => { closeFile(fd); // 垃圾回收时自动关闭文件 }); this.registry.register(this, this.resource.fd, this.resource); } // 显式清理(推荐优先使用) close() { closeFile(this.resource.fd); this.registry.unregister(this.resource); // 取消注册 } } // 使用 const file = new FileWrapper("test.txt"); // 当 file 对象被垃圾回收时,自动触发 closeFile
示例:对象被删除时触发注册的回调
WeakRef
API-WeakRef
构造方法:
- new WeakRef():
(targetObject)
,ES2021,允许创建不会阻止垃圾回收的对象弱引用。
方法:
- weakRef.deref():
()
,ES2021,用于访问弱引用指向的目标对象。
强引用/弱引用
强引用:如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用。
弱引用:如果我们希望是一个弱引用的话,可以使用WeakRef。
强引用的问题:
- 易造成内存泄露:当要删除一个对象时,需要删除它的所有引用才能彻底删除。
基本使用
WeakRef:ES2021,允许开发者创建对对象的弱引用,不会阻止垃圾回收器回收该对象。
创建 WeakRef:
new WeakRef():(targetObject)
,ES2021,允许创建不会阻止垃圾回收的对象弱引用。
const obj = { data: "important" };
const weakRef = new WeakRef(obj);
访问目标对象:获取弱引用对象中的属性值需要通过 ref.deref()
取。
weakRef.deref():()
,ES2021,用于访问弱引用指向的目标对象。
// 获取引用对象(如果尚未被回收)
const target = weakRef.deref();
if (target) {
console.log(target.data); // "important"
} else {
console.log("对象已被回收");
}
核心特性:
弱引用:不会阻止垃圾回收器回收该对象。
只能引用对象:参数不能是原始类型
js// ✅ 参数只能是对象 const obj = {} const weakRef = new WeakRef(obj) // OK // ❌ 错误!不能是原始类型 const invalidRef = new WeakRef(42); // TypeError const invalidRef2 = new WeakRef("text"); // TypeError
不可预测的回收时机
jsconst ref = new WeakRef({}); // 移除强引用 obj = null; // 不可靠! setTimeout(() => { if (!ref.deref()) { console.log("已回收"); // 可能永远不会执行 } }, 1000);
示例:
结合
FinalizationRegistry
使用:
Proxy@
API-Proxy
Proxy
构造方法:
- new Proxy():
(target, handler)
,创建对象的代理(拦截器),用于拦截和自定义对象的底层操作。
handler
方法:
监听普通对象:
- handler.set():
(target, property, value, receiver)
,用于拦截对象属性赋值操作:objProxy.xxx=value
。 - handler.get():
(target, property, receiver?)
,用于拦截属性读取操作:objProxy.xxx
。 - handler.has():
(target, property)
,用于拦截属性存在性检查in
操作:in
。 - handler.defineProperty():
(target,property,descriptor)
,用于拦截对象的属性定义操作:defineProperty()
。 - handler.deleteProperty():
(target, property)
,用于拦截对象的属性删除delete
操作:delete
。
监听函数对象:
- handler.apply():
(target, thisArg, args)
,用于拦截函数调用的操作:fnProxy.apply()
。 - handler.construct():
(target, args, newTarget)
,用于拦截构造函数的new
操作:new FnProxy()
。 - handler.getPrototypeOf():
(target)
,用于拦截对象原型[[Prototype]]
访问的操作:getPrototypeOf()
。 - handler.setPrototypeOf():
(target, prototype)
,用于拦截对象原型[[Prototype]]
设置操作:setPrototype()
。 - handler.isExtensible():
(target)
,用于拦截对象可扩展性检查的操作:isExtensible()
。 - handler.preventExtensions():
(target)
,用于拦截对象的不可扩展性设置操作:preventExtensions()
。 - handler.ownKeys():
(target)
,用于拦截对象自身属性键的枚举操作:ownKeys()等
。 - handler.getOwnPropertyDescriptor():
(target, prop)
,用于拦截对象属性描述符的获取操作:getOwnPropertyDescriptor()
。
监听对象属性
需求:监听对象属性的访问/修改
有一个对象,我们希望监听这个对象中的属性被设置或获取的过程。
实现思路:遍历对象的所有属性,对每个属性使用 Object.defineProperty()
的存储属性描述符监听。
上述实现思路的缺点:
Object.defineProperty()
的设计初衷不是为了监听对象中的所有属性:初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
只能监听属性的设置和获取:
如果我们想监听更加丰富的操作(如新增、删除属性等)
Object.defineProperty()
是无能为力的。Proxy/Reflect 就是为了解决上述问题而出现的
基本使用
Proxy:是 ES6 引入的一种高级元编程特性,它允许你创建一个对象的代理(拦截器),用于拦截并自定义该对象的基本操作(如属性访问、赋值、函数调用等)。
核心概念:
- 目标对象(Target):被代理的原始对象。
- 处理器对象(Handler):定义拦截操作的函数集合(称为“陷阱”或“trap”)。
- 代理对象(Proxy):通过
new Proxy(target, handler)
创建的代理实例,所有对它的操作会被handler
拦截。
语法:
- new Proxy():
(target, handler)
,创建对象的代理(拦截器),用于拦截和自定义对象的底层操作。
示例:监听对象属性的访问/修改(Proxy版)
我们可以将上面的案例用Proxy来实现一次:
我们需要new Proxy对象,并且传入需要侦听的对象以及一个处理对象,可以称之为handler;
我们之后的操作都是直接对Proxy的操作,而不是原有的对象,因为我们需要在handler里面进行侦听;
捕获器
所有捕获器
捕获器(Trap):是定义在处理器对象(handler)中的特殊方法,用于拦截目标对象的基本操作。
如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕获器:
13个捕获器分别是做什么的呢?
监听普通对象:
- handler.set():
(target, property, value, receiver)
,用于拦截对象属性赋值操作:objProxy.xxx=value
。 - handler.get():
(target, property, receiver?)
,用于拦截属性读取操作:objProxy.xxx
。 - handler.has():
(target, property)
,用于拦截属性存在性检查in
操作:in
。 - handler.defineProperty():
(target,property,descriptor)
,用于拦截对象的属性定义操作:defineProperty()
。 - handler.deleteProperty():
(target, property)
,用于拦截对象的属性删除delete
操作:delete
。
监听函数对象:
- handler.apply():
(target, thisArg, args)
,用于拦截函数调用的操作:fnProxy.apply()
。 - handler.construct():
(target, args, newTarget)
,用于拦截构造函数的new
操作:new FnProxy()
。 - handler.getPrototypeOf():
(target)
,用于拦截对象原型[[Prototype]]
访问的操作:getPrototypeOf()
。 - handler.setPrototypeOf():
(target, prototype)
,用于拦截对象原型[[Prototype]]
设置操作:setPrototype()
。 - handler.isExtensible():
(target)
,用于拦截对象可扩展性检查的操作:isExtensible()
。 - handler.preventExtensions():
(target)
,用于拦截对象的不可扩展性设置操作:preventExtensions()
。 - handler.ownKeys():
(target)
,用于拦截对象自身属性键的枚举操作:ownKeys()等
。 - handler.getOwnPropertyDescriptor():
(target, prop)
,用于拦截对象属性描述符的获取操作:getOwnPropertyDescriptor()
。
示例:
监听普通对象
set()/get()
- handler.set():
(target, property, value, receiver)
,用于拦截对象属性赋值操作:objProxy.xxx=value
。 - handler.get():
(target, property, receiver?)
,用于拦截属性读取操作:objProxy.xxx
。
示例:set()/get()
deleteProperty()
- handler.deleteProperty():
(target, property)
,用于拦截对象的属性删除delete
操作:delete
。
示例:deleteProperty()
has()
- handler.has():
(target, property)
,用于拦截属性存在性检查in
操作:in
。
示例:has()
监听函数对象
当然,我们还会看到捕捉器中还有construct和apply,它们是应用于监听函数对象的:
construct()
- handler.construct():
(target, args, newTarget)
,用于拦截构造函数的new
操作:new FnProxy()
。
示例:construct()
apply()
- handler.apply():
(target, thisArg, args)
,用于拦截函数调用的操作:fnProxy.apply()
。
示例:apply()
Reflect@
API-Reflect
Reflect中有哪些常见的方法呢?它和Proxy是一一对应的,也是13个:
- 监听普通对象
- Reflect.get(target, prop, receiver?):
obj.name
,获取属性值 - Reflect.set(target, prop, newValue, receiver?):
obj.name = 'mr'
,设置属性值 - Reflect.has(target, prop):
in
, 判断是否存在某属性 - Reflect.defineProperty(target, prop, descriptor):
Object.defineProperty
, 设置属性描述符 - Reflect.deleteProperty(target, prop):
delete
,删除属性 - 监听函数对象
- Reflect.apply(target, thisArg, args):
Object.prototype.apply
,函数调用 - Reflect.construct(target, args, newTarget?):
new
,调用构造函数 - Reflect.getPrototypeOf(target):
Object.getPrototypeOf
, 获取对象的原型 - Reflect.setPrototypeOf(target, prototype):
Object.setPrototypeOf
, 设置对象的原型 - Reflect.isExtensible(target):
Object.isExtensible
, 判断是否可以新增属性 - Reflect.preventExtensions(target):
Object.preventExtensions
, 阻止对象扩展 - Reflect.ownKeys(target):
Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
,获取自身上的所有属性 - Reflect.getOwnPropertyDescriptor(target, prop):
Object.getOwnPropertyDescriptor
, 获取自身上的属性描述符
基本使用
Reflect:是 JS 的一个内置对象,它提供了一组用于执行对象基本操作的静态方法。这些方法与 Proxy 对象的 handler 方法一一对应,用于实现对象的基本操作(如属性访问、定义、删除等)。
作用:
- 提供了一组统一、函数式的对象操作方法
- 与 Proxy 完美配合,与 Proxy handler 一一对应,实现强大的拦截和自定义行为
- 为对象操作提供了更合理、更安全的替代方案
- 使 JS 的反射式编程能力标准化
Reflect 出现原因:将 Object 中用于操作对象的一类方法抽取到 Reflect 中,实现更加规范的设计。
如果我们有Object可以做这些操作,那么为什么还需要有Reflect这样的新增对象呢?
这是因为在早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面;
但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;
另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的;
所以在ES6中新增了Reflect,让我们将这些操作都集中到了Reflect对象上;
另外在使用Proxy时,可以做到不操作原对象;
对比 Object 方法:
返回值不同:
以下方法中Object返回原对象,而Reflect返回操作是否成功:
defineProperty()
preventExtensions()
setPrototypeOf()
目标对象是否支持原始类型:
以下方法中Object会自动包装原始类型,而Reflect会直接报TypeError错误:
getOwnPropertyDescriptor()
getPrototypeOf()
isExtensible()
示例:对比删除属性
Object的方式:
delete obj.name
缺点:
- 无法直接返回是否删除成功,需要通过重新访问属性判断是否删除成功。
- 严格模式下,删除不可配置属性会报错
Reflect的方式:
Reflect.deleteProperty()
结合Proxy使用
Reflect结合Proxy使用:
结合使用思路:可以将之前Proxy案例中对原对象的操作,都修改为Reflect来操作:
Reflect结合Proxy使用的好处:
单独使用Proxy的问题:
- 缺点一:会直接操作原对象:这就失去了代理的意义。
- 缺点二:无法判断操作是否成功
- 缺点三:无法接收Proxy的
receiver
:
Reflect结合Proxy的好处:
好处1:可以不用再直接操作原对象
好处2:可以通过返回值判断是否操作成功
好处3:可以接收receiver修改setter/getter中的this指向
receiver
就是外层的Proxy对象。Reflect.set/get
的最后一个参数receiver
可以决定对象访问器settter/getter
的this
指向。
接收receiver修改setter/getter中的this指向:实现 setter/getter 中的2次设置都能被 Proxy 监听到。
原理分析:
默认情况下,对象 obj 的 setter/getter 中的 this 是指向原始对象 obj 的。
在 setter/getter 中其实设置了2次 obj 的属性
- 第一次通过
set name
设置属性值 - 第2次通过
this._name
设置属性值
但通过 objProxy 设置/访问 obj 的属性时,只会触发一次 set/get 的监听方法(
this._name
没有监听到)- 这是因为
this._name
设置时,它的this指向的是obj原始对象,没有经过 objProxy 代理,所以无法监听到
- 第一次通过
Reflect中接收的参数 receiver 可以修改 setter/getter 中的this指向,让它指向 objProxy。
从而实现 setter/getter 中的2次设置都能被Proxy监听到。
Reflect.construct()
应用:实现借用构造函数:通过执行父类中的代码创建一个子类的对象实例。
上述代码也可以写在外部
源码:Babel转class继承为ES5写法时有用到
Promise
API-Promise
构造方法:
- new Promise():
(executor)
,ES2015,用于创建一个 Promise 的实例对象。
方法:
- promise.then():
(onFulfilled?, onRejected?)
,ES2015,用于接收当状态变成fulfilled
或rejected
时执行的回调函数。 - promise.catch():
(onRejected)
,ES2015,是 Promise 链式调用中的错误处理方法,用于捕获 Promise 链中发生的任何错误或拒绝。 - promise.finally():
(onFinally)
,ES2018,用于指定一个无论 Promise 最终状态如何(fulfilled 或 rejected)都会执行的回调函数。
静态方法:
- Promise.resolve():
(value)
,ES2015,用于快速创建一个已解决(fulfilled)的 Promise 对象。提供了一种将值转换为 Promise 的标准方式。 - Promise.reject():
(reason)
,ES2015,用于立即创建一个已拒绝(rejected)状态的 Promise 对象。它提供了一种快速失败机制,在异步流程中直接抛出错误或拒绝信号。 - Promise.all():
(iterable)
,ES2015,用于并行处理多个 Promise,当所有 Promise 都成功时返回成功结果,当任意一个 Promise 失败时立即返回失败。 - Promise.allsettled():
(iterable)
,ES2020,用于等待所有 Promise 完成(无论成功或失败),并返回每个 Promise 的最终状态和结果。不会因某个 Promise 失败而提前终止。 - Promise.race():
(iterable)
,ES2015,用于获取第一个完成的 Promise 结果(无论成功或失败)。 - Promise.any():
(iterable)
,ES2021,用于获取第一个成功的 Promise 结果。当所有 Promise 都失败时,它会返回一个包含所有错误信息的 AggregateError。 - Promise.try():
(executor)
,ES2025,用于统一处理 Promise 链中的同步错误和异步错误。 - Promise.withResolvers():
()
,ES2024,
ES5异步处理方案
ES5中异步任务处理方案:回调函数
思路分析:
异步任务指的是那些并不是立即执行的函数,
execCode()
执行完后需要等待3秒才会返回执行结果。此时如果想知道该结果,需要通过在执行函数时传递的回调函数:
当异步任务执行完毕后,会分以下情况:
- 如果任务执行成功,会调用传递的成功回调函数,并将执行结果作为参数传递出去。
- 如果任务执行失败,会调用传递的失败回调函数,并将失败原因作为参数传递出去。
回调函数方案的缺点:
缺点1:定义困难
需要自己设计回调函数:包括回调函数、回调函数的名称、回调函数的使用等。
缺点2:使用困难
对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以便可以理解它这个函数到底怎么用。
缺点3:容易出现回调地狱
针对以上困境,在ES6引入了Promise。
基本使用
Promise:是处理异步操作的核心机制,它代表一个尚未完成但预期将来会完成的操作(如网络请求、文件读取等)。
核心概念:
- 三种状态(单向不可逆):
- Pending(待定):初始状态,操作尚未完成。
- Fulfilled(已兑现):操作成功完成(通过
resolve()
触发)。 - Rejected(已拒绝):操作失败(通过
reject()
触发)。
- 结果不可变: 状态一旦变为
Fulfilled
或Rejected
,结果将永久固定(不可修改或再次改变)。
基本使用:
一、创建 Promise
思路分析:
当我们需要的时候,给予调用者一个承诺:待会儿我会给你回调数据。就可以创建一个Promise的对象;
在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为
executor
。这个回调函数会被立即执行,并且给它传入另外两个回调函数:
resolve
、reject
;- 当我们调用
resolve
回调函数时,会执行Promise对象的then
方法传入的回调函数; - 当我们调用
reject
回调函数时,会执行Promise对象的catch
方法传入的回调函数;
- 当我们调用
二、使用 Promise:
通过 .then()
、.catch()
和 .finally()
处理结果:
方式一:普通写法
方式二:链式调用
核心概念
三种状态
三种状态(单向不可逆):
上面Promise使用过程,我们可以将它划分成三种状态:
- pending(待定):初始状态,操作尚未完成(执行
executor
中代码时的状态)。 - fulfilled(已兑现):操作成功完成(通过
resolve()
触发)。 - rejected(已拒绝):操作失败(通过
reject()
触发)。 - 单向不可逆:状态一旦确定不能更改。只有第一次调用生效,后续调用无效。
- 查看方式:可通过
console.dir(promise)
查看
通过Promise重构之前的异步请求:
那么有了Promise,我们就可以将之前的代码进行重构了:
executor
executor:是在创建Promise时需要传入的一个处理业务逻辑的回调函数,这个回调函数会被立即执行,并且传入两个回调函数作为参数:
new Promise(
// executor 回调函数
(resolve, reject) => {
console.log('executor 代码')
}
)
核心特性:
立即执行:
executor
在 Promise 创建时立即同步执行状态转换:
executor
中执行resolve()
会转为fulfilled
状态executor
中执行reject()
会转为rejected
状态
错误抛出:在
executor
中抛出或出现错误等同于调用reject()
jsnew Promise(() => { throw new Error('出错了!'); // 等同于 reject nonExistentFunction(); // 自动触发 reject }).catch(err => console.log(err)); // 捕获错误
确定 Promise 状态:
通常我们会在executor中确定Promise状态:
通过
resolve
,可以兑现(fulfilled
)Promise的状态,我们也可以称之为已决议(resolved);通过
reject
,可以拒绝(rejected
)Promise的状态;
注意:一旦状态被确定下来,Promise的状态会被锁死(不可逆)
- 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成兑现(fulfilled);
- 之后再去调用reject时,已经不会有任何的响应了(并不是这行代码不会执行,而是无法改变Promise状态);
resolve参数值类型
resolve() 参数值类型的处理:
情况1:普通值(包括原始类型、对象、数组等):
当前 Promise 状态变成 fulfilled,并且该值会作为 then 回调的参数
情况2:另一个 Promise:
当前 Promise 的状态会"跟随"该新 Promise 的状态
情况3:thenable 对象(实现了
then()
方法的对象):会自动执行该 then 方法,并且根据 then 方法的结果来决定当前 Promise 的状态
实例方法
then()
参数
promise.then():(onFulfilled?, onRejected?)
,ES2015,用于接收当状态变成 fulfilled
或 rejected
时执行的回调函数。
onFulfilled?:
res=>void
,当 Promise 解决(fulfilled)时调用的函数onRejected?:
err=>void
,当 Promise 拒绝(rejected)时调用的函数返回:
promise:
Promise
,返回一个新的 Promise 对象。其状态取决于回调函数的返回值类型。
示例:promise.then()
多次调用
一个 promsie 对象的 then 方法可以被多次调用,每次调用都可以传入对应的 fulfilled 回调,当 Promise 状态变成 fulfilled时这些回调都会被执行。
const promise = new Promise((resolve, reject) => {
resolve()
// reject()
})
promise.then(res => console.log('第一次调用', res))
promise.then(res => console.log('第二次调用', res))
promise.then(res => console.log('第三次调用', res))
返回值@
then 方法的返回值会被
new Promise()
包裹:即使返回一个普通的非 Promise类型值,也会被转化为 Promise。
Promise 的状态是等到then方法传入的回调函数有返回值时决定的。
【示例】
链式调用:
then 方法支持链式调用,因为它会返回一个 Promise
then 方法返回的 Promise 的状态:
Promise有三种状态,那么这个Promise处于什么状态呢?
当then方法中的回调函数本身在执行的时候,那么它处于
pending
状态;当then方法中的回调函数返回一个结果时:
情况1:普通值(包括原始类型、对象、数组等):
当前 Promise 状态变成 fulfilled,并且该值会作为 resolve 的参数
情况2:另一个 Promise:
当前 Promise 的状态会"跟随"该新 Promise 的状态,并将新 Promise 的结果作为当前 Promise 的结果
情况3:thenable 对象(实现了
then()
方法的对象):会自动执行该 then 方法,并且根据 then 方法的结果来决定当前 Promise 的状态,并将 thenable 中的结果作为当前 Promise 的结果
当then方法抛出一个异常时,那么它处于
reject
状态;
catch()
多次调用
promise.catch():(onRejected)
,ES2015,是 Promise 链式调用中的错误处理方法,用于捕获 Promise 链中发生的任何错误或拒绝。
onRejected:
err=>any
,当 Promise 被拒绝(rejected)时调用的函数。可以返回:普通值:会使新 Promise 以该值解决(fulfilled)。
Promise:新 Promise 将跟随该 Promise 的状态。
抛出错误:新 Promise 将以该错误拒绝(rejected)。
返回:
promise:
Promise
,返回一个新的 Promise 对象,其状态取决于 onRejected 函数的行为:
多次调用:
一个 promsie 对象的 catch方法可以被多次调用,每次调用都可以传入对应的 rejected 回调,当 Promise 状态变成 rejected 时这些回调都会被执行。
const promise = new Promise((resolve, reject) => {
// resolve()
reject()
})
promise.catch(err => console.log('第一次捕获错误:', err))
promise.catch(err => console.log('第二次捕获错误:', err))
promise.catch(err => console.log('第三次捕获错误:', err))
返回值
事实上catch方法也是会返回一个Promise对象的,所以catch方法后面我们可以继续调用then方法或者catch方法:
特性:
▸ catch方法也会返回一个新的promise对象
可以通过该promise对象继续调用then()、catch()方法
▸ catch方法的执行时机:
catch方法中的回调函数会被最早的promise的reject()方法回调
▸ catch、then方法返回的默认状态都是fulfilled,后续继续执行then方法
下面的代码中,promise.catch()后续是catch中的err2打印,还是then中的res打印呢?
答案是res打印,这是因为catch传入的回调在执行完后,默认状态依然会是fulfilled的;
▸ 如果希望catch、then方法后续执行catch()方法,那么需要通过抛出一个异常修改返回状态为reject
抛出异常的方法:throw new Error('error message')
抛出异常后Promise内部会执行:new Promise(reject => reject(new Error()))
▸ 如果抛出的异常后没有catch()方法捕获处理抛出的异常,就会报错
补充: 中断函数继续执行的方法:
- 方法一:return
- 方法二:throw new Error()
- 方法三:yield(暂时中断函数执行)
finally()
promise.finally():(onFinally)
,ES2018,用于指定一个无论 Promise 最终状态如何(fulfilled 或 rejected)都会执行的回调函数。
onFinally:
()=>any
,回调不接受参数,当 Promise 敲定(settled)时调用的函数(无论成功或失败)。返回:
promise:
Promise
,返回一个新的 Promise 对象。
示例:finally() 基本使用
类方法
resolve()
Promise.resolve():(value)
,ES2015,用于快速创建一个已解决(fulfilled)的 Promise 对象。提供了一种将值转换为 Promise 的标准方式。
value:
any
,可以是任何类型的值,它决定了返回的 Promise 状态。返回:
promise:
Promise
,返回一个 Promise 对象。
前面我们学习的then、catch、finally方法都属于Promise的实例方法,都是存放在Promise.prototype上的。
下面我们再来学习一下Promise的类方法。
特性:
▸ Promise.resolve的用法相当于new Promise,并且执行resolve操作
▸ resolve参数的类型:
情况一:参数是一个普通的值或者对象
情况二:参数本身是Promise
情况三:参数是一个thenable
使用场景:
▸有时候我们已经有一个现成的内容,希望将其转成Promise来使用,这个时候我们可以使用 Promise.resolve 方法来完成。
reject()
Promise.reject():(reason)
,ES2015,用于立即创建一个已拒绝(rejected)状态的 Promise 对象。它提供了一种快速失败机制,在异步流程中直接抛出错误或拒绝信号。
reason:
any|Error
,拒绝原因,可以是任意类型(通常为 Error 对象或错误描述)返回:
promise:
Promise
,始终返回一个已拒绝状态(rejected)的 Promise 对象
特性:
▸ Promise.reject的用法相当于new Promise,只是会调用reject:
▸ Promise.reject传入的参数无论是什么形态,都会直接作为rejected状态的参数传递到catch的。
all()
Promise.all():(iterable)
,ES2015,用于并行处理多个 Promise,当所有 Promise 都成功时返回成功结果,当任意一个 Promise 失败时立即返回失败。
iterable:
Iterator
,可迭代对象(如 Array、Set),包含多个 Promise 或其他值。返回:
promise:
Promise
,返回一个新的 Promise 对象。
特性:
▸ Promise.all()的作用是将多个Promise包裹在一起形成一个新的Promise
▸ 新的Promise状态由包裹的所有promise共同决定,所有promise执行逻辑与运算
所有的Promise状态都变成fulfilled状态时,新Promise状态为fulfilled,并将所有promise的返回值组成一个数组;
有一个Promise状态为reject时,新Promise状态为reject,并将第一个reject的返回值作为参数;
应用场景: 发送网络请求时,当同时发送多个网络请求后,想等所有请求都有结果再一起返回,可以使用Promise.all
allSettled()
Promise.allsettled():(iterable)
,ES2020,用于等待所有 Promise 完成(无论成功或失败),并返回每个 Promise 的最终状态和结果。不会因某个 Promise 失败而提前终止。
iterable:
Iterator
,可迭代对象(如 Array、Set),包含多个 Promise 或其他值。返回:
promise:
Promise
,始终返回一个已兑现状态(fulfilled)的 Promise 对象。
all方法的缺陷:当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。
- 那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的;
特性:
▸ Promise.allSettled()方法会在所有的Promise都有结果(settled),无论是fulfilled,还是rejected时,才会有最终的状态;并且结果一定是fulfilled状态
▸ 结果分析:
allSettled的结果是一个数组,数组中存放着每一个Promise的结果,并且是对应一个对象的;
这个对象中包含status状态,以及对应的value值;
注意:reject结果是reason值
race()
Promise.race():(iterable)
,ES2015,用于获取第一个完成的 Promise 结果(无论成功或失败)。
iterable:
Iterator
,可迭代对象(如 Array、Set),包含多个 Promise 或其他值。返回:
promise:
Promise
,返回一个新的 Promise 对象。
特性:
Promise.race()方法的所有promise谁先有结果,那么就使用谁的结果,无论结果是fulfilled还是rejected
any()
Promise.any():(iterable)
,ES2021,用于获取第一个成功的 Promise 结果。当所有 Promise 都失败时,它会返回一个包含所有错误信息的 AggregateError。
iterable:
Iterator
,可迭代对象(如 Array、Set),包含多个 Promise 或其他值。返回:
promise:
Promise
,返回一个新的 Promise 对象。
特性:
▸ any()方法会等到一个fulfilled状态,才会决定新Promise的状态;如果所有的Promise都是reject,那么也会等到所有的Promise都变成rejected状态
▸ any()方法执行逻辑或运算,如果所有的Promise都是reject的,会报AggregateError错误
手写-Promise@
Iterator@
API-Iterator
iterator:({next(), return()?})
,JS中的迭代器,实现了next方法的对象,在next方法中必须返回一个包含了done和value属性的对象
next():
(arg?)
,返回一个包含value
和done
属性的对象。- arg?:
any
,可选参数。 - 返回:
{done, value}
- done:
boolean
,是否已经遍历完所有元素。false
:表示迭代未完成,value值为any;true
:表示迭代完毕,value值为undefined
- value:
any | undefined
,表示当前元素的值。
- arg?:
return()?:
(value)
,结束当前的迭代,并返回一个包含 done 属性的对象。- value:
any
,是return()
方法返回的值,可以是任何类型的值。通常用来返回一个终止值,标识迭代的结束。 - 返回:
{done, value?}
- done:
boolean
,表示迭代是否完成。调用return()
时,这个属性的值通常是true
。 - value?:
any
,返回迭代结束时的值。可以用来返回终止时需要返回的值。
- value:
- js
const customIterator = { items: [1, 2, 3, 4], index: 0, next() { if (this.index < this.items.length) { return { value: this.items[this.index++], done: false }; } else { return { value: undefined, done: true }; } }, return(value) { console.log("Iteration is being stopped early!"); return { value: value, done: true }; } }; const iter = customIterator; console.log(iter.next()); // { value: 1, done: false } console.log(iter.next()); // { value: 2, done: false } console.log(iter.return(99)); // Iteration is being stopped early! { value: 99, done: true }
概念:
- 迭代器:Iterator。它是一种提供遍历集合元素的机制。
- 可迭代对象:Iterable。它是一种实现了Iterator Protocol方法的对象。
- 迭代器协议:Iterator Protocol。它是一种定义迭代器对象的标准,它规定了迭代器对象必须实现的方法和属性。
迭代器
迭代器协议
迭代器协议(Iterator Protocol):是 JS 中定义迭代器对象的标准规则,它规定了迭代器对象如何产生值序列。在 JS 中这个标准就是一个特定的 next()
方法。
迭代器协议-核心规则:
一个对象要成为迭代器,必须实现一个 next()
方法,该方法返回一个包含两个属性的对象:
interface IteratorResult {
value: any; // 当前迭代的值(可选)
done: boolean; // 迭代是否完成
}
基本要求:
next()
方法:无参数或接受一个参数- 返回值:包含
value
和done
属性的对象 - 迭代结束:
done: true
时,value
可省略或为undefined
- 后续调用:迭代结束后继续调用
next()
应返回{ done: true }
迭代器
迭代器(Iterator):提供了一种标准化的方式来遍历各种数据结构。迭代器协议定义了如何按顺序访问集合中的元素,使得不同的数据结构(如数组、Map、Set 等)可以使用统一的遍历机制。
其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
在 JS 中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议。
JS中的迭代器:是一个实现了迭代器协议的对象,它必须包含一个 next()
方法。每次调用 next()
方法都会返回一个包含两个属性的对象:
value
:当前迭代的值。done
:布尔值,表示迭代是否完成。
const arr = ['a', 'b', 'c']
let index = 0
const iterator = {
next: function() {
if(index < arr.length) {
return {done: false, vlaue: arr[index++]}
} else {
return {done: true, value: undefined}
}
}
}
示例:基本使用
专门针对 friends
数组的迭代器
封装:通用的迭代器生成函数
更加通用,所有的数组都可以使用
可迭代对象
可迭代协议
可迭代协议(Iterable Protocol):定义了对象如何被遍历的标准方式。通过实现可迭代协议,对象可以与 JS 的迭代语法(如 for...of
循环、扩展运算符等)无缝集成,提供统一的遍历接口。
可迭代协议-核心规则:一个对象要成为可迭代对象,必须实现 [Symbol.iterator]()
(规范中叫 @@iterator()
)方法,该方法返回一个迭代器对象(iterator)。迭代器对象必须遵循迭代器协议,即实现 next()
方法。
const iterable = {
// 实现可迭代协议
[Symbol.iterator]() {
// 返回迭代器对象(遵循迭代器协议)
return {
next() {
// 返回 { value: any, done: boolean }
}
};
}
};
可迭代协议的要素:
[Symbol.iterator]()
方法:- 是对象的特殊方法(使用
Symbol.iterator
作为键) - 必须返回一个迭代器对象
- 不接受参数(用于自定义迭代行为)
- 是对象的特殊方法(使用
- 迭代器对象:
- 必须实现
next()
方法 - 可选实现
return()
和throw()
方法(用于资源清理和错误处理)
- 必须实现
next()
方法:- 返回包含两个属性的对象:
value
: 当前迭代的值done
: 布尔值,表示迭代是否完成
- 当
done: true
时,value
可省略
- 返回包含两个属性的对象:
可迭代对象
上面的代码整体来说看起来是有点奇怪的:
我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
可迭代对象(Iterable):实现了可迭代协议,即 [Symbol.iterator]()
方法的对象。该方法返回一个迭代器对象(iterator),该迭代器对象必须包含一个 next()
方法,用于访问数据结构中的元素。可迭代对象可以进行迭代操作(如 for...of
循环)。
迭代操作的本质:就是调用可迭代对象的 @@iterator()
方法。
const iterable = {
[Symbol.iterator]() {
// 返回迭代器对象
return {
next() {
// 返回 { value: any, done: boolean }
}
};
}
};
核心特性:
可迭代对象内部需要实现
[Symbol.iterator]()
:该方法返回一个迭代器对象(iterator),该迭代器对象必须包含一个
next()
方法。可迭代对象可以进行迭代操作:
迭代操作包括:
for...of
遍历
优化
1、通用优化:绑定 this
next函数用箭头函数书写,可以让其内部的this指向可迭代对象infos,从而实现更通用的封装
2、通用优化:迭代对象中的键/值/键值对
内置可迭代对象
事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个可迭代对象:
- String、Array、Map、Set、arguments对象、NodeList集合;
用法:
▸ 数组: arr[Symbol.iterator]()
▸ Set: set[Symbol.iterator]()
▸ arguments: arguments[Symbol.iterator]()
应用场景
可迭代对象应用场景:
那么可迭代对象可以被应用在哪里呢?
JS中语法:
for...of
:遍历...
:展开运算符yield*
(后面讲)[name, age] = arr
:解构赋值(Destructuring Assignment)
创建一些对象时,可传入可迭代对象:
- new Map():
(iterable?)
,用于创建一个新的 Map 对象的构造函数。暂时没有字面量创建方式。 - new WeakMap():
(iterable?)
,用于创建一个 WeakMap 对象的构造函数。暂时没有字面量创建方式。 - new Set():
(iterable?)
,用于创建一个 Set 对象的构造函数。暂时没有字面量创建方式。 - new WeakSet():
(iterable?)
,用于创建一个 WeakSet 对象的构造函数。暂时没有字面量创建方式。
- new Map():
一些方法的调用:
- Promise.all():
(iterable)
,ES2015,用于并行处理多个 Promise,当所有 Promise 都成功时返回成功结果,当任意一个 Promise 失败时立即返回失败。 - Promise.race():
(iterable)
,ES2015,用于获取第一个完成的 Promise 结果(无论成功或失败)。 - Array.from():
(arrayLike,mapFn?,thisArg?)
,静态方法,用于将类数组对象或可迭代对象转换为真正的数组,并支持对元素进行映射处理。
- Promise.all():
自定义类的迭代
在前面我们看到Array、Set、String、Map等类创建出来的对象都是可迭代对象。
在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象。
如果我们也希望自定义类创建出来的对象默认是可迭代对象,那么在设计类的时候我们就可以添加上 @@iterator()
方法。
案例需求:创建一个classroom的类
教室中有自己的位置、名称、当前教室的学生;
这个教室可以进来新学生(push);
创建的教室对象是可迭代对象;
自定义类的迭代实现: 自定义类创建出来的对象默认是可迭代对象
实现思路:在设计类时添加 [Symbol.iterator]()
方法。
迭代器-监听中断
迭代器在某些情况下会在没有完全迭代的情况下中断:
遍历的过程中:通过
break
、return
、throw
中断了循环操作;解构的时候:没有解构所有的值;
那么这个时候我们想要监听中断的话,可以添加 return()
或 throw()
方法:
示例:监听遍历中断
1、在迭代器函数中添加 return()
方法
2、当在遍历中使用 break
、return
、throw
语句时,调用 return()
或 throw()
方法
Generator@
概念
生成器(Generator):是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
- 平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
生成器函数:也是一个函数,但是和普通的函数有一些区别:
首先,生成器函数需要在function的后面加一个符号:* :
function* foo()
或者function *foo()
其次,生成器函数可以通过
yield
关键字来控制函数的执行流程:最后,生成器函数执行时返回一个Generator(生成器):
- 要想执行函数内部的代码,需要通过生成器对象调用它的next方法
- 当遇到yield时,就会中断执行,需要再次调用next方法,才会继续执行
生成器本质上是一种特殊的迭代器;
- MDN:Instead, they return a special type of iterator, called a Generator.
生成器函数
我们发现下面的生成器函数foo的执行体压根没有执行,它只是返回了一个生成器对象。
那么我们如何可以让它执行函数中的东西呢?调用next即可;
我们之前学习迭代器时,知道迭代器的next是会有返回值的;
但是我们很多时候不希望next返回的是一个undefined,这个时候我们可以通过yield来返回结果;
语法
生成器函数:是 JS 中的一种特殊函数类型,它通过 function*
语法定义,具有多次返回值的能力,可以暂停和恢复执行。
语法:
function* generatorFunction() {
// 生成器逻辑
}
function*
:生成器函数使用function*
来定义。可以在执行过程中暂停,并且可以多次返回值。- 生成器函数默认在执行时,返回一个生成器对象。
yield
:表示生成器函数在此暂停,并返回一个值。直到外部调用next()
方法。next()
:生成器函数返回的是一个生成器对象,该对象具有next()
方法。调用next()
方法时,生成器从上次暂停的地方继续执行,直到遇到下一个yield
或结束。return()
:可以提前终止生成器并返回一个值。如果在yield
后调用return()
,生成器将不再返回任何值,done
会变为true
。yield*
:是一个委托表达式,用于将生成器的执行委托给另一个生成器。它会一次性执行另一个生成器,返回它的所有值。
示例:基本使用
// 1. 定义一个生成器函数
function* foo() {
console.log('1111')
console.log('2222')
yield
console.log('3333')
console.log('4444')
yield
console.log('5555')
console.log('6666')
}
// 2. 调用生成器函数,返回一个生成器对象
const gen = foo();
// 3. 调用生成器对象的next方法
console.log(gen.next()); // 1111, 2222
console.log(gen.next()); // 3333, 4444
console.log(gen.next()); // 5555, 6666
生成器函数的执行流程:
function 后面会跟上符号
*
代码的执行可以被
yeild
控制生成器函数默认子在执行时,返回一个生成器对象
要想执行函数内部的代码,需要生成器对象调用它的next()方法
当遇到yeild时,就会中断执行
返回值 yield
通过yield返回结果
传递参数 next()
函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
答案是可以的;
我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;
注意:也就是说我们是为本次的函数代码块执行提供了一个值;
传递参数规则:
- 第一个yield之前代码块中的参数
name1
是通过foo('next1')
函数传递参数,并通过foo(name1)
接收参数 - 第二个yield之前代码块中的参数
name2
是在调用第二个next('next2')
时传递参数,并通过const name2 = yield 'aaaa'
中的name2接收参数 - 之后的yield代码块中的参数按照第二个yield的参数传递规则依次传递。
- 注意: 第一次调用
next()
时,不传递参数
提前结束 return()
还有一个可以给生成器函数传递参数的方法是通过return函数:
return传值后这个生成器函数就会立即结束,之后调用next不会继续生成值了;
抛出异常 throw()
除了给生成器函数内部传递参数之外,也可以向生成器函数内部抛出异常:
1、通过 generator.throw(new Error('error message'))
向生成器函数内部抛出异常
2、在生成器函数内部捕获异常
注意: 在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;
应用场景
生成器替代迭代器
我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
▸ 利用生成器对之前的迭代器代码进行重构
1、之前的迭代器
2、利用生成器重构之前的迭代器
▸ 生成器函数,可以生成某个范围的值
yield*
yield*
是一个委托表达式,用于将生成器的执行委托给另一个生成器或可迭代对象。它会一次性执行另一个生成器,返回它的所有值。
语法:
yield* iterable
iterable
:可以是任何具有迭代协议的对象,通常是另一个生成器、数组、字符串、Set、Map 等。
特性:
yield*
是yield
语句的一种语法糖。它会依次迭代这个可迭代对象,每次迭代其中的一个值。yield*
只能存在于生成器函数中。yield*
语法非常适合处理递归生成器或将多个生成器组合成一个生成器。
用法:
▸ 基本用法:委托给另一个生成器
function* inner() {
yield 1;
yield 2;
}
function* outer() {
yield* inner(); // 委托给 inner 生成器
yield 3;
}
const gen = outer();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
▸ 一行代码重构迭代器:委托给数组
▸ 使用 yield* 处理递归
// flatten 是一个递归生成器函数,使用 yield* 将内部的数组元素“平铺”到外部生成器中。
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // 递归地展开数组
} else {
yield item;
}
}
}
const gen = flatten([1, [2, [3, 4], 5], 6]);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: 6, done: false }
console.log(gen.next()); // { value: undefined, done: true }
▸ 重构自定义类的迭代
在之前的 自定义类的迭代 中,我们也可以换成生成器:
注意: 此处*[Symbol.iterator]()
表示生成器函数,yield*
只能存在于生成器函数中。
对生成器的操作
既然生成器是一个迭代器,那么我们可以对其进行如下的操作:
异步处理@
学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。
异步处理方案:
- 方案一:回调嵌套,会造成回调地狱
- 方案二:Promise链式调用
- 方案三:Generator方案
- 方案四:async await方案(终极方案)
案例需求:
我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;
第二次的请求url依赖于第一次的结果;
第三次的请求url依赖于第二次的结果;
依次类推;
▸ 方案一:回调嵌套,会造成回调地狱
▸ 方案二:Promise链式调用
▸ 方案三:Generator方案
▸ 方案四:async await方案(终极方案)
Generator方案
但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?
自动化执行generator函数@
目前我们的写法有两个问题:
第一,我们不能确定到底需要调用几层的Promise关系;
第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?
所以,我们可以封装一个工具函数execGenerator自动执行生成器函数:
async/await@
异步函数 async function
async关键字用于声明一个异步函数:
async是asynchronous单词的缩写,异步、非同步;
sync是synchronous单词的缩写,同步、同时;
写法: async异步函数可以有很多中写法:
核心特性:
异步函数默认情况下和普通函数一样
异步函数返回的是一个Promise
异步函数返回值
异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行。
异步函数有返回值时,和普通函数会有区别:
情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到Promise.resolve中;
情况二:如果我们的异步函数的返回值是Promise,状态由会由Promise决定;
情况三:如果我们的异步函数的返回值是一个对象并且实现了thenable,那么会由对象的then方法来决定;
▸ 示例:返回一个普通的值
返回的普通值会被包裹到 Promise.resolve(value)
中。
▸ 示例:返回一个 Promise
异步函数的状态由新返回的Promise决定。
▸ 示例:返回一个 thenable 对象
then方法中也可以有timeout()延迟方法
异步函数rejected
什么情况下异步函数的结果是rejected?
情况一:在异步函数中返回一个执行reject的Promise
情况二:如果我们在async中抛出了异常,那么程序它并不会像普通函数一样报错,而是会作为Promise的reject来传递;
▸ 情况一:在异步函数中返回一个执行reject的Promise
▸ 情况二:在异步函数中抛出了异常
await关键字
async函数另外一个特殊之处就是可以在它内部使用await关键字,而普通函数中是不可以的。
特点:
通常使用await是后面会跟上一个表达式,这个表达式会返回一个Promise;
await会等到Promise的状态变成fulfilled状态,之后继续执行异步函数;
表达式返回值类型:
如果await后面是一个普通的值,那么会直接返回这个值;
如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;
如果await后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的reject值;
▸ 基本用法:
▸ await处理异步请求
第二次请求会等待第一次请求的结果
▸ 在异步函数中抛出异常的处理方式
1、方式一:在异步函数后的catch
中捕获异常
2、方式二:在异步函数内部,通过try catch
,捕获异常
await和async结合使用:
await后面既可以跟随返回Promise的普通函数,也可以跟随一个异步函数