JavaScript内核与高级编程之:`JavaScript`的`Map`和`Set`:其与传统`Object`和`Array`的性能对比。

各位观众老爷们,大家好!今天咱们来聊聊JavaScript里一对好基友:MapSet。 别看它们名字有点陌生,其实它们在某些场合比老朋友ObjectArray更好使,甚至能让你的代码跑得更快!咱们今天就扒一扒它们的底裤,看看它们到底有啥本事。

开场白:Object和Array的局限性

在JavaScript的世界里,ObjectArray是元老级的存在。 咱们天天用,用得那叫一个溜。 但是,时间长了,你有没有觉得它们有点不够劲儿?

  • Object的Key只能是字符串或Symbol: 想用数字、对象当Key? 没门! Object会默默地把你转换成字符串,然后告诉你:“我只能帮你到这儿了”。
  • Array的indexOf查找效率: 想在数组里找个东西? indexOf跑一遍,效率嘛… 尤其是数组很大的时候,简直慢到怀疑人生。
  • Object遍历顺序不确定: 你想按顺序遍历Object的属性? 呵呵,JavaScript引擎表示: “我心情好就给你按顺序,心情不好就随机”。 (ES2015后对于非数字键的遍历顺序是按照插入顺序,但是依旧不能保证全部情况)
  • Array删除元素产生空洞:delete删除Array里的元素? 会留下一个undefined的空洞,数组的长度不变,遍历的时候还得小心翼翼地避开它。

正是在这些局限性的刺激下,MapSet应运而生,它们就是来解决这些问题的!

第一节:Map——更灵活的键值对存储

Map,顾名思义,地图。 你可以把它想象成一张地图,每个地点(Key)都对应着一个目的地(Value)。 和Object最大的区别在于,Map的Key可以是任何类型,包括数字、字符串、对象、甚至是另一个Map

1. Map的基本用法

// 创建一个Map
const myMap = new Map();

// 设置键值对
myMap.set('name', '张三');
myMap.set(123, '数字');
myMap.set({ a: 1 }, '对象'); // 对象的引用作为key

// 获取值
console.log(myMap.get('name'));   // 输出: 张三
console.log(myMap.get(123));      // 输出: 数字
console.log(myMap.get({ a: 1 }));  // 输出: undefined (因为这是另一个对象)

const obj = { a: 1 };
myMap.set(obj, '对象引用');
console.log(myMap.get(obj));      // 输出: 对象引用 (使用相同的引用)

// 检查是否存在某个键
console.log(myMap.has('name'));   // 输出: true
console.log(myMap.has('age'));    // 输出: false

// 删除键值对
myMap.delete('name');
console.log(myMap.has('name'));   // 输出: false

// 获取Map的大小
console.log(myMap.size);          // 输出: 2 (删除'name'后)

// 清空Map
myMap.clear();
console.log(myMap.size);          // 输出: 0

2. Map的遍历

Map提供了多种遍历方式,让你可以轻松地访问所有的键值对。

const myMap = new Map([
    ['name', '张三'],
    [123, '数字'],
    [{ a: 1 }, '对象']
]);

// 1. 使用 for...of 循环
for (const [key, value] of myMap) {
    console.log(key, value);
}

// 2. 使用 forEach 方法
myMap.forEach((value, key) => {
    console.log(key, value);
});

// 3. 获取所有的键
for (const key of myMap.keys()) {
    console.log(key);
}

// 4. 获取所有的值
for (const value of myMap.values()) {
    console.log(value);
}

// 5. 获取所有的键值对条目
for (const entry of myMap.entries()) {
    console.log(entry[0], entry[1]);
}

3. Map和Object的性能对比

特性 Object Map
Key的类型 字符串或Symbol 任意类型
大小 手动计算 (没有直接的属性) size属性
遍历 for...in (遍历原型链) , Object.keys() for...of, forEach
性能 (添加/删除) O(1) (平均情况,取决于哈希函数) O(1) (平均情况,取决于哈希函数实现)
查找 O(1) (平均情况,取决于哈希函数) O(1) (平均情况,取决于哈希函数实现)
键冲突 可能发生 (需要考虑原型链) 不会发生 (Map内部处理)
插入顺序 ES2015+ 按照插入顺序 (非数字键),之前不保证 保持插入顺序
  • Key的类型: Map完胜。 想用啥当Key就用啥,不用担心类型转换的问题。
  • 大小: Mapsize属性,直接获取大小,方便快捷。 Object需要自己遍历统计,麻烦!
  • 遍历: Map的遍历方式更多,更灵活。 Objectfor...in还会遍历原型链,需要用hasOwnProperty过滤一下,略显繁琐。
  • 性能: 一般情况下,Map在频繁添加和删除键值对的场景下,性能更好。 特别是当Key不是字符串的时候,Map的优势更明显。 Object在Key是字符串,并且只需要简单存储和访问的时候,性能可能略好。
  • 键冲突: Object的键可能会和原型链上的属性冲突,而Map不会。

代码说话:性能测试

// Map 和 Object 的性能对比
const ITERATIONS = 100000;

// Map
console.time('Map Set');
const map = new Map();
for (let i = 0; i < ITERATIONS; i++) {
    map.set(i, i);
}
console.timeEnd('Map Set');

console.time('Map Get');
for (let i = 0; i < ITERATIONS; i++) {
    map.get(i);
}
console.timeEnd('Map Get');

// Object
console.time('Object Set');
const obj = {};
for (let i = 0; i < ITERATIONS; i++) {
    obj[i] = i;
}
console.timeEnd('Object Set');

console.time('Object Get');
for (let i = 0; i < ITERATIONS; i++) {
    obj[i];
}
console.timeEnd('Object Get');

