1、優先使用數組而不是Object類型來表示有順序的集合
ECMAScript標准並沒有規定對JavaScript的Object類型中的屬性的存儲順序。
但是在使用for..in循環對Object中的屬性進行遍歷的時候,確實是需要依賴於某種順序的。正因為ECMAScript沒有對這個順序進行明確地規范,所以每個JavaScript執行引擎都能夠根據自身的特點進行實現,那麼在不同的執行環境中就不能保證for..in循環的行為一致性了。
比如,以下代碼在調用report方法時的結果就是不確定的:
function report(highScores) { var result = ""; var i = 1; for (var name in highScores) { // unpredictable order result += i + ". " + name + ": " + highScores[name] + "\n"; i++; } return result; } report([{ name: "Hank", points: 1110100 }, { name: "Steve", points: 1064500 }, { name: "Billy", points: 1050200 }]); // ?
如果你確實需要保證運行的結果是建立在數據的順序上,優先使用數組類型來表示數據,而不是直接使用Object類型。同時,也盡量避免使用for..in循環,而使用顯式的for循環:
function report(highScores) { var result = ""; for (var i = 0, n = highScores.length; i < n; i++) { var score = highScores[i]; result += (i + 1) + ". " + score.name + ": " + score.points + "\n"; } return result; } report([{ name: "Hank", points: 1110100 }, { name: "Steve", points: 1064500 }, { name: "Billy", points: 1050200 }]); // "1. Hank: 1110100 2. Steve: 1064500 3. Billy: 1050200\n"
另一個特別依賴於順序的行為是浮點數的計算:
var ratings = { "Good Will Hunting": 0.8, "Mystic River": 0.7, "21": 0.6, "Doubt": 0.9 };
在Item 2中,談到了浮點數的加法操作甚至不能滿足交換律:
(0.1 + 0.2) + 0.3 的結果和 0.1 + (0.2 + 0.3)的結果分別是
0.600000000000001 和 0.6
所以對於浮點數的算術操作,更加不能使用任意的順序了:
var total = 0, count = 0; for (var key in ratings) { // unpredictable order total += ratings[key]; count++; } total /= count; total; // ?
當for..in的遍歷順序不一樣時,最後得到的total結果也就不一樣了,以下是兩種計算順序和其對應的結果:
(0.8 + 0.7 + 0.6 +0.9) / 4 // 0.75 (0.6 + 0.8 + 0.7 +0.9) / 4 // 0.7499999999999999
當然,對於浮點數的計算這一類問題,有一個解決方案是使用整型數來表示,比如我們將上面的浮點數首先放大10倍變成整型數據,然後計算結束之後再縮小10倍:
(8+ 7 + 6 + 9) / 4 / 10 // 0.75 (6+ 8 + 7 + 9) / 4 / 10 // 0.75
2、絕不要向Object.prototype中添加可列舉的(Enumerable)屬性
如果你的代碼中依賴於for..in循環來遍歷Object類型中的屬性的話,不要向Object.prototype中添加任何可列舉的屬性。
但是在對JavaScript執行環境進行增強的時候,往往都需要向Object.prototype對象添加新的屬性或者方法。比如可以添加一個方法用於得到某個對象中的所有的屬性名:
Object.prototype.allKeys = function() { var result = []; for (var key in this) { result.push(key); } return result; };
但是結果是下面這個樣子的:
({ a: 1, b: 2, c: 3}).allKeys(); // ["allKeys", "a", "b","c"]
一個可行的解決方案是使用函數而不是在Object.prototype上定義新的方法:
function allKeys(obj) { var result = []; for (var key in obj) { result.push(key); } return result; }
但是如果你確實需要向Object.prototype上添加新的屬性,同時也不希望該屬性在for..in循環中被遍歷到,那麼可以利用ES5環境提供的Object.defineProject方法:
Object.defineProperty(Object.prototype, "allKeys", { value: function() { var result = []; for (var key in this) { result.push(key); } return result; }, writable: true, enumerable: false, configurable: true });
以上代碼的關鍵部分就是將enumerable屬性設置為false。這樣的話,在for..in循環中就無法遍歷該屬性了。
3、對於數組遍歷,優先使用for循環,而不是for..in循環
雖然上個Item已經說過這個問題,但是對於下面這段代碼,能看出最後的平均數是多少嗎?
var scores = [98, 74, 85, 77, 93, 100, 89]; var total = 0; for (var score in scores) { total += score; } var mean = total / scores.length; mean; // ?
通過計算,最後的結果應該是88。
但是不要忘了在for..in循環中,被遍歷的永遠是key,而不是value,對於數組同樣如此。因此上述for..in循環中的score並不是期望的98, 74等一系列值,而是0, 1等一系列索引。
所以你也許會認為最後的結果是:
(0 + 1+ …+ 6) / 7 = 21
但是這個答案也是錯的。另外一個關鍵點在於,for..in循環中key的類型永遠都是字符串類型,因此這裡的+操作符執行的實際上是字符串的拼接操作:
最後得到的total實際上是字符串00123456。這個字符串轉換成數值類型後的值是123456,然後再將它除以元素的個數7,就得到了最後的結果:17636.571428571428
所以,對於數組遍歷,還是使用標准的for循環最好
4、優先使用遍歷方法而非循環
在使用循環的時候,很容易違反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循環。循環的細節都被很好地封裝起來了。
5、總結
以上就是本文的全部內容,希望通過這篇文章大家更加了解javascript循環的原理,大家共同進步。