JS随笔:数据结构与集合

22 阅读8分钟

JS随笔:数据结构与集合

本篇是「JS随笔」系列中的数据结构篇,聚焦 JavaScript 的核心数据结构与集合类型,包括 Array、Object、拷贝与引用、原型链与继承、Map/WeakMap、Set/WeakSet、Symbol,以及部分函数式编程与柯里化的实践,并在结尾附带 2025/2026 与集合/迭代相关的语言更新。


原文地址

墨渊书肆/JS随笔:数据结构与集合


数组(Array)

数组是灵活的序列容器:

特性

  • 动态大小:长度可变
  • 类型无关:可存储任意类型
  • 索引:从 0 开始

基础操作

let arr = [];
let numbers = [1, 2, 3, 4, 5];
let element = numbers[0];
numbers[0] = 10;
let length = numbers.length;
numbers.length = 3; // 截断

栈与队列方法

numbers.push(6);
let lastElement = numbers.pop();
let firstElement = numbers.shift();
numbers.unshift(0);

排序、搜索与迭代

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,值可为任意类型。

基本操作

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');

属性特性

Object.defineProperty(person, 'age', {
  value: 25,
  writable: false,
  enumerable: true,
  configurable: false
});

拷贝与引用

浅拷贝

let original = { name: 'Alice', age: 25 };
let shallowCopy1 = { ...original };
let shallowCopy2 = Object.assign({}, original);
let copiedArray = originalArray.slice();

深拷贝

let deepCopy = JSON.parse(JSON.stringify(original)); // 注意:无法处理函数/undefined/循环引用
// lodash.cloneDeep 可用于健壮的深拷贝
手写深拷贝
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

继承模式

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';
组合继承
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; };
原型式与寄生式
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 类继承
class ParentX { constructor(name) { this.name = name; } }
class ChildX extends ParentX {
  constructor(name, age) { super(name); this.age = age; }
}

函数式编程与柯里化(选摘)

高阶函数

function createAdder(x) {
  return function(y) { return x + y; };
}
const addFive = createAdder(5);
console.log(addFive(3)); // 8

原生高阶:mapfilterreduceforEachapplycall

柯里化

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

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

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)

WeakSetWeakMap 类似,只适合存放对象引用,且无法遍历,多用于记录“某对象是否已处理过”等场景。

Symbol 与私有属性

const mySymbol = Symbol('mySymbol');
const obj = { [mySymbol]: 'Only one key' };

与字符串键不同,同一描述符生成的多个 Symbol('desc') 也互不相等, 因此非常适合在库内部定义“不会与用户代码冲突”的元数据键。

数组方法进阶

  • slice vs spliceslice 返回副本;splice 原地修改
  • flat/flatMap:多维数组扁平化与映射扁平化
  • includes/indexOf:包含与索引查询的区别
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(不可扩展可改值)
Object.freeze(obj);
Object.isFrozen(obj);     // true
Object.seal(obj);
Object.isSealed(obj);     // true

冻结/密封对象更多是“意图表达”:强调该结构不应被随意扩展或修改, 在大型团队协作中能有效避免误用;但需要注意这仍然是运行时行为,并不能替代类型系统。

Map vs Object 对比

  • 键类型Map 的键可为任意类型;Object 键被强制为字符串或 Symbol
  • 顺序Map 保留插入顺序;Object 键顺序规则更复杂
  • APIMap 提供 size、迭代器与便捷方法
const m = new Map([[{k:1}, 'v']]);
m.get({k:1}); // undefined(引用不一致)

Set 运算的手写实现

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 概览

  • 类型化数组Int8ArrayUint8ArrayFloat32Array 等,面向二进制数据与性能敏感场景
  • ArrayBuffer 与 DataView:底层缓冲区与视图,适合网络/文件/图像数据处理
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setUint32(0, 0xdeadbeef);
view.getUint32(0); // 3735928559

WeakMap 的私有数据模式

使用 WeakMap 存储“实例 → 私有数据”的映射,是一种兼顾封装性与垃圾回收友好的做法。 相比把所有字段直接挂在实例上,这种模式可以:

  • 将真正的内部状态藏在模块私有作用域中,对外只暴露访问接口
  • 当实例对象被回收时,对应的私有数据也会自动被清理,不需要显式删除键

需要注意的是,WeakMap 无法被遍历,因此更适合作为“附加私有元数据”的容器, 而不是通用缓存或列表存储结构。

const _priv = new WeakMap();
class User {
  constructor(name) {
    _priv.set(this, { name });
  }
  get name() {
    return _priv.get(this).name;
  }
}

Symbol 的实际用途

  • 避免键名冲突:为库或框架内部保留元数据键
  • 自定义迭代:实现 [Symbol.iterator] 以支持 for...of
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
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

  • 在迭代器上构建惰性管道,降低内存峰值
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:在对象被垃圾回收后执行清理回调
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