優先使用遍歷方法而非循環
在使用迴圈的時候,很容易違反DRY(Don't Repeat Yourself)原則。這是因為我們通常會選擇複製貼上的方法來避免手寫一段段落的循環語句。但是這樣做回讓程式碼中出現大量重複程式碼,開發人員也在沒有意義地」重複造輪子」。更重要的是,複製貼上的時候很容易忽略循環中的那些細節,例如起始索引值,終止判斷條件等。
例如以下的for迴圈就存在這個問題,假設n是集合物件的長度:
for (var i = 0; i <= n; i++) { ... } // 终止条件错误,应该是i < n for (var i = 1; i < n; i++) { ... } // 起始变量错误,应该是i = 0 for (var i = n; i >= 0; i--) { ... } // 起始变量错误,应该是i = n - 1 for (var i = n - 1; i > 0; i--) { ... } // 终止条件错误,应该是i >= 0
可見在迴圈的一些細節處理上很容易出錯。而利用JavaScript提供的閉包(參見Item 11),可以將循環的細節給封裝起來供重用。實際上,ES5就提供了一些方法來處理這個問題。其中的Array.prototype.forEach是最簡單的一個。利用它,我們可以將循環這樣寫:
// 使用for循环 for (var i = 0, n = players.length; i < n; i++) { players[i].score++; } // 使用forEach players.forEach(function(p) { p.score++; });
除了對集合物件進行遍歷之外,另一種常見的模式是對原始集合中的每個元素進行某種操作,然後得到一個新的集合,我們也可以利用forEach方法實現如下:
// 使用for循环 var trimmed = []; for (var i = 0, n = input.length; i < n; i++) { trimmed.push(input[i].trim()); } // 使用forEach var trimmed = []; input.forEach(function(s) { trimmed.push(s.trim()); });
但是由於這個由將一個集合轉換為另一個集合的模式十分常見,ES5也提供了Array.prototype.map方法用來讓程式碼更簡單、更優雅:
var trimmed = input.map(function(s) { return s.trim(); });
另外,還有一個常見模式是對集合根據某種條件進行過濾,然後得到一個原集合的子集。 ES5中提供了Array.prototype.filter來實現此模式。此方法接受一個Predicate作為參數,它是一個傳回true或false的函數:傳回true表示該元素會保留在新的集合中;傳回false則表示該元素不會出現在新集合中。例如,我們使用以下程式碼來對商品的價格進行過濾,僅保留價格在[min, max]區間的商品:
listings.filter(function(listing) { return listing.price >= min && listing.price <= max; });
當然,以上的方法是在支援ES5的環境中可用的。在其它環境中,我們有兩種選擇: 1. 使用第三方函式庫,如underscore或lodash,它們都提供了相當多的通用方法來操作物件和集合。 2. 根據需要自行定義。
例如,定義如下的方法來根據某個條件取得集合中前面的若干元素:
function takeWhile(a, pred) { var result = []; for (var i = 0, n = a.length; i < n; i++) { if (!pred(a[i], i)) { break; } result[i] = a[i]; } return result; } var prefix = takeWhile([1, 2, 4, 8, 16, 32], function(n) { return n < 10; }); // [1, 2, 4, 8]
為了更好的重用該方法,我們可以將它定義在Array.prototype物件上,具體的影響可以參考Item 42。
Array.prototype.takeWhile = function(pred) { var result = []; for (var i = 0, n = this.length; i < n; i++) { if (!pred(this[i], i)) { break; } result[i] = this[i]; } return result; }; var prefix = [1, 2, 4, 8, 16, 32].takeWhile(function(n) { return n < 10; }); // [1, 2, 4, 8]
只有一個場合使用迴圈會比使用遍歷函數好:需要使用break和continue的時候。 例如,當使用forEach來實作上面的takeWhile方法時就會有問題,在不滿足predicate的時候該如何實作呢?
function takeWhile(a, pred) { var result = []; a.forEach(function(x, i) { if (!pred(x)) { // ? } result[i] = x; }); return result; }
我們可以使用一個內部的異常來進行判斷,但是它同樣有些笨拙和低效:
function takeWhile(a, pred) { var result = []; var earlyExit = {}; // unique value signaling loop break try { a.forEach(function(x, i) { if (!pred(x)) { throw earlyExit; } result[i] = x; }); } catch (e) { if (e !== earlyExit) { // only catch earlyExit throw e; } } return result; }
可是使用forEach之後,程式碼甚至比使用它之前更加冗長。這顯然是存在問題的。 對於這個問題,ES5提供了some和every方法用來處理存在提前終止的循環,它們的用法如下所示:
[1, 10, 100].some(function(x) { return x > 5; }); // true [1, 10, 100].some(function(x) { return x < 0; }); // false [1, 2, 3, 4, 5].every(function(x) { return x > 0; }); // true [1, 2, 3, 4, 5].every(function(x) { return x < 3; }); // false
這兩個方法都是短路方法(Short-circuiting):只要有任何一個元素在some方法的predicate中回傳true,那麼some就會回傳;只有有任何一個元素在every方法的predicate中傳回false,那麼every方法也會回傳false。
因此,takeWhile就可以實現如下:
function takeWhile(a, pred) { var result = []; a.every(function(x, i) { if (!pred(x)) { return false; // break } result[i] = x; return true; // continue }); return result; }
實際上,這就是函數式程式設計的想法。在函數式程式設計中,你很少能夠看見明確的for迴圈或while迴圈。循環的細節都被很好地封裝起來了。
以上是javascript中為何優先使用遍歷方法而非循環程式碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!