跟我學(xué)習(xí)javascript的循環(huán)
1、優(yōu)先使用數(shù)組而不是Object類型來表示有順序的集合
ECMAScript標(biāo)準(zhǔn)并沒有規(guī)定對JavaScript的Object類型中的屬性的存儲順序。
但是在使用for..in循環(huán)對Object中的屬性進行遍歷的時候,確實是需要依賴于某種順序的。正因為ECMAScript沒有對這個順序進行明確地規(guī)范,所以每個JavaScript執(zhí)行引擎都能夠根據(jù)自身的特點進行實現(xiàn),那么在不同的執(zhí)行環(huán)境中就不能保證for..in循環(huán)的行為一致性了。
比如,以下代碼在調(diào)用report方法時的結(jié)果就是不確定的:
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 }]);
// ?
如果你確實需要保證運行的結(jié)果是建立在數(shù)據(jù)的順序上,優(yōu)先使用數(shù)組類型來表示數(shù)據(jù),而不是直接使用Object類型。同時,也盡量避免使用for..in循環(huán),而使用顯式的for循環(huán):
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"
另一個特別依賴于順序的行為是浮點數(shù)的計算:
var ratings = {
"Good Will Hunting": 0.8,
"Mystic River": 0.7,
"21": 0.6,
"Doubt": 0.9
};
在Item 2中,談到了浮點數(shù)的加法操作甚至不能滿足交換律:
(0.1 + 0.2) + 0.3 的結(jié)果和 0.1 + (0.2 + 0.3)的結(jié)果分別是
0.600000000000001 和 0.6
所以對于浮點數(shù)的算術(shù)操作,更加不能使用任意的順序了:
var total = 0, count = 0;
for (var key in ratings) { // unpredictable order
total += ratings[key];
count++;
}
total /= count;
total; // ?
當(dāng)for..in的遍歷順序不一樣時,最后得到的total結(jié)果也就不一樣了,以下是兩種計算順序和其對應(yīng)的結(jié)果:
(0.8 + 0.7 + 0.6 +0.9) / 4 // 0.75 (0.6 + 0.8 + 0.7 +0.9) / 4 // 0.7499999999999999
當(dāng)然,對于浮點數(shù)的計算這一類問題,有一個解決方案是使用整型數(shù)來表示,比如我們將上面的浮點數(shù)首先放大10倍變成整型數(shù)據(jù),然后計算結(jié)束之后再縮小10倍:
(8+ 7 + 6 + 9) / 4 / 10 // 0.75 (6+ 8 + 7 + 9) / 4 / 10 // 0.75
2、絕不要向Object.prototype中添加可列舉的(Enumerable)屬性
如果你的代碼中依賴于for..in循環(huán)來遍歷Object類型中的屬性的話,不要向Object.prototype中添加任何可列舉的屬性。
但是在對JavaScript執(zhí)行環(huán)境進行增強的時候,往往都需要向Object.prototype對象添加新的屬性或者方法。比如可以添加一個方法用于得到某個對象中的所有的屬性名:
Object.prototype.allKeys = function() {
var result = [];
for (var key in this) {
result.push(key);
}
return result;
};
但是結(jié)果是下面這個樣子的:
({ a: 1, b: 2, c: 3}).allKeys(); // ["allKeys", "a", "b","c"]
一個可行的解決方案是使用函數(shù)而不是在Object.prototype上定義新的方法:
function allKeys(obj) {
var result = [];
for (var key in obj) {
result.push(key);
}
return result;
}
但是如果你確實需要向Object.prototype上添加新的屬性,同時也不希望該屬性在for..in循環(huán)中被遍歷到,那么可以利用ES5環(huán)境提供的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
});
以上代碼的關(guān)鍵部分就是將enumerable屬性設(shè)置為false。這樣的話,在for..in循環(huán)中就無法遍歷該屬性了。
3、對于數(shù)組遍歷,優(yōu)先使用for循環(huán),而不是for..in循環(huán)
雖然上個Item已經(jīng)說過這個問題,但是對于下面這段代碼,能看出最后的平均數(shù)是多少嗎?
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; // ?
通過計算,最后的結(jié)果應(yīng)該是88。
但是不要忘了在for..in循環(huán)中,被遍歷的永遠是key,而不是value,對于數(shù)組同樣如此。因此上述for..in循環(huán)中的score并不是期望的98, 74等一系列值,而是0, 1等一系列索引。
所以你也許會認(rèn)為最后的結(jié)果是:
(0 + 1+ …+ 6) / 7 = 21
但是這個答案也是錯的。另外一個關(guān)鍵點在于,for..in循環(huán)中key的類型永遠都是字符串類型,因此這里的+操作符執(zhí)行的實際上是字符串的拼接操作:
最后得到的total實際上是字符串00123456。這個字符串轉(zhuǎn)換成數(shù)值類型后的值是123456,然后再將它除以元素的個數(shù)7,就得到了最后的結(jié)果:17636.571428571428
所以,對于數(shù)組遍歷,還是使用標(biāo)準(zhǔn)的for循環(huán)最好
4、優(yōu)先使用遍歷方法而非循環(huán)
在使用循環(huán)的時候,很容易違反DRY(Don't Repeat Yourself)原則。這是因為我們通常會選擇復(fù)制粘貼的方法來避免手寫一段段的循環(huán)語句。但是這樣做回讓代碼中出現(xiàn)大量重復(fù)代碼,開發(fā)人員也在沒有意義地”重復(fù)造輪子”。更重要的是,在復(fù)制粘貼的時候很容易忽視循環(huán)中的那些細節(jié),比如起始索引值,終止判斷條件等。
比如以下的for循環(huán)就存在這個問題,假設(shè)n是集合對象的長度:
for (var i = 0; i <= n; i++) { ... }
// 終止條件錯誤,應(yīng)該是i < n
for (var i = 1; i < n; i++) { ... }
// 起始變量錯誤,應(yīng)該是i = 0
for (var i = n; i >= 0; i--) { ... }
// 起始變量錯誤,應(yīng)該是i = n - 1
for (var i = n - 1; i > 0; i--) { ... }
// 終止條件錯誤,應(yīng)該是i >= 0
可見在循環(huán)的一些細節(jié)處理上很容易出錯。而利用JavaScript提供的閉包(參見Item 11),可以將循環(huán)的細節(jié)給封裝起來供重用。實際上,ES5就提供了一些方法來處理這一問題。其中的Array.prototype.forEach是最簡單的一個。利用它,我們可以將循環(huán)這樣寫:
// 使用for循環(huán)
for (var i = 0, n = players.length; i < n; i++) {
players[i].score++;
}
// 使用forEach
players.forEach(function(p) {
p.score++;
});
除了對集合對象進行遍歷之外,另一種常見的模式是對原集合中的每個元素進行某種操作,然后得到一個新的集合,我們也可以利用forEach方法實現(xiàn)如下:
// 使用for循環(huán)
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());
});
但是由于這種由將一個集合轉(zhuǎn)換為另一個集合的模式十分常見,ES5也提供了Array.prototype.map方法用來讓代碼更加簡單和優(yōu)雅:
var trimmed = input.map(function(s) {
return s.trim();
});
另外,還有一種常見模式是對集合根據(jù)某種條件進行過濾,然后得到一個原集合的子集。ES5中提供了Array.prototype.filter來實現(xiàn)這一模式。該方法接受一個Predicate作為參數(shù),它是一個返回true或者false的函數(shù):返回true意味著該元素會被保留在新的集合中;返回false則意味著該元素不會出現(xiàn)在新集合中。比如,我們使用以下代碼來對商品的價格進行過濾,僅保留價格在[min, max]區(qū)間的商品:
listings.filter(function(listing) {
return listing.price >= min && listing.price <= max;
});
當(dāng)然,以上的方法是在支持ES5的環(huán)境中可用的。在其它環(huán)境中,我們有兩種選擇: 1. 使用第三方庫,如underscore或者lodash,它們都提供了相當(dāng)多的通用方法來操作對象和集合。 2. 根據(jù)需要自行定義。
比如,定義如下的方法來根據(jù)某個條件取得集合中前面的若干元素:
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]
只有一個場合使用循環(huán)會比使用遍歷函數(shù)要好:需要使用break和continue的時候。 比如,當(dāng)使用forEach來實現(xiàn)上面的takeWhile方法時就會有問題,在不滿足predicate的時候應(yīng)該如何實現(xiàn)呢?
function takeWhile(a, pred) {
var result = [];
a.forEach(function(x, i) {
if (!pred(x)) {
// ?
}
result[i] = x;
});
return result;
}
我們可以使用一個內(nèi)部的異常來進行判斷,但是它同樣有些笨拙和低效:
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方法用來處理存在提前終止的循環(huán),它們的用法如下所示:
[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就可以實現(xiàn)如下:
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;
}
實際上,這就是函數(shù)式編程的思想。在函數(shù)式編程中,你很少能夠看見顯式的for循環(huán)或者while循環(huán)。循環(huán)的細節(jié)都被很好地封裝起來了。
5、總結(jié)
- 在使用for..in循環(huán)時,不要依賴于遍歷的順序。
- 當(dāng)使用Object類型來保存數(shù)據(jù)時,需要保證其中的數(shù)據(jù)是無序的。
- 當(dāng)需要表示帶有順序的集合時,使用數(shù)組類型而不是Object類型。
- 避免向Object.prototype中添加任何屬性。
- 如果確實有必要向Object.prototype中添加方法屬性,可以考慮使用獨立函數(shù)替代。
- 使用Object.defineProperty來添加可以不被for..in循環(huán)遍歷到的屬性。
- 當(dāng)遍歷數(shù)組時,使用標(biāo)準(zhǔn)的for循環(huán),而不要使用for..in循環(huán)。
- 在必要的場合考慮預(yù)先保存數(shù)組的長度,以提高性能。
- 使用遍歷方法Array.prototype.forEach和Array.prototype.map來代替循環(huán),從而讓代碼更加清晰可讀。
- 對于重復(fù)出現(xiàn)的循環(huán),可以考慮將它們進行抽象。通過第三方提供的方法或者自己實現(xiàn)。
- 顯式的循環(huán)在一些場合下還是有用武之地的,相應(yīng)的也可以使用some或者every方法。
以上就是本文的全部內(nèi)容,希望通過這篇文章大家更加了解javascript循環(huán)的原理,大家共同進步。
- javascript下for循環(huán)用法小結(jié)
- javascript之循環(huán)停頓上下滾動
- javascript中利用數(shù)組實現(xiàn)的循環(huán)隊列代碼
- javascript 循環(huán)讀取JSON數(shù)據(jù)的代碼
- javascript forEach通用循環(huán)遍歷方法
- javascript 循環(huán)語句 while、do-while、for-in、for用法區(qū)別
- Javascript中的for in循環(huán)和hasOwnProperty結(jié)合使用
- JavaScript的遞歸之遞歸與循環(huán)示例介紹
- JavaScript中for..in循環(huán)陷阱介紹
- javascript中的循環(huán)語句for語句深入理解
- javascript閉包傳參和事件的循環(huán)綁定示例探討
- JavaScript中對循環(huán)語句的優(yōu)化技巧深入探討
- javascript使用for循環(huán)批量注冊的事件不能正確獲取索引值的解決方法
- javascript實現(xiàn)圖片循環(huán)漸顯播放的方法
- 簡單學(xué)習(xí)JavaScript中的for語句循環(huán)結(jié)構(gòu)
- JavaScript中利用各種循環(huán)進行遍歷的方式總結(jié)
- 跟我學(xué)習(xí)javascript的for循環(huán)和for...in循環(huán)
相關(guān)文章
JavaScript返回網(wǎng)頁中超鏈接數(shù)量的方法
這篇文章主要介紹了JavaScript返回網(wǎng)頁中超鏈接數(shù)量的方法,使用javascript中的document.links實現(xiàn)這一功能,需要的朋友可以參考下2015-04-04
JS實現(xiàn)對json對象排序并刪除id相同項功能示例
這篇文章主要介紹了JS實現(xiàn)對json對象排序并刪除id相同項功能,涉及javascript針對json格式數(shù)據(jù)的遍歷、運算、判斷、添加、刪除等相關(guān)操作技巧,需要的朋友可以參考下2018-04-04
JavaScript實現(xiàn)數(shù)組隨機排序的方法
這篇文章主要介紹了JavaScript實現(xiàn)數(shù)組隨機排序的方法,涉及javascript數(shù)組遍歷與排序的相關(guān)技巧,需要的朋友可以參考下2015-06-06
Jquery Autocomplete 結(jié)合asp.net使用要點
Jquery的Autocomplete是一個很好的智能提示插件,但是在實際使用過程中還是會遇到一些小問題.2010-10-10

