前言
目前在工作中会大量使用 cursor 作为主力编辑器,发现大模型很多边际条件和性能优化处理的都很优雅,用到了很多之前没有去用过的 ES6 方法;这里记录下跟 cursor 都学会了哪些更优雅的实现。
Set
Set
是一个类数组的数据结构,用于存储唯一值,Set
对象允许你存储任何类型的值,包括对象和原始值。
用法: const set = new Set([1, 2, 3]);
javascript
const set = new Set([1, 2, 3]);
// 操作数据方法
set.add(4);
set.delete(1);
set.has(4);
set.clear();
//遍历方法 Set的遍历顺序就是插入顺序
// keys(),values(),entries() 方法返回的都是遍历器对象 , Set 结构没有键名,只有键值所以valus 和 keys方法返回相同。
set.keys();
set.values();
set.entries();
set.forEach((value, key) => console.log(key + " : " + value));
// 扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构;所以通过遍历器数组方法也可以同样作用于set数据。
let arr = [...set];
add 和 delete 方法
向 Set 加入值的时候,不会发生类型转换,所以 5 和"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做"Same-value-zero equality",它类似于精确相等运算符(===)严格相等比较的是引用(即内存地址)。 加入两个对象 set 会认为不相等
javascript
let set = new Set();
set.add({});
set.size; // 1
set.add({});
set.size; // 2
因为 Set
的这种引用存储特性(通过内存地址而非内容识别对象),它特别适合维护任务函数队列: 当某个函数执行后,可以直接用 delete()
方法精准删除队列中的该函数,无需担心因内容相同而误删其他函数。
javascript
// 创建任务队列
const taskQueue = new Set();
// 定义任务函数(必须是具名函数或变量引用)
const task1 = () => console.log("执行任务1");
const task2 = () => console.log("执行任务2");
const task3 = () => console.log("执行任务3");
// 添加任务到队列
taskQueue.add(task1);
taskQueue.add(task2);
taskQueue.add(task3);
// 执行并删除任务的函数
const runAndRemove = (task) => {
if (taskQueue.has(task)) {
task(); // 执行任务
taskQueue.delete(task); //删除当前任务
}
};
// 执行任务1并删除
runAndRemove(task1);
console.log("当前队列长度:", taskQueue.size); // 2
console.log("剩余任务:", [...taskQueue]); // [task2, task3]
// 尝试删除新创建的相同内容函数
const fakeTask = () => console.log("执行任务2");
taskQueue.delete(fakeTask); // 不会删除真正的 task2
console.log("安全防护后长度:", taskQueue.size); // 2(未变化)
Set 与 Object 在管理任务队列上的对比 Set 的优势
- 引用精确性
- 内存自动管理
- 操作简洁性 操作复杂度 O(1) vs Object 的 O(n)
- 遍历效率更高 缺点
- 元数据存储限制
- 兼容性
所以简单场景使用Set
,复杂场景可以Set
配合Map
实现。
Map
Map 和 WeakMap 之前在 React 和 Lit 的源码中就看到过大量使用,项目中可以用起来更优雅的管理数据。
Map 类似对象,Map 的键(key)可以为任何值(包括函数、对象或任何原始值); 序列化和解析:没有对序列化或解析的原生支持。 (但你可以通过使用 JSON.stringify() 及其 replacer 参数和 JSON.parse() 及其 reviver 参数来为 Map 构建自己的序列化和解析支持。参见 Stack Overflow 问题 How do you JSON.stringify an ES6 Map?) 前往 MDN 查看。
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作 Map 构造函数的参数。这就是说,Set 和 Map 都可以用来生成新的 Map。 通过下面的例子可以看出数组的第三项并不会加入Map
,set
同样可以作为 Map 的键。 Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
javascript
// Map(2) {'name' => '张三', 'title' => 'Author'}
const map = new Map([
["name", "张三"],
["title", "Author"],
]);
// Map(2) {'name' => '张三', 'title' => 'Author'}
const map1 = new Map([
["name", "张三", "2333"],
["title", "Author"],
]);
//Map(3) {'name' => '张三', 'title' => 'Author', Set(2) => undefined}
const set = new Set([11, 22]);
map1.set(set);
基础使用方法
javascript
//根据内存地址查找
//Map.prototype.set(key, value)
let map = new Map().set(1, "a").set(2, "b");
map.set("foo", true);
//Map.prototype.get(key)
m.get("foo");
//Map.prototype.has(key)
m.has("foo");
//Map.prototype.delete(key)
m.has("foo");
//Map.prototype.clear()
map.clear();
//size 返回数量
map.size;
遍历方法
Map 的遍历顺序就是插入顺序。
javascript
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
//Map 结构转为数组结构
[...map.keys()]
//['F', 'T']
[...map.values()]
// ['no', 'yes']
[...map.entries()]
// [['F', 'no'] , ['T', 'yes']]
[...map]
// [['F', 'no'] , ['T', 'yes']]
// forEach
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
Map、Object、JSON
Map 转为对象 所有 Map 的键都是字符串,它可以无损地转为对象 ,有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
javascript
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k, v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map().set("yes", true).set("no", false);
strMapToObj(myMap);
对象转为 Map
javascript
let obj = { a: 1, b: 2 };
let map = new Map(Object.entries(obj));
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({ yes: true, no: false });
Map 转为 JSON
javascript
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set("yes", true).set("no", false);
strMapToJson(myMap);
Map 实际应用
通常在开发前端用户监控SDK中很适合使用 Map 作为数据结构,因为存在复杂数据需要记录,并且严格维护插入顺序; 或者也可以用来记录用户操作链路(独立上报和整体上报)作为整体上报部分。
javascript
class ErrorTracker {
constructor() {
// 使用 Map 存储错误信息,key 为错误实例,value 为附加的上下文信息
this.errorContexts = new Map();
// 初始化错误监控
window.addEventListener('error', this.captureError.bind(this));
}
// 捕获错误并添加上下文
captureError(event) {
const error = event.error || event;
const context = {
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
stack: error.stack || 'No stack trace'
};
// 将错误对象作为键存储上下文(避免字符串序列化问题)
this.errorContexts.set(error, context);
// 实际SDK中这里会上报到服务器
console.error('Captured error:', error, 'Context:', context);
}
// 添加自定义上下文到已有错误
enrichError(error, extraData) {
if (this.errorContexts.has(error)) {
const context = this.errorContexts.get(error);
this.errorContexts.set(error, { ...context, ...extraData });
}
}
// 获取所有错误报告(按发生顺序)
getErrorReports() {
return Array.from(this.errorContexts.entries()).map(([error, context]) => ({
name: error.name,
message: error.message,
...context
}));
}
}
// 使用示例
const monitor = new ErrorTracker();
// 模拟错误
try {
throw new Error('Demo error');
} catch (err) {
// 捕获后添加业务上下文
monitor.enrichError(err, {
userId: 'u123',
component: 'CheckoutPage'
});
}
// 获取按时间排序的错误报告
console.log(monitor.getErrorReports());
高频增删键值对的情况下适合比 Object 的增删性能更好
WeakMap
了解WeakMap看到手写cloneDeep时候看到的。
WeakMap结构与Map结构类似,也是用于生成键值对的集合。 阮一峰ES6入门 WeakMap与Map的区别有两点: 首先,WeakMap只接受对象(null除外)和 Symbol 值作为键名,不接受其他类型的值作为键名。 其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。
这段代码就很好利用WeakMap
的特性。
javascript
function cloneDeep(source, hash = new WeakMap()) {
if (!isObject(source)) return source
if (hash.has(source)) return hash.get(source)
var target = Array.isArray(source) ? [] : {}
hash.set(source, target)
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep(source[key], hash)
} else {
target[key] = source[key]
}
}
}
return target
}