(运行结果会因环境而异,但通常在大量操作时,Map在非字符串Key的情况下会表现出优势)

第二节:Set——集合的艺术

Set,集合。 你可以把它想象成一个不允许重复元素的数组。 Set最大的特点就是元素唯一,它可以用来去重,或者判断某个元素是否存在。

1. Set的基本用法

// 创建一个Set
const mySet = new Set();

// 添加元素
mySet.add(1);
mySet.add(2);
mySet.add(3);
mySet.add(1); // 重复添加,会被忽略

console.log(mySet); // 输出: Set(3) { 1, 2, 3 }

// 检查是否存在某个元素
console.log(mySet.has(2));    // 输出: true
console.log(mySet.has(4));    // 输出: false

// 删除元素
mySet.delete(2);
console.log(mySet.has(2));    // 输出: false

// 获取Set的大小
console.log(mySet.size);      // 输出: 2 (删除2后)

// 清空Set
mySet.clear();
console.log(mySet.size);      // 输出: 0

2. Set的遍历

Set也提供了多种遍历方式,和Map类似。

const mySet = new Set([1, 2, 3, 4, 5]);

// 1. 使用 for...of 循环
for (const item of mySet) {
    console.log(item);
}

// 2. 使用 forEach 方法
mySet.forEach(item => {
    console.log(item);
});

// 3. 获取所有的值 (Set没有键的概念,所以keys()和values()方法返回的是相同的结果)
for (const value of mySet.values()) {
    console.log(value);
}

// 4. 获取所有的键 (Set没有键的概念,所以keys()和values()方法返回的是相同的结果)
for (const key of mySet.keys()) {
    console.log(key);
}

// 5. 获取所有的键值对条目 (Set没有键的概念,所以entries()方法返回的键和值是相同的)
for (const entry of mySet.entries()) {
    console.log(entry[0], entry[1]); // entry[0] 和 entry[1] 相同
}

3. Set和Array的性能对比

特性 Array Set
元素唯一性 需要手动去重 自动去重
查找 indexOf, includes (O(n)) has (O(1) 平均情况)
删除 splice, filter (可能产生空洞) delete (不会产生空洞)
大小 length属性 size属性
性能 查找、删除元素效率较低 (特别是大型数组) 查找、删除元素效率较高 (基于哈希表)
  • 元素唯一性: Set完胜。 Array需要自己写代码去重,麻烦不说,效率还低。
  • 查找: Sethas方法查找效率更高,特别是当数组很大的时候。 ArrayindexOfincludes需要遍历整个数组,效率较低。
  • 删除: Setdelete方法不会产生空洞,Arraydelete会留下undefined,需要小心处理。
  • 性能: 在需要频繁查找和删除元素的场景下,Set的性能更好。

代码说话:去重和查找的性能测试

const ITERATIONS = 100000;
const array = Array.from({ length: ITERATIONS }, () => Math.floor(Math.random() * ITERATIONS / 2)); // 创建一个包含重复元素的数组

// Array 去重
console.time('Array 去重');
const uniqueArray = [...new Set(array)];
console.timeEnd('Array 去重');

// Set 去重
console.time('Set 去重');
const uniqueSet = new Set(array);
console.timeEnd('Set 去重');

const searchValue = Math.floor(Math.random() * ITERATIONS / 2);

// Array 查找
console.time('Array 查找');
array.includes(searchValue);
console.timeEnd('Array 查找');

// Set 查找
console.time('Set 查找');
uniqueSet.has(searchValue);
console.timeEnd('Set 查找');

(运行结果会因环境而异,但通常Set在去重和查找方面的性能优势明显)

第三节:Map和Set的应用场景

说了这么多,MapSet到底能干啥呢?

  • Map的应用场景:

    • 存储配置信息: 可以用对象作为Key,存储不同配置对应的参数。
    • 缓存: 可以用URL作为Key,缓存请求的结果。
    • 统计: 可以用元素作为Key,统计出现的次数。
    • DOM元素和数据的关联: 将DOM元素作为Key,存储相关数据,避免使用data-*属性。
  • Set的应用场景:

    • 数据去重: 简单粗暴,一键去重。
    • 判断元素是否存在: 快速判断某个元素是否已经存在。
    • 跟踪访问过的URL: 防止重复访问相同的URL。
    • 实现数学上的集合运算: 例如并集、交集、差集等。

举个栗子:统计字符出现的次数

const str = 'hello world';

// 使用Object
const charCountObj = {};
for (const char of str) {
    charCountObj[char] = (charCountObj[char] || 0) + 1;
}
console.log(charCountObj);

// 使用Map
const charCountMap = new Map();
for (const char of str) {
    charCountMap.set(char, (charCountMap.get(char) || 0) + 1);
}
console.log(charCountMap);

在这个例子里,MapObject都可以实现统计字符出现次数的功能。 但是,如果字符串包含特殊字符,Map的处理方式更优雅,不会出现属性命名冲突的问题。

总结:选择合适的工具

MapSet是JavaScript里非常有用的数据结构,它们在某些场景下比ObjectArray更高效、更方便。 但是,这并不意味着ObjectArray就没用了。 选择合适的工具,取决于你的具体需求。

  • 如果Key是字符串或Symbol,只需要简单存储和访问,Object可能更合适。
  • 如果Key需要是任意类型,需要频繁添加和删除键值对,Map更合适。
  • 如果需要存储唯一值,需要频繁查找和删除元素,Set更合适。
  • 如果只需要简单存储和访问,并且对元素唯一性没有要求,Array可能更合适。

希望今天的讲座能让你对MapSet有更深入的了解。 记住,没有最好的工具,只有最合适的工具! 下次写代码的时候,不妨考虑一下MapSet,说不定它们能给你带来惊喜! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注