JS随笔:数据结构与集合
本篇是「JS随笔」系列中的数据结构篇,聚焦 JavaScript 的核心数据结构与集合类型,包括 Array、Object、拷贝与引用、原型链与继承、Map/WeakMap、Set/WeakSet、Symbol,以及部分函数式编程与柯里化的实践,并在结尾附带 2025/2026 与集合/迭代相关的语言更新。
原文地址
数组(Array)
数组是灵活的序列容器:
特性
- 动态大小:长度可变
- 类型无关:可存储任意类型
- 索引:从 0 开始
基础操作
javascript
let arr = [];
let numbers = [1, 2, 3, 4, 5];
let element = numbers[0];
numbers[0] = 10;
let length = numbers.length;
numbers.length = 3; // 截断
栈与队列方法
javascript
numbers.push(6);
let lastElement = numbers.pop();
let firstElement = numbers.shift();
numbers.unshift(0);
排序、搜索与迭代
javascript
numbers.sort(); // 默认按 Unicode 排序
numbers.sort((a, b) => a - b); // 数字升序
numbers.reverse();
let index = numbers.indexOf(3);
let lastIndex = numbers.lastIndexOf(3);
numbers.forEach((item) => console.log(item));
let squares = numbers.map((item) => item * item);
let evenNumbers = numbers.filter((item) => item % 2 === 0);
let sum = numbers.reduce((acc, cur) => acc + cur);
其他常用方法
slice():返回片段副本splice():增删改原数组includes():包含判断find()/some()/every()
在数组相关 API 中,最容易混淆的是"是否修改原数组"和"是否返回新数组":
- 修改原数组:
push/pop/shift/unshift/splice/sort/reverse - 返回新数组:
slice/map/filter/concat等
在引入状态管理或不可变数据结构时,强烈建议统一使用"返回新数组"的方式进行更新, 例如 const next = prev.map(...),避免难以追踪的隐式共享引用。
对象(Object)
对象是键值对集合,键为字符串或 Symbol,值可为任意类型。
基本操作
javascript
let person = { name: 'Tom', age: 30 };
let person2 = new Object();
let person3 = Object.create({ name: 'Tom', age: 30 });
let name = person.name;
let age = person['age'];
person.email = 'Tom@example.com';
delete person.email;
for (let key in person) {
console.log(key + ': ' + person[key]);
}
if ('name' in person) { /* ... */ }
let values = Object.values(person);
let keys = Object.keys(person);
let desc = Object.getOwnPropertyDescriptor(person, 'name');
属性特性
javascript
Object.defineProperty(person, 'age', {
value: 25,
writable: false,
enumerable: true,
configurable: false
});
拷贝与引用
浅拷贝
javascript
let original = { name: 'Alice', age: 25 };
let shallowCopy1 = { ...original };
let shallowCopy2 = Object.assign({}, original);
let copiedArray = originalArray.slice();
深拷贝
javascript
let deepCopy = JSON.parse(JSON.stringify(original)); // 注意:无法处理函数/undefined/循环引用
// lodash.cloneDeep 可用于健壮的深拷贝
手写深拷贝
javascript
function deepCopy(obj) {
if (obj === null || typeof obj !== 'object') return obj;
let temp = new obj.constructor();
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
temp[key] = deepCopy(obj[key]);
}
}
return temp;
}
注意事项
- 复杂类型(函数、日期、正则、错误等)需专门处理
- 循环引用需用弱引用或专用算法
- 深拷贝在大对象上可能有性能问题
在实战中通常建议:
- 默认使用浅拷贝配合"不可变更新"模式,例如对象使用展开运算符、数组使用
map/filter - 深拷贝仅用于初始化或边界层(如数据进出接口),并谨慎评估性能与可维护性
- 对于存在图结构或循环引用的复杂对象,更适合使用专门的持久化结构或定制序列化方案
原型链与继承
原型链
- [[Prototype]]:每个对象都有原型引用
- 属性查找:沿原型链向上查找
- 终点 :
Object.prototype
继承模式
javascript
function Parent() { this.property = 'parent'; }
function Child() {
Parent.call(this);
this.childProperty = 'child';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const parent = { property: 'parent' };
const childObj = Object.create(parent);
childObj.childProperty = 'child';
组合继承
javascript
function Parent2(name) { this.name = name; }
Parent2.prototype.getName = function() { return this.name; };
function Child2(name, age) {
Parent2.call(this, name);
this.age = age;
}
Child2.prototype = Object.create(Parent2.prototype);
Child2.prototype.constructor = Child2;
Child2.prototype.getAge = function() { return this.age; };
原型式与寄生式
javascript
function createObject(proto) {
function F() {}
F.prototype = proto;
return new F();
}
function createObject2(proto) {
const result = Object.create(proto);
result.someNewProperty = 'new property';
return result;
}
ES6 类继承
javascript
class ParentX { constructor(name) { this.name = name; } }
class ChildX extends ParentX {
constructor(name, age) { super(name); this.age = age; }
}
函数式编程与柯里化(选摘)
高阶函数
javascript
function createAdder(x) {
return function(y) { return x + y; };
}
const addFive = createAdder(5);
console.log(addFive(3)); // 8
原生高阶:map、filter、reduce、forEach、apply、call
柯里化
javascript
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
const curriedAdd = curry(function(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
});
Map 与 WeakMap
javascript
const map = new Map();
map.set(key, value);
map.get(key);
map.has(key);
map.delete(key);
map.forEach((value, key) => { /* ... */ });
WeakMap:键必须是对象;弱引用;不可遍历;无 size。
在设计 API 时,可以根据访问模式选择合适的结构:
- 若键是字符串/数字 ID,且需要序列化/JSON 交互,优先使用普通对象或
Record - 若键是对象实例(如 DOM 节点、模型对象),并且希望不阻止其被回收,则可使用
WeakMap存储关联数据
需要注意,WeakMap 无法被遍历,因此不适合用于需要"枚举所有项"的缓存,只适合作为"附加元数据"的载体。
Set 与 WeakSet
javascript
const mySet = new Set();
mySet.add(1);
mySet.add('text');
mySet.add({ name: 'Tom' });
mySet.has(1);
mySet.size;
mySet.delete('text');
mySet.forEach((value) => console.log(value));
const uniqueNumbers = [...new Set([1,2,2,3,4,4,5])];
WeakSet:仅存对象引用;弱引用;不可遍历。
相比数组,Set 更适合表示"去重集合"或"是否存在"的语义:
- 数组去重可以用
new Set(array)实现 - 检查存在性时,
set.has的语义更直接,且平均复杂度为 O(1)
WeakSet 与 WeakMap 类似,只适合存放对象引用,且无法遍历,多用于记录"某对象是否已处理过"等场景。
Symbol 与私有属性
javascript
const mySymbol = Symbol('mySymbol');
const obj = { [mySymbol]: 'Only one key' };
与字符串键不同,同一描述符生成的多个 Symbol('desc') 也互不相等, 因此非常适合在库内部定义"不会与用户代码冲突"的元数据键。
数组方法进阶
- slice vs splice :
slice返回副本;splice原地修改 - flat/flatMap:多维数组扁平化与映射扁平化
- includes/indexOf:包含与索引查询的区别
javascript
const arr = [1,2,3,4];
arr.slice(1,3); // [2,3]
arr.splice(1,2,'a'); // arr -> [1,'a',4]
[1,[2,[3]]].flat(2); // [1,2,3]
[1,2,3].flatMap(x => [x,x]); // [1,1,2,2,3,3]
在重构老代码时,可以优先将"循环 + 手动 push"改为 map/filter/reduce 管道, 既提升可读性,也减少状态变量的散落与副作用。
对象属性与枚举
- 可枚举性与自有属性 :
Object.keys/values/entries只枚举自有可枚举属性 - 获取所有键 :
Reflect.ownKeys包含字符串键与Symbol键 - 冻结与密封 :
Object.freeze(不可扩展不可修改),Object.seal(不可扩展可改值)
javascript
Object.freeze(obj);
Object.isFrozen(obj); // true
Object.seal(obj);
Object.isSealed(obj); // true
冻结/密封对象更多是"意图表达":强调该结构不应被随意扩展或修改, 在大型团队协作中能有效避免误用;但需要注意这仍然是运行时行为,并不能替代类型系统。
Map vs Object 对比
- 键类型 :
Map的键可为任意类型;Object键被强制为字符串或Symbol - 顺序 :
Map保留插入顺序;Object键顺序规则更复杂 - API :
Map提供size、迭代器与便捷方法
javascript
const m = new Map([[{k:1}, 'v']]);
m.get({k:1}); // undefined(引用不一致)
Set 运算的手写实现
javascript
function union(a, b) {
return new Set([...a, ...b]);
}
function intersection(a, b) {
const res = new Set();
for (const v of a) if (b.has(v)) res.add(v);
return res;
}
function difference(a, b) {
const res = new Set();
for (const v of a) if (!b.has(v)) res.add(v);
return res;
}
function isSubsetOf(a, b) {
for (const v of a) if (!b.has(v)) return false;
return true;
}
TypedArray 概览
- 类型化数组 :
Int8Array、Uint8Array、Float32Array等,面向二进制数据与性能敏感场景 - ArrayBuffer 与 DataView:底层缓冲区与视图,适合网络/文件/图像数据处理
javascript
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setUint32(0, 0xdeadbeef);
view.getUint32(0); // 3735928559
WeakMap 的私有数据模式
使用 WeakMap 存储"实例 → 私有数据"的映射,是一种兼顾封装性与垃圾回收友好的做法。 相比把所有字段直接挂在实例上,这种模式可以:
- 将真正的内部状态藏在模块私有作用域中,对外只暴露访问接口
- 当实例对象被回收时,对应的私有数据也会自动被清理,不需要显式删除键
需要注意的是,WeakMap 无法被遍历,因此更适合作为"附加私有元数据"的容器, 而不是通用缓存或列表存储结构。
javascript
const _priv = new WeakMap();
class User {
constructor(name) {
_priv.set(this, { name });
}
get name() {
return _priv.get(this).name;
}
}
Symbol 的实际用途
- 避免键名冲突:为库或框架内部保留元数据键
- 自定义迭代 :实现
[Symbol.iterator]以支持for...of
javascript
const iterable = {
[Symbol.iterator]() {
let i = 0;
return { next() { return i < 3 ? { value: i++, done: false } : { done: true }; } };
}
};
[...iterable]; // [0,1,2]
小结
- 熟练掌握数组/对象/集合的语义与差异
- 利用手写集合运算与 TypedArray 应对性能与算法需求
- 通过 WeakMap/Symbol 提升封装与可维护性
ES2025 集合与迭代增强
Set 扩展
- 集合运算:
union/intersection/difference/symmetricDifference - 关系判断:
isSubsetOf/isSupersetOf/isDisjointFrom
javascript
const A = new Set([1,2,3]);
const B = new Set([3,4,5]);
A.union(B); // Set {1,2,3,4,5}
A.intersection(B); // Set {3}
A.difference(B); // Set {1,2}
A.symmetricDifference(B); // Set {1,2,4,5}
A.isSubsetOf(new Set([1,2,3,4])); // true
Iterator Helpers
- 在迭代器上构建惰性管道,降低内存峰值
javascript
const it = new Set([1,2,3,4,5]).values();
const out = it.filter(x => x % 2).map(x => x * 2).take(2);
[...out]; // [2,6]
WeakRef 与 FinalizationRegistry(概览)
- WeakRef:创建对象的弱引用,避免阻止垃圾回收
- FinalizationRegistry:在对象被垃圾回收后执行清理回调
javascript
let obj = { cache: new Array(1000).fill(0) };
const ref = new WeakRef(obj);
const registry = new FinalizationRegistry(token => {
// 释放与 token 相关的资源
});
registry.register(obj, 'cache-1');
obj = null; // 允许被回收
实战建议
- 使用 Set 扩展完成集合数学运算,替换手写版本
- 在大集合上优先使用迭代器助手避免中间数组
- 对于缓存与资源清理场景,谨慎引入 WeakRef/FinalizationRegistry