JavaScript数据结构实战指南:从业务场景到性能优化

📚 目录

基础篇

  • 前言
  • [一、对象 (Object) - 业务中的万能容器](#一、对象 (Object) - 业务中的万能容器)
    • [1.1 对象是什么?](#1.1 对象是什么?)
    • [1.2 业务场景:用户信息管理](#1.2 业务场景:用户信息管理)
  • [二、函数 (Function) - 可执行的对象](#二、函数 (Function) - 可执行的对象)
    • [2.1 函数的双重身份](#2.1 函数的双重身份)
    • [2.2 实际应用场景](#2.2 实际应用场景)

核心数据结构篇

  • [三、数组 (Array) - 有序业务数据的首选](#三、数组 (Array) - 有序业务数据的首选)
    • [3.1 业务场景:消息列表、商品列表](#3.1 业务场景:消息列表、商品列表)
    • [3.2 数组和对象的核心差异](#3.2 数组和对象的核心差异)
      • [3.2.1 设计目的不同](#3.2.1 设计目的不同)
      • [3.2.2 数组的专属超能力](#3.2.2 数组的专属超能力)
      • [3.2.3 性能优化的底层原理](#3.2.3 性能优化的底层原理)
      • [3.2.4 业务场景对比](#3.2.4 业务场景对比)
    • [3.3 总结](#3.3 总结)
  • [四、类数组 (Array-like) - DOM操作的日常](#四、类数组 (Array-like) - DOM操作的日常)
    • [4.1 业务场景:DOM元素批量处理](#4.1 业务场景:DOM元素批量处理)

遍历与迭代篇

  • [五、可迭代对象 & for...of - 业务中的遍历艺术](#五、可迭代对象 & for…of - 业务中的遍历艺术)
    • [5.1 什么是可迭代](#5.1 什么是可迭代)
    • [5.2 为什么有可迭代和不可迭代的区别](#5.2 为什么有可迭代和不可迭代的区别)
    • [5.3 业务场景:统一处理各种数据源](#5.3 业务场景:统一处理各种数据源)
  • [六、迭代器 (Iterator) - 统一遍历的秘密武器](#六、迭代器 (Iterator) - 统一遍历的秘密武器)
    • [6.1 迭代 vs 遍历:细微但重要的区别](#6.1 迭代 vs 遍历:细微但重要的区别)
    • [6.2 什么是迭代器?"智能书签"的比喻](#6.2 什么是迭代器?"智能书签"的比喻)
    • [6.3 迭代器的核心:next() 方法](#6.3 迭代器的核心:next() 方法)
    • [6.4 业务中的迭代器](#6.4 业务中的迭代器)
    • [6.5 哪些东西有迭代器?](#6.5 哪些东西有迭代器?)
    • [6.6 自己创建迭代器:让自定义对象可遍历](#6.6 自己创建迭代器:让自定义对象可遍历)
    • [6.7 迭代器的业务价值](#6.7 迭代器的业务价值)
    • [6.8 总结](#6.8 总结)
  • [七、普通对象的遍历 - 业务中的各种姿势](#七、普通对象的遍历 - 业务中的各种姿势)
    • [7.1 为什么对象不可迭代?](#7.1 为什么对象不可迭代?)
    • [7.2 五种遍历方式及业务场景](#7.2 五种遍历方式及业务场景)
      • [7.2.1 方式1:for...in - 遍历可枚举属性](#7.2.1 方式1:for…in - 遍历可枚举属性)
      • [7.2.2 方式2:Object.keys() - 只遍历自身属性](#7.2.2 方式2:Object.keys() - 只遍历自身属性)
      • [7.2.3 方式3:Object.values() - 直接获取值](#7.2.3 方式3:Object.values() - 直接获取值)
      • [7.2.4 方式4:Object.entries() - 同时获取键值](#7.2.4 方式4:Object.entries() - 同时获取键值)
      • [7.2.5 方式5:Object.getOwnPropertyNames() - 包含不可枚举属性](#7.2.5 方式5:Object.getOwnPropertyNames() - 包含不可枚举属性)

现代数据结构篇

  • [八、Set - 高效的去重与集合运算](#八、Set - 高效的去重与集合运算)
    • [8.1 业务场景1:数据去重](#8.1 业务场景1:数据去重)
    • [8.2 业务场景2:权限与状态管理](#8.2 业务场景2:权限与状态管理)
    • [8.3 业务场景3:集合运算](#8.3 业务场景3:集合运算)
    • [8.4 性能对比:Set vs Array](#8.4 性能对比:Set vs Array)
    • [8.5 使用建议](#8.5 使用建议)
  • [九、Map - 现代业务数据管理](#九、Map - 现代业务数据管理)
    • [9.1 业务场景1:复杂键名的数据存储](#9.1 业务场景1:复杂键名的数据存储)
    • [9.2 业务场景2:需要顺序保证的键值对](#9.2 业务场景2:需要顺序保证的键值对)
  • [十、Set vs Map 的直观对比](#十、Set vs Map 的直观对比)
    • [10.1 Set:值的集合(关注值本身)](#10.1 Set:值的集合(关注值本身))
    • [10.2 Map:键值对的集合(关注键值映射)](#10.2 Map:键值对的集合(关注键值映射))
    • [10.3 业务中的选择标准](#10.3 业务中的选择标准)
    • [10.4 性能方面:两者都是 O(1)](#10.4 性能方面:两者都是 O(1))
    • [10.5 总结](#10.5 总结)

性能优化篇

  • [十一、O(1) vs O(n) - 业务性能的生死线](#十一、O(1) vs O(n) - 业务性能的生死线)
    • [11.1 通俗理解:找人的不同方式](#11.1 通俗理解:找人的不同方式)
    • [11.2 业务性能影响:数据量越大,差距越恐怖](#11.2 业务性能影响:数据量越大,差距越恐怖)
    • [11.3 业务实战:优化前后对比](#11.3 业务实战:优化前后对比)

综合实战篇

  • [十二、综合实战:IntersectionObserver 业务优化](#十二、综合实战:IntersectionObserver 业务优化)
    • [12.1 业务背景](#12.1 业务背景)
    • [12.2 优化前的问题](#12.2 优化前的问题)
    • [12.3 优化后的方案](#12.3 优化后的方案)
    • [12.4 业务收益](#12.4 业务收益)
    • [12.5 总结:业务选择指南](#12.5 总结:业务选择指南)
    • [12.6 快速决策指南](#12.6 快速决策指南)

前言

在日常前端开发中,我们每天都在与各种数据结构打交道。但你是否曾遇到过这样的困惑:

  • 为什么有时候代码"能跑",但性能很差?
  • 为什么DOM操作经常遇到奇怪的报错?
  • 如何从"实现功能"进阶到"写出优雅高效的代码"?

这份文档源于我在实际项目中的深度思考和踩坑经验,聚焦于最常用数据结构在业务中的实战应用,特别是如何通过合理的数据结构选择来提升性能。

读完你会发现,很多"高级"概念其实就隐藏在日常业务中:

  • 类数组 就在document.querySelectorAll的返回值里
  • O(1)优化就在一个简单的Map查找中
  • 空间换时间就在建立索引的策略里

通过真实的业务场景和性能对比,我希望帮你:

  • 🎯 理解本质:不再死记API,而是理解为什么这样设计
  • 🚀 提升性能:掌握数据结构选择的性能影响
  • 💡 避免陷阱:识别常见但容易忽略的问题
  • 🔧 实战应用:每个知识点都配了真实的业务代码

无论你是想夯实基础的中级开发者,还是希望优化代码性能的资深工程师,这份指南都能给你带来实用的价值。


一、对象 (Object) - 业务中的万能容器

1.1 对象是什么?

对象 = 属性的集合

对象就像一个"文件夹",里面可以存放多个"文件"(属性),每个文件都有名字(键)和内容(值)。

javascript 复制代码
// 创建一个对象
let person = {
  name: '张三', // 键: 值
  phone: '13800138000',
  age: 25,
  address: {
    // 值也可以是对象(嵌套)
    city: '北京',
    district: '朝阳区',
  },
};

// 访问属性
console.log(person.name); // "张三"
console.log(person.address.city); // "北京"

1.2 为什么需要对象?

原因 1:数据关联
javascript 复制代码
// ❌ 没有对象:数据分散,关联性弱
let userName = '张三';
let userAge = 25;

// ✅ 有对象:数据集中,关联性强
let user = {
  name: '张三',
  age: 25,
};
原因 2:语义清晰
javascript 复制代码
// ❌ 难以理解
function createUser(p1, p2, p3, p4) {
  // p1 是什么?p2 是什么?
}

// ✅ 一目了然
function createUser(userInfo) {
  console.log(userInfo.name); // 清晰!
  console.log(userInfo.age); // 清晰!
}
原因 3:灵活扩展
javascript 复制代码
let user = {
  name: '张三',
  age: 25,
};

// 轻松添加新属性
user.email = 'zhangsan@example.com';
user.hobbies = ['篮球', '游泳'];

1.3 对象的本质特征

  1. 无序性:属性没有固定顺序
  2. 键值对结构:每个属性由"键"和"值"组成
  3. 键必须是字符串或 Symbol(会自动转换)
  4. 值可以是任何类型(包括对象、数组、函数)
javascript 复制代码
let obj = {
  name: '张三', // 字符串键(引号可省略)
  123: '数字键', // 数字会转成字符串 "123"
  [Symbol('id')]: 1, // Symbol 键
};

console.log(obj['123']); // "数字键"

二、函数 (Function) - 可执行的对象

2.1 函数是什么?

函数 = 可执行的代码块 + 对象

javascript 复制代码
// 定义函数
function greet(name) {
  return `Hello, ${name}!`;
}

// 调用函数
console.log(greet('张三')); // "Hello, 张三!"

// 函数也是对象!
console.log(typeof greet); // "function"(特殊的对象类型)
greet.customProperty = '我是属性';
console.log(greet.customProperty); // "我是属性"

2.2 为什么需要函数?

原因 1:代码复用
javascript 复制代码
// ❌ 没有函数:重复代码
console.log('Hello, 张三!');
console.log('Hello, 李四!');
console.log('Hello, 王五!');

// ✅ 有函数:复用代码
function greet(name) {
  console.log(`Hello, ${name}!`);
}

greet('张三');
greet('李四');
greet('王五');
原因 2:封装逻辑
javascript 复制代码
// 复杂逻辑封装成函数
function calculateTotalPrice(items, taxRate, discountRate) {
  let subtotal = items.reduce((sum, item) => sum + item.price, 0);
  let tax = subtotal * taxRate;
  let discount = subtotal * discountRate;
  return subtotal + tax - discount;
}

// 使用时无需关心内部实现
let total = calculateTotalPrice(cartItems, 0.1, 0.05);
原因 3:模块化
javascript 复制代码
// 不同功能拆分成不同函数
function validateUser(user) {
  /* ... */
}
function saveUser(user) {
  /* ... */
}
function sendEmail(user) {
  /* ... */
}

function registerUser(userData) {
  if (!validateUser(userData)) return;
  saveUser(userData);
  sendEmail(userData);
}

2.3 为什么函数是对象?

证据 1:函数可以有属性
javascript 复制代码
function myFunc() {
  console.log('函数执行');
}

// 添加属性
myFunc.description = '这是一个函数';
myFunc.version = '1.0';

// 访问属性
console.log(myFunc.description); // "这是一个函数"
证据 2:函数可以作为值传递
javascript 复制代码
// 函数可以赋值给变量
let fn = function () {
  return 'hello';
};

// 函数可以作为参数
function execute(callback) {
  callback();
}

execute(function () {
  console.log('我是回调函数');
});

// 函数可以作为返回值
function createMultiplier(factor) {
  return function (num) {
    return num * factor;
  };
}

let double = createMultiplier(2);
console.log(double(5)); // 10
证据 3:函数继承自 Function.prototype
javascript 复制代码
function myFunc() {}

// 函数的原型链
console.log(myFunc.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true

// 所以函数有对象的方法
console.log(myFunc.toString()); // "function myFunc() {}"
console.log(myFunc.hasOwnProperty('name')); // true

2.4 函数的特殊性

虽然函数是对象,但有特殊之处:

javascript 复制代码
function myFunc() {
  return 'hello';
}

// 特殊性 1:可以被调用
console.log(myFunc()); // "hello"

// 特殊性 2:有 prototype 属性(用于构造函数)
console.log(myFunc.prototype); // { constructor: [Function: myFunc] }

// 特殊性 3:有 name 属性
console.log(myFunc.name); // "myFunc"

// 特殊性 4:有 length 属性(参数个数)
function add(a, b, c) {}
console.log(add.length); // 3

2.5 函数的多种形式

javascript 复制代码
// 1. 函数声明
function func1() {}

// 2. 函数表达式
let func2 = function () {};

// 3. 箭头函数
let func3 = () => {};

// 4. 方法(对象中的函数)
let obj = {
  method() {},
};

// 5. 构造函数
function Person(name) {
  this.name = name;
}

// 6. 生成器函数
function* generator() {
  yield 1;
  yield 2;
}

// 7. 异步函数
async function asyncFunc() {
  await somePromise;
}

三、数组 (Array) - 有序业务数据的首选

3.1 业务场景:消息列表、商品列表

javascript 复制代码
// 聊天消息 - 必须保持顺序!
const messages = [
  { id: 1, content: '你好', time: '09:00' },
  { id: 2, content: '在吗?', time: '09:01' },
  { id: 3, content: '有个需求', time: '09:02' },
];

// 业务操作
messages.push({ id: 4, content: '看到了', time: '09:03' }); // 新消息追加
const lastMessage = messages[messages.length - 1]; // 获取最后一条
messages.forEach(msg => renderMessage(msg)); // 按顺序渲染

业务要点

  • ✅ 必须用数组的场景:时间线、步骤流程、排序列表
  • ✅ 需要用到数组方法的场景:filter搜索、map转换、reduce统计

3.2 数组和对象的核心差异

3.2.1 设计目的不同

数组相比对象,远不止是遍历优化,它在很多方面都是专门为"有序数据集合"这个场景设计的。

3.2.1.1 对象:键值对存储(关心"谁是谁")
javascript 复制代码
const user = {
  id: 1,
  name: '张三',
  age: 25,
};
// 设计目标:通过键名快速访问值
3.2.1.2 数组:有序集合(关心"谁在哪儿")
javascript 复制代码
const users = ['张三', '李四', '王五'];
// 设计目标:维护元素顺序,提供顺序操作
3.2.2 数组的专属超能力
3.2.2.1 自动维护的 length 属性
javascript 复制代码
const arr = [1, 2, 3];
console.log(arr.length); // 3

arr.push(4); // length 自动变成 4
arr.pop(); // length 自动变成 3

// 对象需要手动维护
const obj = { 0: 1, 1: 2, 2: 3 };
obj.length = 3; // 要自己记!
3.2.2.2 专门的顺序操作方法
javascript 复制代码
const tasks = ['吃饭', '睡觉', '写代码'];

// 数组特有的顺序操作
tasks.push('摸鱼'); // 末尾添加
tasks.pop(); // 末尾删除
tasks.unshift('起床'); // 开头添加
tasks.shift(); // 开头删除
tasks.splice(1, 1); // 中间删除

// 对象做不到这种精细的顺序控制!
3.2.2.3 内置的迭代器和顺序保证
javascript 复制代码
const arr = ['第一', '第二', '第三'];

// 数组保证顺序
for (const item of arr) {
  console.log(item); // 永远是"第一", "第二", "第三"
}

// 对象不保证顺序
const obj = { 0: '第一', 1: '第二', 2: '第三' };
for (const key in obj) {
  console.log(obj[key]); // 顺序可能变化!
}
3.2.2.4 丰富的内置方法
javascript 复制代码
const numbers = [1, 2, 3, 4, 5];

// 数组特有的高阶函数
const doubled = numbers.map(x => x * 2); // 映射
const evens = numbers.filter(x => x % 2 === 0); // 过滤
const sum = numbers.reduce((a, b) => a + b, 0); // 归并

// 对象没有这些方法!
3.2.3 性能优化的底层原理
3.2.3.1 数组:内存连续分配
复制代码
内存: [1][2][3][4][5] ← 连续存储,CPU缓存友好
索引:  0  1  2  3  4
3.2.3.2 对象:哈希表散列存储
复制代码
内存: [ ][x][ ][x][x][ ] ← 分散存储,需要哈希计算
键名:   a     c  d
3.2.4 业务场景对比
3.2.4.1 用数组的场景(需要顺序和位置)
javascript 复制代码
// 消息列表 - 必须保持时间顺序
const messages = [
  { id: 1, content: '你好', time: '09:00' },
  { id: 2, content: '在吗?', time: '09:01' },
  { id: 3, content: '回复', time: '09:02' },
];

// 业务操作
const lastMsg = messages[messages.length - 1]; // 获取最后一条
messages.push(newMsg); // 新消息追加
messages.find(msg => msg.id === 2); // 按条件查找
3.2.4.2 用对象的场景(需要键值映射)
javascript 复制代码
// 用户信息 - 按键名快速访问
const user = {
  id: 123,
  name: "张三",
  profile: {...},
  settings: {...}
};

// 业务操作
console.log(user.name); // 直接访问
user.avatar = "url";   // 动态扩展

3.3 总结

数组相比对象的优势:

特性 数组 对象
顺序保证 ✅ 严格保持插入顺序 ❌ 不保证顺序
长度管理 ✅ 自动维护length ❌ 需要手动维护
顺序操作 ✅ push/pop/shift/unshift ❌ 无法直接操作顺序
内存布局 ✅ 连续存储,缓存友好 ❌ 散列存储,需要哈希计算
内置方法 ✅ map/filter/reduce等 ❌ 无顺序相关方法

简单说 :数组是专门为"有序集合"这个场景设计的瑞士军刀,而对象是通用的键值对容器。选择哪个,取决于你的数据是否需要顺序这个核心特征!

四、类数组 (Array-like) - DOM操作的日常

4.1 业务场景:DOM元素批量处理

javascript 复制代码
// 实际业务中经常遇到类数组
const productCards = document.querySelectorAll('.product-card'); // NodeList
const formElements = document.forms[0].elements; // HTMLFormControlsCollection

// ❌ 错误做法:直接使用类数组
// productCards.forEach(card => { ... }); // 可能报错!

// ✅ 正确做法:转换为真正数组
const cardsArray = Array.from(productCards);
cardsArray.forEach(card => {
  card.addEventListener('click', this.handleProductClick);
});

// ✅ 或者使用展开运算符
[...productCards].filter(card => {
  return card.dataset.category === 'hot';
});

业务陷阱

  • 不同浏览器对DOM集合的支持不一致
  • 某些类数组有live特性(动态更新)
  • 忘记转换直接调用数组方法会报错

五、可迭代对象 & for...of - 业务中的遍历艺术

5.1 什么是可迭代?

可迭代(Iterable)= 可以被 for...of 遍历的对象

javascript 复制代码
// 可迭代对象
let arr = [1, 2, 3];
for (let item of arr) {
  console.log(item); // 1, 2, 3
}

// 不可迭代对象
let obj = { a: 1, b: 2 };
for (let item of obj) {
  // ❌ TypeError: obj is not iterable
}

5.2 为什么有可迭代和不可迭代的区别?

核心原因:设计哲学不同
  • 数组:天生就是"列表",需要按顺序遍历元素
  • 对象 :是"属性集合",没有固定顺序,不适合 for...of
javascript 复制代码
// 数组:有明确的顺序
let colors = ['红', '绿', '蓝'];
// 需求:按顺序显示颜色

// 对象:没有保证的顺序
let person = {
  name: '张三',
  age: 25,
  city: '北京',
};
// 不关心属性的顺序

5.3 业务场景:统一处理各种数据源

javascript 复制代码
// 业务中需要处理多种数据源
function processItems(items) {
  // ✅ for...of 可以统一处理各种可迭代对象
  for (const item of items) {
    processItem(item);
  }
}

// 所有这些都能正常工作:
processItems([1, 2, 3]); // 数组
processItems('hello'); // 字符串
processItems(new Set([1, 2, 3])); // Set
processItems(document.querySelectorAll('div')); // NodeList
processItems(new Map([['key', 'value']])); // Map

// ❌ 但这个会报错!
// processItems({a: 1, b: 2}); // 普通对象不可迭代

业务价值

  • 🔄 统一接口:用相同方式处理不同数据源
  • 🚀 性能优化:for...of 比 forEach 在某些场景更快
  • 💡 代码清晰:语义明确的遍历

六、迭代器 (Iterator) - 统一遍历的秘密武器

6.1 迭代 vs 遍历:细微但重要的区别

在深入迭代器之前,先理清一个常见困惑:

遍历 (Traversal) = 做什么 (访问每个元素的行为)
迭代 (Iteration) = 怎么做(实现遍历的底层机制)

javascript 复制代码
const arr = [1, 2, 3];

// 遍历:你在做的事情
for (const item of arr) {
  console.log(item); // 遍历数组
}

// 迭代:底层的实现机制
const iterator = arr[Symbol.iterator](); // 获取迭代器
iterator.next(); // {value: 1, done: false} - 迭代过程
iterator.next(); // {value: 2, done: false}
iterator.next(); // {value: 3, done: false}
生活化理解:
  • 遍历 = "我要读完这本书"(目标)
  • 迭代 = "用书签一页页翻"(方法)
为什么区分这个概念很重要?

因为不同的数据结构有不同的迭代方式 ,但遍历体验可以统一

javascript 复制代码
// 数组:按索引顺序迭代
[1, 2, 3][Symbol.iterator]();

// Map:按插入顺序迭代键值对
new Map([['a', 1]])[Symbol.iterator]();

// Set:按插入顺序迭代值
new Set([1, 2, 3])[Symbol.iterator]();

// 不同的迭代方式,相同的遍历体验:
for (const item of 任意可迭代对象) {
  // 统一的遍历语法
}

核心理解:迭代器让不同的数据结构能够提供统一的遍历接口。


6.2 什么是迭代器?"智能书签"的比喻

迭代器就像读书时的智能书签

没有迭代器时:
javascript 复制代码
// 像没有书签,每次都要从头开始找
const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
  // 手动管理位置
  console.log(arr[i]);
}
有迭代器时:
javascript 复制代码
// 像有个智能书签,记住你读到哪了
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator](); // 拿到"书签"

console.log(iterator.next()); // {value: 1, done: false} - 读第一页
console.log(iterator.next()); // {value: 2, done: false} - 读第二页
console.log(iterator.next()); // {value: 3, done: false} - 读第三页
console.log(iterator.next()); // {value: undefined, done: true} - 读完了

6.3 迭代器的核心:next() 方法

每个迭代器都有一个 next() 方法,返回:

javascript 复制代码
{
  value: 当前值,     // 这页的内容
  done: 是否结束     // 是否读到最后一页
}

6.4 业务中的迭代器:

for...of 循环的背后
javascript 复制代码
const fruits = ['苹果', '香蕉', '橙子'];

// 这背后其实就是迭代器在工作
for (const fruit of fruits) {
  console.log(fruit);
}

// 相当于:
const iterator = fruits[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
  console.log(result.value);
  result = iterator.next();
}
展开运算符的背后
javascript 复制代码
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 展开运算符也在用迭代器
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
解构赋值的背后
javascript 复制代码
const [first, second] = [1, 2, 3];
console.log(first); // 1 - 迭代器帮你取第一个值
console.log(second); // 2 - 迭代器帮你取第二个值

6.5 哪些东西有迭代器?

可迭代对象 (有 [Symbol.iterator] 方法的):

javascript 复制代码
// 数组
[1, 2, 3][Symbol.iterator]();

// 字符串
'hello'[Symbol.iterator]();

// Map
new Map()[Symbol.iterator]();

// Set
new Set()[Symbol.iterator]();

// NodeList
document
  .querySelectorAll('div')
  [Symbol.iterator]()(
    // 但普通对象没有!
    { a: 1 }
  )
  [Symbol.iterator](); // ❌ 报错

6.6 自己创建迭代器:让自定义对象可遍历

javascript 复制代码
class TaskList {
  constructor() {
    this.tasks = ['写代码', '测bug', '部署'];
  }

  // 添加迭代器方法
  [Symbol.iterator]() {
    let index = 0;
    const tasks = this.tasks;

    return {
      next() {
        if (index < tasks.length) {
          return { value: tasks[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  }
}

// 现在可以用 for...of 遍历了!
const myTasks = new TaskList();
for (const task of myTasks) {
  console.log(task); // "写代码", "测bug", "部署"
}

6.7 迭代器的业务价值

统一遍历接口

不管什么数据结构,只要支持迭代器,就能用相同方式遍历:

javascript 复制代码
function processItems(items) {
  for (const item of items) {
    // 数组、Set、Map、字符串都能用
    console.log(item);
  }
}
惰性计算(需要时才计算)
javascript 复制代码
function* numberGenerator() {
  let num = 0;
  while (true) {
    yield num++; // 需要时才生成下一个数
  }
}

const numbers = numberGenerator();
console.log(numbers.next().value); // 0 - 现算的
console.log(numbers.next().value); // 1 - 现算的
处理大数据集
javascript 复制代码
// 可以逐个处理,不用一次性加载所有数据到内存
function* processLargeData() {
  for (let i = 0; i < 1000000; i++) {
    yield processItem(i); // 一次处理一个
  }
}

6.8 总结

迭代器就是:

  • 📖 智能书签:记住遍历位置
  • 🔄 统一协议:所有可迭代对象都用相同方式遍历
  • 🚀 惰性能力:需要时才计算值
  • 🎯 遍历抽象:把"怎么遍历"的逻辑封装起来

现在用的 for...of...展开解构 都在底层使用了迭代器。它让 JavaScript 的遍历变得统一而强大!

七、普通对象的遍历 - 业务中的各种姿势

7.1 为什么对象不可迭代?

回顾前面讲的迭代器知识,可迭代对象必须实现 [Symbol.iterator] 方法

javascript 复制代码
// 数组有迭代器,所以可迭代
const arr = [1, 2, 3];
console.log(arr[Symbol.iterator]); // ƒ values() { [native code] }

// 对象没有迭代器,所以不可迭代
const obj = { a: 1, b: 2 };
console.log(obj[Symbol.iterator]); // undefined

// 因此会报错:
// for (const item of obj) {} // ❌ TypeError: obj is not iterable

设计原因:对象的无序性

对象之所以不默认实现迭代器,是因为:

  1. 属性无序:对象不保证属性顺序,而迭代依赖于明确的顺序
  2. 键的类型复杂:对象的键可以是字符串、Symbol,迭代器难以统一处理
  3. 原型链继承:遍历时是否包含原型链属性?没有统一标准

如何让对象可迭代?

如果需要让对象支持 for...of,可以手动添加迭代器:

javascript 复制代码
const user = {
  name: '张三',
  age: 25,
  department: '技术部',

  // 自定义迭代器
  [Symbol.iterator]: function* () {
    yield this.name;
    yield this.age;
    yield this.department;
  },
};

// 现在可以用 for...of 了!
for (const value of user) {
  console.log(value); // "张三", 25, "技术部"
}

但在日常业务中,通常更推荐使用 Object.keys()、Object.values()、Object.entries() 这些专门为对象设计的遍历方法。

7.2 五种遍历方式及业务场景

7.2.1 方式1:for...in - 遍历可枚举属性
javascript 复制代码
const user = {
  id: 'U1001',
  name: '张三',
  age: 25,
  [Symbol('internal')]: '内部数据', // Symbol属性不会被遍历
};

// 遍历自身和原型链上的可枚举属性
for (const key in user) {
  console.log(`${key}: ${user[key]}`);
}
// 输出:
// id: U1001
// name: 张三
// age: 25

// ✅ 业务场景:通用对象遍历
// ❌ 问题:会遍历到原型链上的属性
7.2.2 方式2:Object.keys() - 只遍历自身属性
javascript 复制代码
const user = {
  id: 'U1001',
  name: '张三',
  age: 25,
};

// 只遍历自身可枚举属性
const keys = Object.keys(user);
keys.forEach(key => {
  console.log(`${key}: ${user[key]}`);
});
// 输出:
// id: U1001
// name: 张三
// age: 25

// ✅ 业务场景:表单数据处理、API参数构造
// ✅ 安全:不会遍历到原型链
7.2.3 方式3:Object.values() - 直接获取值
javascript 复制代码
const config = {
  theme: 'dark',
  language: 'zh-CN',
  fontSize: 14,
};

// 直接获取值数组
const values = Object.values(config);
console.log(values); // ["dark", "zh-CN", 14]

// ✅ 业务场景:配置项批量应用、数据统计
values.forEach(value => applySetting(value));
7.2.4 方式4:Object.entries() - 同时获取键值
javascript 复制代码
const formData = {
  username: 'zhangsan',
  password: '123456',
  remember: true,
};

// 同时获取键值对
Object.entries(formData).forEach(([key, value]) => {
  console.log(`字段${key}: 值${value}`);
  // 字段username: 值zhangsan
  // 字段password: 值123456
  // 字段remember: 值true
});

// ✅ 业务场景:表单验证、数据转换、表格渲染
7.2.5 方式5:Object.getOwnPropertyNames() - 包含不可枚举属性
javascript 复制代码
const obj = Object.create(null, {
  name: { value: '张三', enumerable: true },
  id: { value: 'U1001', enumerable: false }, // 不可枚举
});

console.log(Object.keys(obj)); // ["name"]
console.log(Object.getOwnPropertyNames(obj)); // ["name", "id"]

// ✅ 业务场景:框架开发、深度调试
// ❌ 日常业务很少用

八、Set - 高效的去重与集合运算

8.1 业务场景1:数据去重

javascript 复制代码
// 数组去重 - 最经典的用法
const duplicateIds = [1, 2, 2, 3, 3, 3, 4];
const uniqueIds = [...new Set(duplicateIds)]; // [1, 2, 3, 4]

// 对象数组去重(需要配合其他方法)
const users = [
  { id: 1, name: '张三' },
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
];
const uniqueUsers = [...new Map(users.map(user => [user.id, user])).values()];

8.2 业务场景2:权限与状态管理

javascript 复制代码
// 用户权限集合
const userRoles = new Set(['admin', 'editor']);

// O(1) 权限验证
function checkPermission(role) {
  return userRoles.has(role);
}

// 动态权限管理
userRoles.add('viewer'); // 添加权限
userRoles.delete('editor'); // 移除权限
console.log(userRoles.size); // 查看权限数量

8.3 业务场景3:集合运算

javascript 复制代码
const groupA = new Set(['张三', '李四', '王五']);
const groupB = new Set(['李四', '赵六', '钱七']);

// 交集 - 两个组都有的成员
const intersection = new Set([...groupA].filter(x => groupB.has(x))); // {"李四"}

// 并集 - 两个组所有成员
const union = new Set([...groupA, ...groupB]); // {"张三", "李四", "王五", "赵六", "钱七"}

// 差集 - A组有但B组没有的成员
const difference = new Set([...groupA].filter(x => !groupB.has(x))); // {"张三", "王五"}

8.4 性能对比:Set vs Array

javascript 复制代码
// 创建测试数据
const largeArray = Array.from({ length: 10000 }, (_, i) => i);
const largeSet = new Set(largeArray);

// 查找性能测试
console.time('Array includes');
largeArray.includes(9999); // O(n)
console.timeEnd('Array includes');

console.time('Set has');
largeSet.has(9999); // O(1)
console.timeEnd('Set has');

业务优势

  • 🚀 去重性能:比数组方法快得多
  • 查找速度:O(1) 的存在判断
  • 🔧 集合操作:内置交集、并集、差集能力
  • 🎯 语义清晰:明确表示"唯一值集合"
  • 📊 大小获取:直接通过 size 属性获取元素数量

8.5 使用建议

适合使用 Set 的场景:
  • ✅ 需要快速判断元素是否存在
  • ✅ 数据去重需求
  • ✅ 集合运算(交集、并集、差集)
  • ✅ 需要维护唯一值集合
不适合的场景:
  • ❌ 需要按键名访问特定元素(用 Map)
  • ❌ 需要保持元素顺序(用 Array)
  • ❌ 需要重复元素(用 Array)

总结:Set 是处理唯一值集合的最佳选择,特别是在需要快速查找和去重的业务场景中。

九、Map - 现代业务数据管理

9.1 业务场景1:复杂键名的数据存储

javascript 复制代码
// 用对象作为键 - 普通对象做不到!
const userSessionMap = new Map();

const user1 = { id: 'U1001', name: '张三' };
const user2 = { id: 'U1002', name: '李四' };

userSessionMap.set(user1, { token: 'abc123', expires: '2024-01-01' });
userSessionMap.set(user2, { token: 'def456', expires: '2024-01-02' });

// 业务查询
const session = userSessionMap.get(user1);
console.log(session.token); // "abc123"

9.2 业务场景2:需要顺序保证的键值对

javascript 复制代码
// 操作记录需要保持顺序
const operationLog = new Map();

operationLog.set(1, { action: '登录', time: '09:00' });
operationLog.set(2, { action: '查询', time: '09:01' });
operationLog.set(3, { action: '修改', time: '09:02' });

// 业务展示 - 保持操作顺序
for (const [step, log] of operationLog) {
  console.log(`步骤${step}: ${log.action} - ${log.time}`);
}
// 步骤1: 登录 - 09:00
// 步骤2: 查询 - 09:01
// 步骤3: 修改 - 09:02

业务优势

  • 🗝️ 灵活键类型:对象、函数等都可以作为键
  • 📊 顺序保证:操作记录、步骤流程等场景
  • 🎯 专用API:size、has、delete等方法更语义化

对的!你这个理解很准确!Set 的核心优势就是去重,这是它和 Map 最大的区别。

十、Set vs Map 的直观对比

10.1 Set:值的集合(关注值本身)

javascript 复制代码
const set = new Set();
set.add('张三');
set.add('李四');
set.add('张三'); // 自动去重,只有一个"张三"

console.log([...set]); // ["张三", "李四"]

10.2 Map:键值对的集合(关注键值映射)

javascript 复制代码
const map = new Map();
map.set('user1', '张三');
map.set('user2', '李四');
map.set('user1', '张三'); // 不会去重,只是覆盖值

console.log([...map]); // [["user1", "张三"], ["user2", "李四"]]

10.3 业务中的选择标准

用 Set 的场景:只关心"有什么"
javascript 复制代码
// 场景:用户选择的标签
const selectedTags = new Set();
selectedTags.add('VIP');
selectedTags.add('新用户');
selectedTags.add('VIP'); // 自动去重

// 我只需要知道:用户选了哪些标签(不关心顺序,不关心数量)
console.log(selectedTags.has('VIP')); // true
用 Map 的场景:关心"什么对应什么"
javascript 复制代码
// 场景:用户配置信息
const userSettings = new Map();
userSettings.set('theme', 'dark');
userSettings.set('language', 'zh-CN');
userSettings.set('notifications', true);

// 我需要知道:theme对应什么值,language对应什么值
console.log(userSettings.get('theme')); // "dark"

10.4 性能方面:两者都是 O(1)

javascript 复制代码
// Set 和 Map 的查找性能一样快
const set = new Set([1, 2, 3, 4, 5]);
const map = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

set.has(3); // O(1) - 瞬间完成
map.has('b'); // O(1) - 瞬间完成

10.5 总结

特性 Set Map
核心功能 值唯一性 键值映射
去重能力 ✅ 自动去重 ❌ 不会去重
查找性能 O(1) O(1)
适用场景 标签、权限、唯一ID 配置、缓存、对象索引

简单记法

  • 只要去重,就用 Set
  • 需要键值对应,就用 Map
  • 两者查找都很快

所以你的理解完全正确!Set 的杀手锏就是去重,这是 Map 做不到的。

十一、O(1) vs O(n) - 业务性能的生死线

11.1 通俗理解:找人的不同方式

O(n) - 数组查找:一个个问

javascript 复制代码
// 就像在人群中一个个问:"你是张三吗?"
const people = ['李四', '王五', '张三', '赵六'];

function findPerson(people, name) {
  for (let i = 0; i < people.length; i++) {
    if (people[i] === name) {
      // 一个个比较
      return i;
    }
  }
  return -1;
}
// 1000个人就要问1000次!

O(1) - Map查找:直接叫名字

javascript 复制代码
// 就像有个人名册,直接找到张三的位置
const peopleMap = new Map([
  ['李四', 0],
  ['王五', 1],
  ['张三', 2],
  ['赵六', 3],
]);

function findPersonFast(peopleMap, name) {
  return peopleMap.get(name); // 直接获取,不用比较
}
// 不管有1000人还是10000人,都只需要1步!

11.2 业务性能影响:数据量越大,差距越恐怖

数据量 O(n) 查找次数 O(1) 查找次数 性能差距
10个元素 平均5次 1次 5倍
100个元素 平均50次 1次 50倍
1000个元素 平均500次 1次 500倍
10000个元素 平均5000次 1次 5000倍

11.3 业务实战:优化前后对比

优化前:O(n) 查找(性能差)

javascript 复制代码
class ProductList {
  constructor(products) {
    this.products = products; // 1000个商品
  }

  findProduct(id) {
    // ❌ O(n) - 数据量大时很慢!
    return this.products.find(product => product.id === id);
  }

  // 业务操作:频繁查找
  updateProductPrice(id, newPrice) {
    const product = this.findProduct(id); // 每次都要遍历
    if (product) {
      product.price = newPrice;
    }
  }
}

优化后:O(1) 查找(性能极佳)

javascript 复制代码
class OptimizedProductList {
  constructor(products) {
    this.products = products;
    // ✅ 建立索引 - 一次性O(n)开销
    this.productMap = new Map();
    products.forEach(product => {
      this.productMap.set(product.id, product);
    });
  }

  findProduct(id) {
    // ✅ O(1) - 瞬间找到!
    return this.productMap.get(id);
  }

  updateProductPrice(id, newPrice) {
    const product = this.findProduct(id); // 瞬间完成
    if (product) {
      product.price = newPrice;
    }
  }
}

十二、综合实战:IntersectionObserver 业务优化

12.1 业务背景

直播列表页面,需要监听哪些直播项进入视口,进行视频预加载和状态更新。

12.2 优化前的问题

javascript 复制代码
handleIntersect(entries) {
  entries.forEach((entry) => {
    // ❌ 每次触发都要转换+遍历
    const array = Array.from(this.itemRefs);
    const index = array.findIndex((item) => item === entry.target);
    // 如果有100个元素,最坏情况要比较100次!

    // 业务逻辑...
    this.updateLiveStatus(index, entry.isIntersecting);
  });
}

12.3 优化后的方案

javascript 复制代码
class OptimizedLiveList {
  initIntersectionObserver() {
    const items = document.querySelectorAll('.live-item');

    // ✅ 一次性转换:类数组 → 真正数组
    this.itemRefs = Array.from(items);

    // ✅ 建立索引:O(1) 快速查找
    this.itemIndexMap = new Map();
    this.itemRefs.forEach((item, index) => {
      this.itemIndexMap.set(item, index);
    });

    this.observer = new IntersectionObserver(this.handleIntersect.bind(this));
    this.itemRefs.forEach(item => this.observer.observe(item));
  }

  handleIntersect(entries) {
    entries.forEach(entry => {
      // ✅ O(1) 查找:不管多少元素都瞬间完成
      const index = this.itemIndexMap.get(entry.target);
      if (index === undefined) return;

      // 业务逻辑
      this.updateLiveStatus(index, entry.isIntersecting);
      this.preloadVideo(index);
    });
  }
}

12.4 业务收益

  • 🚀 性能提升:滚动时更流畅,无卡顿
  • 🔋 电量节省:减少不必要的计算
  • 📱 用户体验:视频预加载更及时
  • 🛠️ 代码健壮:边界情况处理更好

12.5 总结:业务选择指南

数据结构 业务场景 性能特点 使用建议
Object 配置对象、实体数据 O(1)访问 结构简单时首选
Array 列表、时间线、步骤 有序存储 需要顺序时使用
Map 复杂键、需要顺序的键值对 O(1)访问 高级场景使用
Set 去重、集合运算 O(1)判断存在 去重需求时使用

12.6 快速决策指南

遇到具体场景时:

  • 🕒 需要时间顺序 → Array
  • 🔍 需要快速查找 → Map/Set
  • 🏷️ 需要去重 → Set
  • 📝 需要键值映射 → Map/Object
  • 🎯 需要DOM操作 → 记得Array.from()

黄金法则:根据业务需求选择数据结构,在性能敏感场景优先选择 O(1) 操作!

相关推荐
Glommer3 小时前
某易易盾验证码处理思路(下)
javascript·逆向
砺能4 小时前
window.postMessage与window.dispatchEvent
前端·javascript
雪中何以赠君别4 小时前
【框架】CLI 工具笔记
javascript·node.js
th7394 小时前
Symbol的11个内置符号的使用场景
javascript
古夕4 小时前
基于 Vue 3 + Monorepo + 微前端的中后台前端项目框架全景解析
前端·javascript·vue.js
JustNow_Man4 小时前
【Cline】插件中clinerules的实现逻辑分析
开发语言·前端·javascript
呼叫69454 小时前
requestAnimationFrame 深度解析
前端·javascript
Bigger4 小时前
🚀 真正实用的前端算法技巧:从 semver-compare 到智能版本排序
前端·javascript·算法
hope_wisdom5 小时前
C/C++数据结构之用链表实现栈
c语言·数据结构·c++·链表·