前言
在JavaScript中,数据结构策略是指根据问题需求选择合适的数据结构(如数组、对象、Set、Map等)来高效地存储和操作数据。不同的数据结构有不同的特点和适用场景。
大多数情况下我都是依赖数组、对象和基本循环来解决项目编码工作。
基础数据结构选择策略
1、数组(Array)
适用于有序集合,特别是需要按索引访问元素的情况。
提供多种内置方法(如push, pop, shift, unshift, splice, slice, map, filter, reduce等)便于操作。
当需要维护元素的顺序时,数组是首选。
注意:在数组开头插入或删除元素(shift和unshift)的时间复杂度为O(n),因为需要移动所有后续元素。
// 用索引快速操作尾部
const stack = [];
stack.push(1); // O(1)
stack.pop(); // O(1)
// 避免在数组开头操作
const queue = [];
// 低效:queue.shift() → 改用链表或循环数组
2、对象(Object)
适用于存储键值对,其中键是字符串或Symbol。
可以快速访问、插入和删除属性(平均O(1))。
当需要表示实体(如用户信息)或需要根据特定键快速查找值时使用。
注意:对象键是无序的(尽管现代JavaScript引擎可能按照创建顺序维护,但不要依赖顺序)。
常见的高级数据结构策略
在构建大型系统或在性能敏感环境中工作时,这些基本的数据结构就有点不够用了。
于是几个常见的高级数据结构策略也用的越来越多了,比如经常需要不能重复的类数组类型;属性有序的对象等
1、Set
存储唯一值(无重复值)的集合。
维护插入顺序,可以快速检查值是否存在(has()
方法),添加和删除值。
适用于去重或需要快速检查成员存在的场景。
const unique = new Set([1, 2, 2, 3]); // {1, 2, 3}
unique.has(2); // true (比数组的 includes 快)
2、Map
类似于对象,但键可以是任意类型(包括对象、函数等)
维护键值对的插入顺序,迭代时按插入顺序返回。
提供了更便捷的方法(如set, get, has, delete)和属性(size)
当键不是字符串或需要维护插入顺序时,Map比对象更合适。
const map = new Map();
map.set({ id: 1 }, "User1"); // 对象作为键
map.get(key); // O(1) 访问
3、WeakMap / WeakSet
But,因为Map保留对键的强引用,即使元素从DOM中移除,也会阻止它们被垃圾回收。
在一些向DOM节点附加临时元数据:工具提示、事件监听器的情况下,尤其是在动态UI繁重的前端框架中(如React、Angular甚至一些动态生成临时dom的原生JS),如果使用标准Map将元数据与DOM元素关联,可能会导致内存泄漏。
顾名思义,与Map和Set类似,但键(WeakMap)或值(WeakSet)是弱引用,不会阻止垃圾回收。
适用于需要临时存储或避免内存泄漏的场景。
问题代码:
const metadata = new Map();
const button = document.getElementById('submit');
metadata.set(button, { clicked: true });
button.remove(); // 仍然保留在内存中!
改用WeakMap
,它对键持有弱引用——允许垃圾回收清理未引用的对象:
const weakMetadata = new WeakMap();
weakMetadata.set(button, { clicked: true });
button.remove(); // 弱引用,可以被回收
注意:不可迭代,没有`size`属性。
由此……引发思考🤔
还有哪些未被充分利用的JavaScript数据结构呢?
不常见的高级数据结构策略
1、Object.freeze()
and
Object.seal()
在协作项目或大型应用中,共享配置对象可能会被意外修改,引入难以调试的副作用。当处理环境设置、Redux状态或模块之间共享的常量时,这尤其危险哦。
Object.freeze()
实现完全不可变性
Object.freeze()
方法可以冻结一个对象。被冻结的对象不能被扩展,不能添加新属性,不能删除已有属性,不能修改已有属性的可枚举性、可配置性或 writable 特性,也不能修改已有属性的值。此外,冻结一个对象也会递归地冻结该对象的所有子对象。
Object.seal()
实现部分保护
Object.seal()
方法可以密封一个对象。密封对象会阻止添加新属性,并将所有现有属性标记为不可配置(non - configurable)。属性的值仍然可以修改,前提是属性本身是可写(writable)的。
// 可变共享对象 — 危险
const config = { debug: true };
config.debug = false; // 允许这样做,可能导致隐藏的bug
// 完全不可变
const frozenConfig = Object.freeze({ debug: true });
frozenConfig.debug = false; // 失败(或在严格模式下抛出错误)
// 密封但值可变
const sealedConfig = Object.seal({ debug: true });
sealedConfig.newProp = 'x'; // 失败
sealedConfig.debug = false; // 有效
总结来说,Object.freeze()
提供了更严格的不可变性,对象的所有方面都被锁定,而 Object.seal()
主要防止对象结构的改变(添加或删除属性),同时允许对可写属性的值进行修改。
// 创建一个普通对象
let car = {
brand: 'Toyota',
model: 'Corolla',
year: 2020
};
// 密封对象
Object.seal(car);
// 尝试添加新属性
car.color ='red';
console.log('color' in car); // 输出 false,添加失败
// 尝试删除属性
delete car.brand;
console.log('brand' in car); // 输出 true,删除失败
// 尝试修改属性值(如果属性是可写的)
car.year = 2021;
console.log(car.year); // 输出 2021,修改成功
// 尝试将属性标记为不可配置(已经是不可配置的,所以操作无效)
Object.defineProperty(car,'model', { configurable: false });
// 这不会抛出错误,但也不会改变任何东西,因为已经密封,属性是不可配置的
适用于:
• 防止Redux/全局状态中的配置被意外更改
• 在前端和后端之间共享环境常量
• 保护库选项不被篡改
有助于维护代码安全,强制执行数据契约,并消除由意外修改引起的bug。
2、Array.from()
Array.from()
方法从一个类似数组或可迭代对象创建一个新的数组实例。
在 JavaScript 中,NodeList
是 DOM 操作返回的类似数组的对象,但它缺少许多数组的方法。Array.from()
结合映射函数可以有效地将 NodeList
转换为真正的数组,并对其中的每个元素进行操作,从而实现更简洁、干净的代码。
当你需要对 NodeList
中的每个元素进行操作时,可以在 Array.from()
中传入一个映射函数。映射函数会对 NodeList
中的每个元素执行,并将返回值组成新的数组。
const divs = document.querySelectorAll('div');
const texts = [...divs].map(div => div.textContent); // 效率低下
const texts = Array.from(divs, div => div.textContent); // 高效
适用于:
• 转换和变换NodeList
、arguments
、Set
或Map
值
• 在富文本编辑器或可视化工具中解析或操作内容
通过减少中间数组创建和方法链,提高性能和代码清晰度。
3、Object.groupBy()
无需Reduce地狱的原生分组,对数组数据进行分组是一项常见任务——无论是用户角色、销售报告还是API响应。大多数开发者使用冗长的reduce()
逻辑或依赖Lodash等实用库。
// 老式reduce方式
const grouped = users.reduce((acc, user) => {
const dept = user.department;
(acc[dept] ||= []).push(user);
return acc;
}, {});
//在Babel或带有polyfill的Node等现代环境中可用
const grouped = Object.groupBy(users, user => user.department);
适用于:
• 按状态、类别、角色对API响应项进行分组
• 创建仪表板、摘要报告或分析图表
降低复杂性,提高可读性,并减少对外部库的依赖。
总结
JavaScript 的内置数据结构,其强大程度远超多数开发者的认知。
当你深入探索并掌握 WeakMap、Set、Object.freeze () 的应用时机与方式,甚至将目光投向 Object.groupBy () 这类崭露头角的新特性时,就能以更优雅的方式攻克复杂难题,同时避开那些在大型应用开发过程中极易遭遇的陷阱。如此一来,你在 JavaScript 的编程之路上,将能更加从容地应对各种挑战,构建出更为健壮、高效的应用程序。