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循环的原理,大家共同进步。