📚 目录
基础篇
- 前言
- [一、对象 (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 对象的本质特征
- 无序性:属性没有固定顺序
- 键值对结构:每个属性由"键"和"值"组成
- 键必须是字符串或 Symbol(会自动转换)
- 值可以是任何类型(包括对象、数组、函数)
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
设计原因:对象的无序性
对象之所以不默认实现迭代器,是因为:
- 属性无序:对象不保证属性顺序,而迭代依赖于明确的顺序
- 键的类型复杂:对象的键可以是字符串、Symbol,迭代器难以统一处理
- 原型链继承:遍历时是否包含原型链属性?没有统一标准
如何让对象可迭代?
如果需要让对象支持 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) 操作!