- 教程
【免费下载链接】jstips
This is about useful JS tips!
這篇文章是 jstips 專案中第 79 號技巧「循環陣列」的完整展開,原始內容收錄於 _posts/zh_TW/javascript/2017-06-15-looping-over-arrays.md(英文原版見 _posts/en/javascript/2017-06-15-looping-over-arrays.md)。文章從最典型的while、for開始,一路延伸到 ES5/ES6 標準加入的forEach、map、reduce、filter、every、some等高階遍歷方法,並結合倉庫內相關技巧(短路遍歷、模數無限迴圈、reduce進階用法)補足原理與實戰細節。讀完後你將能依據「是否需要回傳值」「是否需要中途跳出」「是否需要聚合結果」等需求,在八種寫法中做出正確選擇。
從典型迴圈到標準擴充:一次看懂八種遍歷寫法
JavaScript 對陣列的遍歷有非常豐富的寫法。原文件的組織邏輯是「先看傳統寫法,再看語言標準後續加入的抽象方法」——後者讓我們把「怎麼走訪」與「對每個元素做什麼」分離開來,程式碼更短、意圖更清晰。以下每一種方法都配上可直接執行的範例與輸出說明。
註:繁體中文版原文中的
for範例與map範例輸出存在筆誤(index++被誤寫成index, index++、${squares}被誤寫成${array}),本文依英文原版修正後呈現。
while 迴圈
while是條件式迴圈:先判斷條件,條件成立才執行迴圈體,因此迴圈體可能完全不會執行。需要自行維護索引計數器:
let index = 0; const array = [1,2,3,4,5,6]; while (index < array.length) { console.log(array[index]); index++; }這種寫法的優點是控制力最強——break、continue、return都可用;缺點是索引的遞增必須手動處理,漏寫index++會造成無限迴圈。
典型 for 迴圈
for把「初始化、條件判斷、索引更新」收斂到一行,是傳統寫法中最常用的形式:
const array = [1,2,3,4,5,6]; for (let index = 0; index < array.length; index++) { console.log(array[index]); }這裡使用let宣告index,讓每次疊代的變數作用域限定在當次迴圈內——這正是 _posts/en/javascript/2016-08-25-keyword-var-vs-let.md 所探討的var與let差異的典型應用場景:若改用var,迴圈結束後index仍會殘留在外層作用域,且閉包捕獲的是同一個變數,容易產生預期之外的結果。
forEach
forEach是 ES5 加入的遍歷方法,回呼函式依序收到三個參數:current_value(目前元素)、index(索引)、array(原陣列):
const array = [1,2,3,4,5,6]; array.forEach(function(current_value, index, array) { console.log(`At index ${index} in array ${array} the value is ${current_value}`); }); // => undefined重點是:forEach永遠回傳undefined。它只負責「走訪並執行副作用」,不會產生新的陣列。此外它無法被break中斷——在回呼中return只是跳過目前元素、進入下一個,行為類似continue,這點會在後文「短路遍歷」小節深入說明。
map
如果forEach的「不回傳新陣列」不符合需求,map就是答案:它對每個元素套用函式,收集每次回呼的回傳值,組成並回傳一個全新的陣列,原陣列不受影響。
const array = [1,2,3,4,5,6]; const square = x => Math.pow(x, 2); const squares = array.map(square); console.log(`Original array: ${array}`); console.log(`Squared array: ${squares}`);map的完整宣告方式與forEach相同:.map(current_value, index, array)。也就是說回呼最多可以接收三個參數,但通常只需要第一個。需要注意的是,如果陣列中存在「空洞」(稀疏陣列),map會跳過空洞並保留洞——如何繞過這個特性,可參考 _posts/en/javascript/2017-03-12-3-array-hacks.md 中提到的Array.apply(null, new Array(N))手法。
reduce
reduce是八種方法中最「進階」也最靈活的一個。原文件引用了 MDN 的定義:
reduce() 方法對累加器和數組中的每個元素(從左到右)應用一個函式,將其減少為一個值。
const array = [1,2,3,4,5,6]; const sum = (x, y) => x + y; const array_sum = array.reduce(sum, 0); console.log(`The sum of array: ${array} is ${array_sum}`);這個範例把陣列累加成單一數值21。reduce的回呼簽名是(accumulator, currentValue, currentIndex, array),第二個參數0是初始累加值——若省略,第一次回呼會直接以陣列前兩個元素作為累加器與目前值。
倉庫中的 _posts/en/javascript/2016-02-17-reminders-about-reduce-function-usage.md(第 48 號技巧)把reduce玩得更深:初始值不只能是數字,也可以是物件或陣列。該文示範了仿照 ReduxcombineReducers的思路,把多個「部分 reducer」組合成一個「大 reducer」,一次計算出美元、歐元、英鎊、日圓四種貨幣的總價——這證明reduce的累加器可以是任意型別,只要你的 reducer 函式知道怎麼更新它。
filter
filter基於一個「布林函式」過濾陣列:回呼回傳true的元素會被保留,回傳false的被丟棄,最後回傳一個由符合條件的元素組成的新陣列。
const array = [1,2,3,4,5,6]; const even = x => x % 2 === 0; const even_array = array.filter(even); console.log(`Even numbers in array ${array}: ${even_array}`);輸出為Even numbers in array 1,2,3,4,5,6: 2,4,6。與map一樣,filter也回傳新陣列、不修改原陣列,兩者常搭配使用形成「先過濾、再轉換」的資料管線。
every
every用來測試「每個元素是否都滿足給定條件」。它走訪陣列直到遇到第一個不滿足條件的元素即停止(短路),並回傳true/false:
const array = [1,2,3,4,5,6]; const under_seven = x => x < 7; if (array.every(under_seven)) { console.log('Every element in the array is less than 7'); } else { console.log('At least one element in the array was bigger than 7'); }由於[1,2,3,4,5,6]全部小於 7,此段會印出Every element in the array is less than 7。every不會回傳陣列,它的角色是「條件判定」。
some
some是every的對偶:測試「至少一個元素是否 match 我們的布林函式」,遇到第一個滿足條件的元素即提前停止並回傳true。
const array = [1,2,3,9,5,6,4]; const over_seven = x => x > 7; if (array.some(over_seven)) { console.log('At least one element bigger than 7 was found'); } else { console.log('No element bigger than 7 was found'); }陣列中存在9,因此會印出At least one element bigger than 7 was found。some與every的短路特性,讓它們在「只想知道有沒有/是不是」的場景下,比forEach或完整for迴圈更有效率——因為不需要走完整個陣列。
深入原理:forEach 為何無法 break?用 some / every 實作短路遍歷
傳統for迴圈可以break提前結束、continue跳過本次疊代;但forEach的回呼是獨立函式,break/continue語法在回呼內並不合法。這是 _posts/en/javascript/2016-08-17-break-continue-loop-functional.md(第 58 號技巧)處理的核心問題。
首先,forEach的回呼裡return只相當於continue:
[0, 1, 2, 3, 4].forEach(function(val, i) { if (val === 2) { return true; // 只是跳過,迴圈仍會繼續 } console.log(val); // 你的程式碼 }); //> 0, 1, 3, 4若想在函數式寫法中實現「中斷」,最接近的做法是利用some:回呼回傳true即中斷整個遍歷,且some本身回傳true;回傳false(或任何 falsy 值)則繼續下一個元素。
[0, 1, 2, 3, 4].some(function(val, i) { if (val === 2) { return true; // break } console.log(val); // 你的程式碼 }); //> 0, 1同樣地,every也可以達到短路效果,只是邏輯相反——every在回呼回傳false時中斷。因此可以歸納出一組對應關係:想要「提早停止並得知結果」,some用return true、every用return false;想要「繼續走訪」,則保持回傳相反的值。這正是把break/continue的語意移植到宣告式 API 上的標準做法。
延伸應用:模數運算打造無限循環陣列
「遍歷陣列」還有一種特殊形態:讓遍歷永遠循環下去。_posts/zh_TW/javascript/2016-05-12-make-easy-loop-on-array.md(第 55 號技巧)以旋轉木馬圖片或播放清單為例,展示了如何用%(模數)運算子讓索引在陣列範圍內循環:
var aList = ['A', 'B', 'C', 'D', 'E']; function make_looper( arr ){ arr.loop_idx = 0; // 回傳目前項目 arr.current = function(){ this.loop_idx = ( this.loop_idx ) % this.length; // 沒有驗證! return arr[ this.loop_idx ]; }; // 增加 loop_idx 並回傳 new current arr.next = function(){ this.loop_idx++; return this.current(); }; // 減少 loop_idx 並回傳 new current arr.prev = function(){ this.loop_idx += this.length - 1; return this.current(); }; } make_looper( aList); aList.current(); // -> A aList.next(); // -> B aList.next(); // -> C aList.next(); // -> D aList.next(); // -> E aList.next(); // -> A aList.pop() ; // -> E aList.prev(); // -> D aList.prev(); // -> C aList.prev(); // -> B aList.prev(); // -> A aList.prev(); // -> D關鍵在%運算子:2 % 5 = 2、5 % 5 = 0,因此loop_idx遞增到length時會自然歸零;prev反向走訪時則先加上length - 1再取模,避免負索引。實務上有兩點提醒:第一,此寫法是在陣列物件上掛載方法(會修改原陣列),若不想污染陣列,可以改用閉包封裝;第二,範例中的current()註明「沒有驗證」,因為pop()改變長度後,取模結果仍能保持在有效範圍內——這正是模數寫法比if判斷更簡潔的原因。
遍歷方法對照表與選型建議
將八種方法放在一起對照,決策就變得直觀:
| 方法 | 回傳值 | 能否提前中斷 | 修改原陣列 | 典型用途 |
|---|---|---|---|---|
while | 無(依迴圈體而定) | 可(break/continue/return) | 否(自行控制) | 條件式走訪、需要完全自訂控制流 |
for | 無 | 可(break/continue/return) | 否(自行控制) | 需要索引的經典遍歷 |
forEach | undefined | 否(return僅等同continue) | 否 | 對每個元素執行副作用 |
map | 新陣列(等長) | 否 | 否 | 元素逐一轉換 |
reduce | 單一值(任意型別) | 否(可自行設計提早結束) | 否 | 聚合、累加、組合多個 reducer |
filter | 新陣列(子集) | 否 | 否 | 依條件篩選元素 |
every | 布林值 | 可(回呼回傳false時短路) | 否 | 驗證所有元素滿足條件 |
some | 布林值 | 可(回呼回傳true時短路) | 否 | 驗證至少一個元素滿足條件 |
選型時可以依三個問題快速定位:
- 需要回傳什麼?需要新陣列選
map/filter,需要單一結果選reduce,需要真假值選every/some,只要副作用選forEach,需要索引或完全控制流程選while/for。 - 需要中途跳出嗎?宣告式寫法中,
some/every是唯一能「短路」的高階方法;若條件複雜,直接回到for搭配break更清晰。 - 是否要保留原陣列?上述高階方法皆不修改原陣列,適合函數式風格;
while/for是否修改完全取決於迴圈體。
結語
jstips 第 79 號技巧示範了 JavaScript 遍歷陣列的完整光譜:從掌控力最強、程式碼最繁瑣的while/for,到語意明確、易讀易維護的forEach/map/reduce/filter/every/some。搭配倉庫內第 58 號技巧的短路遍歷、第 55 號技巧的模數無限循環、第 48 號技巧的reduce進階組合,你已經具備在各種遍歷需求下選擇正確工具的能力。核心心法是:優先選用語意最貼近需求的高階方法,讓程式碼自己說明意圖;只有在需要break提前跳出、或需要細粒度索引控制時,才回到傳統迴圈。
- 教程
【免费下载链接】jstips
This is about useful JS tips!
相关推荐
HuLa 发布流程中的生产配置注入:GitHub Actions Secrets 与 production.yaml 实战指南
HuLa 发布流程中的生产配置注入:GitHub Actions Secrets 与 production.yaml 实战指南 本文档以 HuLa 仓库 doc
教程jstips 第 58 期:用 .some() 与 .every() 在 JavaScript 函数式遍历中实现 break / continue
jstips 第 58 期:用 .some 与 .every 在 JavaScript 函数式遍历中实现 break / continue 在 JavaScri
教程Functional-Light JavaScript 第 9 章解读:用 map / filter / reduce 驾驭列表操作
Functional Light JavaScript 第 9 章解读:用 map / filter / reduce 驾驭列表操作 导读 :本文深入剖析《Fu
教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考