迭代器是 JS 统一数据遍历的 “通用接口”,解构则是简化数据提取的 “语法糖”—— 两者结合能让你更优雅地处理数组、对象、DOM 集合等各种数据结构。这篇文章从原理到实战,带你吃透这两个高频用到的核心特性。
一、迭代器:统一数据遍历的 “通用接口” 1.1 什么是迭代器? 迭代器本质是一套 标准化协议 (Iterator Protocol),定义了 “如何按顺序访问数据集合” 的规则。简单说:只要一个对象实现了这套协议,就能用统一的方式(比如 for...of)遍历,不管它是数组、字符串还是自定义对象。
举个生活化的例子:你有一个文件夹,里面有多个文件。迭代器就像一个 “文件读取器”—— 它记住当前读到哪个文件,每次告诉你 “下一个文件是什么”,直到所有文件读完。
看个自定义迭代器的实现,理解它的核心逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const folder = { files : ['笔记.txt' , '照片.jpg' , '文档.pdf' ], [Symbol .iterator ]() { let currentIndex = 0 ; return { next : () => { if (currentIndex < this .files .length ) { return { value : this .files [currentIndex++], done : false , }; } else { return { done : true }; } }, }; }, }; const fileIterator = folder[Symbol .iterator ]();console .log (fileIterator.next ()); console .log (fileIterator.next ()); console .log (fileIterator.next ()); console .log (fileIterator.next ());
1.2 迭代器的三个核心要素 要实现迭代器协议,必须满足以下三点:
[Symbol.iterator] 方法 :对象必须有这个特殊方法(用 Symbol.iterator 作为键),调用后返回一个 “迭代器对象”。
next() 方法 :迭代器对象必须有 next() 方法,调用后返回 { value, done } 结构的对象。
value:当前遍历到的值(遍历完时可省略)。
done:布尔值,false 表示还有值,true 表示遍历结束。
内部状态 :迭代器需要记录当前遍历位置(比如上面的 currentIndex),确保每次 next() 能拿到下一个值。
1.3 可迭代对象:能被遍历的 “合格对象” 只要实现了 [Symbol.iterator]() 方法的对象,它就是可迭代对象 。JS 中很多原生对象天生就是可迭代的,比如:
数组、字符串、Map、Set、TypedArray
DOM 集合:NodeList(比如 document.querySelectorAll 的返回值)
生成器对象(function* 生成的对象)
这些可迭代对象能被以下语法 “消费”(即遍历或提取数据):
for...of 循环
展开运算符(...)
解构赋值
Array.from()(把可迭代对象转数组)
Promise.all()/Promise.race()(接收可迭代的 Promise 集合)
二、解构:简化数据提取的 “语法糖” 2.1 解构的本质:基于迭代器的便捷操作 解构赋值(Destructuring)看似是 “直接拆数据”,其实底层依赖迭代器协议。比如对数组解构时,JS 引擎会悄悄做两件事:
调用数组的 [Symbol.iterator]() 方法,获取迭代器。
多次调用迭代器的 next() 方法,按顺序提取值,赋值给变量。
看个例子,理解解构的底层逻辑:
1 2 3 4 5 6 7 const [a, b] = [1 , 2 ];const tempIterator = [1 , 2 ][Symbol .iterator ](); const a = tempIterator.next ().value ; const b = tempIterator.next ().value ;
这也解释了为什么 “普通对象不能用数组解构”—— 因为普通对象没实现 [Symbol.iterator]() 方法,不是可迭代对象。
2.2 解构的常用能力:告别繁琐的取值 解构最核心的价值是 “少写重复代码”,比如不用再写 arr[0]、obj.name 这种重复的取值逻辑。分享几个高频用法:
跳过不需要的值 :用逗号跳过不想取的元素(比如只想要数组的第三个值)。
1 2 const [, , third] = ['a' , 'b' , 'c' , 'd' ];console .log (third);
收集剩余值 :用 ...变量名 收集解构后剩下的所有值(只能放在最后)。
1 2 3 const [first, ...rest] = [1 , 2 , 3 , 4 ];console .log (first); console .log (rest);
设置默认值 :当解构的值是 undefined 时,使用默认值(注意:null、0、"" 不会触发默认值)。
1 2 3 4 5 6 7 const [name = '匿名用户' ] = [];console .log (name); const { age = 18 } = { name : '小明' };console .log (age);
交换变量 :不用临时变量,一行代码交换两个变量的值。
1 2 3 let x = 1 , y = 2 ; [x, y] = [y, x];
三、不同数据结构的解构实战 不同数据结构的解构方式略有差异,这里整理了日常开发中最常用的 8 种场景,附具体代码示例:
3.1 字符串解构:按字符拆分 字符串是可迭代对象,解构时会按单个字符提取:
1 2 3 4 5 6 7 8 9 10 11 12 13 const [firstChar, secondChar] = 'JS' ;console .log (firstChar); console .log (secondChar); const [head, ...tail] = 'JavaScript' ;console .log (head); console .log (tail); const { length : strLen } = 'hello' ;console .log (strLen);
3.2 数组解构:按位置取值 数组解构最常用,变量的位置和数组元素的位置一一对应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const [x, y] = [10 , 20 ];console .log (x, y); const [foo, [[bar], baz]] = [1 , [[2 ], 3 ]];console .log (foo); console .log (bar); console .log (baz); const [, , third] = ['foo' , 'bar' , 'baz' ];console .log (third); const [head, ...tail] = [1 , 2 , 3 , 4 ];console .log (head); console .log (tail); const [a, b, ...c] = ['a' ];console .log (a); console .log (b); console .log (c); const [d, [e], f] = [1 , [2 , 3 ], 4 ];console .log (d); console .log (e); console .log (f);
3.3 对象解构:按属性名取值 对象解构和数组不同 —— 变量名必须和对象的属性名一致,和属性的顺序无关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const person = { name : 'Alice' , age : 30 , address : { city : 'London' , zip : 10001 , }, hobbies : ['reading' , 'hiking' ], }; const user = { name : 'Alice' , age : 28 , address : { city : 'Beijing' , street : 'Main St' , }, }; const { name, age } = user;console .log (name); console .log (age); const { name : userName } = user;console .log (userName); const { address : { city }, } = user; console .log (city); const { gender = 'unknown' } = user;console .log (gender);
3.4 Map 解构:按键值对拆分 Map 是 “键值对集合”,解构时会按 [key, value] 的形式提取每一项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const userMap = new Map ([ ['name' , 'Bob' ], ['age' , 30 ], ['isAdmin' , false ], ]); for (const [key, value] of userMap) { console .log (`${key} : ${value} ` ); } const [firstEntry, secondEntry] = userMap;console .log (firstEntry); console .log (secondEntry[1 ]); const allKeys = [...userMap.keys ()];const allValues = [...userMap.values ()];console .log (allKeys);
3.5 Set 解构:按元素顺序拆分 Set 是 “唯一值集合”,解构时按元素插入顺序提取:
1 2 3 4 5 6 7 8 9 10 11 const tagSet = new Set (['html' , 'css' , 'js' ]);const [firstTag, secondTag] = tagSet;console .log (firstTag); console .log (secondTag); const tagArray = [...tagSet];const [, , thirdTag] = tagArray;console .log (thirdTag);
3.6 函数参数解构:简化参数处理 函数参数如果是对象或数组,用解构能直接提取需要的属性,不用再手动 obj.xxx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function renderChart ({ width = 400 , // 默认宽度 height = 300 , // 默认高度 type = 'line' , // 默认图表类型 } ) { console .log (`绘制 ${type} 图表,尺寸 ${width} x${height} ` ); } renderChart ({ width : 600 });function printUser ({ name, address: { city } } ) { console .log (`${name} 住在 ${city} ` ); } printUser ({ name : 'Charlie' , address : { city : 'Shanghai' , zip : '200000' }, });
3.7 DOM 集合(NodeList)解构 document.querySelectorAll 返回的 NodeList 是可迭代对象,能直接解构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <div class ="item" > 首页</div > <div class ="item" > 列表</div > <div class ="item" > 详情</div > <script > const items = document .querySelectorAll ('.item' ); const [homeItem, listItem] = items; console .log (homeItem.textContent ); const [, , detailItem] = [...items]; console .log (detailItem.textContent ); </script >
3.8 生成器对象解构 生成器函数(function*)返回的对象是可迭代的,解构时会按 yield 的顺序提取值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function * generateNumbers ( ) { yield 1 ; yield 2 ; yield 3 ; } const [num1, num2] = generateNumbers ();console .log (num1, num2); const allNums = [...generateNumbers ()];console .log (allNums);
四、高级解构技巧:解决复杂场景 4.1 动态属性名解构:按变量名提取属性 如果对象的属性名是动态的(比如由变量决定),可以用 [变量名] 来解构:
1 2 3 4 5 6 7 8 9 10 11 const apiRes = { status_200 : '请求成功' , status_404 : '资源未找到' , status_500 : '服务器错误' , }; const statusCode = 404 ;const { [`status_${statusCode} ` ]: message } = apiRes;console .log (message);
4.2 混合解构:处理嵌套复杂的数据 实际开发中,API 响应常是 “对象嵌套数组” 的结构,用混合解构能一步提取关键数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const apiResponse = { code : 200 , data : { products : [ { id : 1 , name : '手机' , price : 5999 }, { id : 2 , name : '平板' , price : 3999 }, ], pagination : { current : 1 , total : 10 }, }, }; const { code, data : { products : [{ name : firstProductName }], pagination : { total }, }, } = apiResponse; console .log (`状态码:${code} ,第一个商品:${firstProductName} ,总页数:${total} ` );
4.3 解构 + 迭代器:处理分页数据 结合生成器(迭代器)和解构,可以优雅地处理分页数据(比如一次获取前三页数据):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 async function * fetchPages (url ) { let page = 1 ; while (true ) { const res = await fetch (`${url} ?page=${page} ` ); const { data, hasMore } = await res.json (); yield data; if (!hasMore) break ; page++; } } async function getFirstThreePages ( ) { const pageGenerator = fetchPages ('/api/articles' ); const [page1, page2, page3] = await Promise .all ([ pageGenerator.next (), pageGenerator.next (), pageGenerator.next (), ]).then ((results ) => results.map ((r ) => r.value )); return { page1, page2, page3 }; }
五、避坑指南:解构的常见问题 5.1 普通对象不能用数组解构 普通对象没实现迭代器协议,直接用数组解构会报错:
1 2 const obj = { a : 1 , b : 2 };const [x, y] = obj;
5.2 解构声明必须用 let/const/var 如果直接写 { name } = obj,会被引擎解析为 “代码块”,导致语法错误:
1 2 3 4 5 { name } = { name : "Alice" }; const { name } = { name : "Alice" };
5.3 默认值只在值为 undefined 时生效 如果解构的值是 null、0、"" 等 “假值”,默认值不会触发:
1 2 3 4 5 6 7 const { value = 10 } = { value : null };console .log (value); const { value = 10 } = { otherKey : 'test' };console .log (value);
5.4 嵌套解构时,外层属性必须存在 如果嵌套解构的外层属性不存在,会报错:
1 2 3 4 5 6 7 8 9 const user = { name : 'Dave' };const { address : { city }, } = user; const { address : { city } = {} } = user;console .log (city);
六、实际应用场景:让代码更优雅 场景 1:简化函数参数 之前写函数参数要记顺序,用解构后直接按属性名取,不用关心顺序:
1 2 3 4 5 6 7 8 9 10 11 12 function createUser (id, name, email, role ) { } function createUser ({ id, name, email, role = 'user' } ) { } createUser ({ id : 1 , name : 'Eve' , email : 'eve@test.com' });
场景 2:处理 API 响应 不用层层 res.data.xxx,一步提取需要的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 async function getArticleDetail (id ) { try { const { data : { article : { title, content }, author : { name : authorName }, }, } = await axios.get (`/api/articles/${id} ` ); renderArticle (title, content, authorName); } catch ({ message }) { alert (`获取文章失败:${message} ` ); } }
场景 3:组件 props 处理(以 React 为例) 组件接收 props 时,用解构直接提取需要的属性,代码更简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const ProductCard = ({ product: { name, price, image }, theme = 'light' , // 默认主题 onAddToCart, } ) => ( <div className ={ `product-card ${theme }`}> <img src ={image} alt ={name} /> <h3 > {name}</h3 > <p > ¥{price}</p > <button onClick ={onAddToCart} > 加入购物车</button > </div > ); <ProductCard product ={{ name: '耳机 ', price: 799 , image: 'headphone.jpg ' }} onAddToCart ={() => console.log('加入购物车')}/> ;
迭代器和解构是 JS 中 “提升代码优雅度” 的关键特性 —— 迭代器统一了数据遍历的方式,解构简化了数据提取的逻辑。掌握它们后,不管是处理简单的数组对象,还是复杂的 API 响应、DOM 集合,都能写出更简洁、更易维护的代码~