關於數組或對象遍歷,相信不少人都沒有深刻觀察過執行效率。這是一個曾在羣裏吵翻天的話題,讀懂後你將成爲遍歷效率話題的大師。javascript
遍歷數組或對象是一名程序員的基本素養之一. 然而遍歷卻不是一件簡單的事, 優秀的程序員知道怎麼去選擇合適的遍歷方法, 優化遍歷效率. 本篇將帶你走進JavaScript遍歷的世界, 享受分析JS循環的快感. 本篇全部代碼均可以直接運行, 但願您通讀本篇後, 不止是瀏覽, 最好是親手去實踐下.php
js有以下兩種數據須要常常遍歷html
同時又提供了以下8種方法方便咱們遍歷元素前端
最終咱們將分析遍歷效率選出最佳遍歷選手.java
本文將針對以下兩種數據進行詳細的分析和舉慄. 下面舉慄中若是不加特殊說明將會用到以下數據.jquery
var array = ["囚徒","過客","領袖"];//職場3種人
var o = {0:"linda",1:"style",2:"nick",length:3};複製代碼
語法: for(初始化; 循環執行條件; 每遍歷一個元素後作的事情;){}git
(function(){//循環置於閉包以內
for(var i=0,length=array.length;i<length;i++){//緩存數組長度
console.log(array[i]);//內部方法如有可能相互影響,也要置於閉包以內
}
})();複製代碼
for循環只能遍歷數組, 不能遍歷對象. 寫for循環時有兩點須要注意.程序員
語法: do{...}while(true);github
do while
(function() {
var i = 0,
len = array.length;
do {
if (i == 2) {
break; // 循環被終止, 此處若是是continue就會形成循環沒法退出
};
console.log('array['+ i +']:' + array[i]);
i++;//此句建議放置循環while頭部
} while(i<len);
})();複製代碼
do/while的語法簡化了循環的實現, 只保留對循環條件的判斷, 因此咱們要在循環內部構造出循環退出的條件, 不然有可能形成死循環. 特別要注意的是: 使用 continue 跳出本次遍歷時, 要保證循環可以自動進入到下一次遍歷, 所以保證循環走到下一次遍歷的語句須要放到 continue 前面執行, 建議置於循環頭部.(如上, i++ 語句最好放置循環頭部)chrome
do/while 循環與for循環大致差很少,只支持數組遍歷, 多用於對循環退出條件不是很明確的場景. 通常來講不建議使用這種方式遍歷數組.
語法: array.forEach(function(item){}), 參數item表示數組每一項的元素
array.forEach(function(item){
if(item=="囚徒")
return;//這裏只能使用return跳過當前元素處理
console.log(item);
});複製代碼
forEach回調function默認有三個參數: item, index, array.
使用forEach循環有幾點須要特別注意:
語法: for(var item in array){}
for(var item in array){
console.log(item);
}//0 1 2
for(var item in o){
console.log(item);
}//0 1 2 length複製代碼
for in 可用於遍歷數組和對象, 但它輸出的只是數組的索引和對象的key, 咱們能夠經過索引和key取到對應的值. 以下:
for(var item in array){
console.log(array[item]);
}//"囚徒" "過客" "領袖"
for(var item in o){
console.log(o[item]);
}//"linda" "style" "nick" "length"複製代碼
語法: $.each(array|o, function(i, ele){}) 支持數組和對象
$.each(array, function(i, ele){
console.log(i,ele,this==ele);
});
//0 "囚徒" true
//1 "過客" true
//2 "領袖" true
$.each(o, function(i, ele){
console.log(i,ele,this==ele);
});
//0 "linda" true
//1 "style" true
//2 "nick" true複製代碼
這裏咱們注意到 this對象 指向當前屬性的值,這是由於:
參考jQuery api:
$.each()
方法會迭代jQuery對象中的每個DOM元素。每次回調函數執行時,會傳遞當前循環次數做爲參數(從0開始計數)。更重要的是,回調函數是在當前DOM元素爲上下文的語境中觸發的。所以關鍵字this
老是指向這個元素。
同時,上述遍歷時, o 對象的屬性中有一個length屬性並無被輸出. 這是爲何呢? 請耐心往下看.
首先, 咱們來看看遍歷對象o時, 當前的this對象究竟是什麼?
$.each(o, function(i, ele){
if(this=="linda"){//咱們隨機選取第一個屬性
console.log(this,this==ele);
$.each(this, function(e, ele2) {
console.log(e, ele2);
});
}
});
//String {0: "l", 1: "i", 2: "n", 3: "d", 4: "a", length: 5, [[PrimitiveValue]]: "linda"} true
//0 "l"
//1 "i"
//2 "n"
//3 "d"
//4 "a"複製代碼
咱們發現, this對象等於回調函數的第二個形參. 且它的 length 屬性和 [[PrimitiveValue]] 屬性並無被打印出來, 爲此咱們來查看下length的內部屬性.
$.each(o, function(i, ele){
if(this=="linda")//咱們仍是隨機選取第一個屬性(這仍是隨機嗎?)
console.log(Object.getOwnPropertyDescriptor(this, 'length'));
});
//Object {value: 5, writable: false, enumerable: false, configurable: false}複製代碼
可見, this對象的length屬性的 enumerable 屬性被設置成了false, 這表示該對象不能被列舉或遍歷, 同時還不能被配置(configurable: false), 也不能被賦值(writable: false).
此時, 前面遍歷 o 對象時,它的 length 屬性沒有被打印出來的疑問彷佛有解了. 讓咱們來看看 o.length 的內部屬性吧.
console.log(Object.getOwnPropertyDescriptor(o, 'length'));
//Object {value: 3, writable: true, enumerable: true, configurable: true}複製代碼
o.length 值爲3, 可賦值, 可列舉, 可配置. 這可不對, 剛剛不是說 enumerable 屬性被設置成了false 纔不會被遍歷嗎. 如今該值爲 true, 而且還不可遍歷. 這不合常理, 天然該有別的緣由. 咱們接着往下看.
var o = {0:"linda",1:"style",2:"nick",length:1}; // 試着改變length的值
$.each(o, function(i, ele){//再遍歷一次
console.log(i,ele);
});
//0 "linda"
var o = {0:"linda",1:"style",2:"nick",length:5}; // 堅持改變length的值
$.each(o, function(i, ele){//再遍歷一次
console.log(i,ele);
});
// 0 linda
// 1 style
// 2 nick
// length 5
var o = {0:"linda",1:"style",2:"nick"}; // 試試去掉length屬性
$.each(o, function(i, ele){//再遍歷一次
console.log(i,ele);
});
// 0 linda
// 1 style
// 2 nick複製代碼
現象明瞭, 結合jquery源碼, 當對象中存在length屬性時, $.each 內部使用for循環去遍歷對象, 不然它將使用for in循環去遍歷, 所以$.each遍歷對象遵循以下規律:
不只如此, $.each的具體使用過程當中還有如下幾點須要注意:
return
或者 return true
爲跳過一個元素,繼續執行後面的循環;return false
爲終止循環的執行, 這是由於在 jquery.each 中, 若返回值指定爲false, 才跳出循環, 若是感興趣請翻看 jquery.each 源碼;語法: $(selecter|array|o).each(function(i, ele){}) 支持數組和對象, 該方法基本上與$.each方法相同.
$('div').each(function(i,ele){
console.log(this,i,this == ele);
});
//dom... 0 dom.... true
$(array).each(function(i,ele){//處理數組
if(this == "領袖")
console.log(this,i,this == ele);
});
//String {0: "領", 1: "袖", length: 2, [[PrimitiveValue]]: "領袖"} 2 true
$(o).each(function(i,ele){//處理對象
if(this == "nick")
console.log(this,i,this == ele);
});
//String {0: "n", 1: "i", 2: "c", 3: "k", length: 4, [[PrimitiveValue]]: "nick"} 2 true複製代碼
dom表示div元素, 因爲this恆等ele, 說明this也表示div元素, 因此this並非jquery對象, 而是普通的DOM對象
(能夠在this上隨意使用DOM方法). 使用$(selecter).each方法,請注意如下幾點:
即 Array.prototype.map,該方法只支持數組
語法: array.map(callback[,thisArg]) map方法使用其提供函數的每次返回結果生成一個新的數組.
var array = [1, 4, 9];
var roots = array.map(Math.sqrt);//map包裹方法名
// roots is now [1, 2, 3], array is still [1, 4, 9]
var array = [1, 4, 9];
var doubles = array.map(function(num) {//map包裹方法實體
return num * 2;
});
// doubles is now [2, 8, 18]. array is still [1, 4, 9]複製代碼
實際上,因爲map方法被設計成支持 [鴨式辨型
][] , 該方法也能夠用來處理形似數組的對象, 例如 NodeList.
var elems = document.querySelectorAll('select option:checked');
var values = Array.prototype.map.call(elems, function(obj) {
return obj.value;
});複製代碼
甚至還能夠用來處理字符串, 以下:
var map = Array.prototype.map;
var array = map.call('Hello 中國', function(x) {
return x.charCodeAt(0);
});
console.log(array);
//[72, 101, 108, 108, 111, 32, 20013, 22269]複製代碼
map處理字符串的方式多種多樣, 例如 反轉等.
var str = '12345';
var output = Array.prototype.map.call(str, function(x) {
return x;
}).reverse().join('');
console.log(output);//54321複製代碼
例如 將字符串數組轉換爲數字數組, 只需一條語句, 以下:
console.log(['1', '2', '3'].map(Number));//[1,2,3]複製代碼
目前map方法被大部分瀏覽器支持, 除了IE 6,7,8.
即 Array.prototype.every, 該方法同上述map方法也只支持數組
語法: arr.every(callback[, thisArg]) every 方法用於檢驗數組中的每一項是否符合某個條件, 若符合則放回true, 反之則返回false.
function isBigEnough(element, index, array) {
return element >= 10;
}
[12, 5, 8, 130, 44].every(isBigEnough); // false
[12, 54, 18, 130, 44].every(isBigEnough); // true複製代碼
該方法還有簡寫方式, 以下:
[12, 5, 8, 130, 44].every(elem => elem >= 10); // false
[12, 54, 18, 130, 44].every(elem => elem >= 10); // true複製代碼
以上, 遍歷數組和對象的8種方法簡單的介紹完, 小結以下:
下面咱們來測試下上述方法的效率.
注: array數組默認爲空, 依次賦值數組長度爲1 000 000, 10 000 000, 100 000 000, 分別在 Chrome, Firefox, Safari 瀏覽器上進行兩輪測試, 取測試時間平均值做爲比較對象, 時間單位爲ms. 以下是測試代碼:
var array = [],
length = array.length = 10000000;//(一千萬)
//for(var i=0;i<length;i++){
// array[i] = 'louis';
//}
console.log(array[0]);
//-------------------------for
var t1 = +new Date();
for(var i=0;i<length;i++){
}
var t2 = +new Date();
console.log('for:' + (t2-t1));
//-------------------------do/while
var t1 = +new Date();
var i = 0;
do {
i++;
} while(i<length);
var t2 = +new Date();
console.log('do while:' + (t2-t1));
//-------------------------forEach
var t1 = +new Date();
array.forEach(function(item){
});
var t2 = +new Date();
console.log('forEach:' + (t2-t1));
//-------------------------for in
var t1 = +new Date();
for(var item in array){
}
var t2 = +new Date();
console.log('for in:' + (t2-t1));
//------------------------- $.each
var t1 = +new Date();
$.each(array, function(i, ele){
});
var t2 = +new Date();
console.log('$.each:' + (t2-t1));
//-------------------------$().each
var t1 = +new Date();
$(array).each(function(i,ele){
});
var t2 = +new Date();
console.log('$(ele).each:' + (t2-t1));
//-------------------------map
var t1 = +new Date();
array.map(function(num){
});
var t2 = +new Date();
console.log('map:' + (t2-t1));
//-------------------------every
var t1 = +new Date();
array.every(function(e,i,arr){
});
var t2 = +new Date();
console.log('every:' + (t2-t1));複製代碼
測試機器正常運行 IDE, 編輯器, 瀏覽器, qq, 微信等經常使用應用, 系統空閒. 硬件設備以下:
以上多輪測試結果彙總以下三張表(單位:ms):
數組長度爲10^6 | chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|---|
for | (16+19)/2 = 17.5 | (6+7)/2 = 6.5 | (6+7)/2 = 6.5 |
do while | (24+17)/2 = 20.5 | (7+5)/2 = 6 | (5+5)/2 = 5 |
for in | (19+28)/2 = 23.5 | (0+0)/2 = 0 | (0+0)/2 = 0 |
forEach | (41+28)/2 = 34.5 | (4+4)/2 = 4 | (31+29)/2 = 30 |
map | (26+32)/2 = 28 | (4+4)/2 = 4 | (32+26)/2 = 28 |
every | (22+24)/2 = 23 | (4+5)/2 = 4.5 | (41+45)/2 = 43 |
$.each | (29+27)/2 = 28 | (306+311)/2 = 308.5 | (111+97)/2 = 104 |
$(e).each | (94+98)/2 = 96 | (484+488)/2 = 486 | (79+64)/2 = 71.5 |
數組長度爲10^7 | chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|---|
for | (164+161)/2 = 162.5 | (26+30)/2 = 28 | (30+31)/2 = 30.5 |
do while | (163+157)/2 = 160 | (27+25)/2 = 26 | (28+27)/2 = 27.5 |
for in | (78+86)/2 = 82 | (0+0)/2 = 0 | (0+0)/2 = 0 |
forEach | (211+205)/2 = 208 | (31+30)/2 = 30.5 | (291+289)/2 = 290 |
map | (349+282)/2 = 315.5 | (24+22)/2 = 23 | (259+260)/2 = 259.5 |
every | (221+219)/2 = 220 | (24+24)/2 = 24 | (251+257)/2 = 254 |
$.each | (210+215)/2 = 212.5 | (2868+2789)/2 = 2828.5 | (699+724)/2 = 711.5 |
$(e).each | (730+669)/2 = 699.5 | (4674+4722)/2 = 4698 | (523+546)/2 = 534.5 |
數組長度爲10^8 | chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|---|
for | (1486+1583)/2 = 1534.5 | (222+238)/2 = 230 | (261+251)/2 = 256 |
do while | (1548+1608)/2 = 1578 | (236+247)/2 = 241.5 | (272+265)/2 = 268.5 |
for in | (0+0)/2 = 0 | (0+0)/2 = 0 | (0+0)/2 = 0 |
forEach | (25838+22307)/2 = 24072.5 | (212+209)/2 = 210.5 | (2565+2568)/2 = 2566.5 |
map | (23795+22787)/2 = 23291 | (215+206)/2 = 210.5 | (2556+2573)/2 = 2564.5 |
every | (22393+22378)/2 = 22385.5 | (212+215)/2 = 213.5 | (2550+2548)/2 = 2549 |
$.each | (14523+14776)/2 = 14649.5 | (28007+27698)/2 = 27852.5 | (7109+7156)/2 = 7132.5 |
$(e).each | chrome 奔潰了... | (49352+49530)/2 = 49441 | (5505+4616)/2 = 5060.5 |
綜上, 咱們發現for in 循環的性能不穩定, 猜想它可能沒有進入循環. 所以將數組各元素進行以下賦值. 從新進行以下兩輪測試.
var array = [],
length = array.length = 1000000;
for(var i=0;i<length;i++){
array[i] = 'louis';
}複製代碼
數組長度爲10^6 | chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|---|
for | (21+22)/2 = 21.5 | (8+10)/2 = 9 | (6+5)/2 = 5.5 |
do while | (22+19)/2 = 20.5 | (6+6)/2 = 6 | (6+5)/2 = 5.5 |
for in | (178+184)/2 = 181 | (318+268)/2 = 293 | (413+464)/2 = 438.5 |
forEach | (42+45)/2 = 43.5 | (4+4)/2 = 4 | (21+24)/2 = 22.5 |
map | (137+153)/2 = 145 | (9+8)/2 = 8.5 | (38+43)/2 = 40.5 |
every | (0+0)/2 = 0 | (0+0)/2 = 0 | (0+0)/2 = 0 |
$.each | (85+84)/2 = 84.5 | (15+19)/2 = 17 | (37+25)/2 = 31 |
$(e).each | (81+83)/2 = 82 | (34+31)/2 = 32.5 | (37+46)/2 = 41.5 |
數組長度爲10^7 | chrome 52.0.2743.116 (64-bit) | Firefox Developer Edition 49.0a2 (2016-08-01) | Safari 9.1.1 (11601.6.17) |
---|---|---|---|
for | (171+157)/2 = 164 | (27+26)/2 = 26.5 | (26+28)/2 = 27 |
do while | (168+158)/2 = 163 | (27+27)/2 = 27 | (28+29)/2 = 28.5 |
for in | (1469+1715)/2 = 1592 | (2922+3123)/2 = 3022.5 | (5755+5742)/2 = 5748.5 |
forEach | (347+329)/2 = 338 | (32+36)/2 = 34 | (171+174)/2 = 172.5 |
map | (1320+1335)/2 = 1327.5 | (147+137)/2 = 142 | (448+469)/2 = 458.5 |
every | (0+0)/2 = 0 | (0+0)/2 = 0 | (0+0)/2 = 0 |
$.each | (438+441)/2 = 439.5 | (142+141)/2 = 141.5 | (254+248)/2 = 251 |
$(e).each | (876+935)/2 = 905.5 | (315+328)/2 = 321.5 | (450+402)/2 = 426 |
可見, 對數組進行賦值後, 代碼運行基本穩定.(every還不清楚爲何執行時間爲0.歡迎大神告知緣由.)
經過以上 30 次運行測試(實際上爲了獲得比較穩定的數據, 擯棄了許多異常的測試數據), 咱們發如今數組長度爲10^6, 10^7, 10^8 時, 代碼運行基本穩定. 各方法運行須要的時間大體排序以下:
for ~= do while < forEach ~= map ~= every < $.each < $(e).each < for in
根據統計數據, 可得這8個方法的運行速度大體排序爲:
咱們翻看jquery代碼就會知道, $.each方法內部經過調用for循環來實現, 而$().each是先用jquery包裹數組對象, 而後再調用for循環, 所以後者效率略低於前者.
綜上, 最佳遍歷選手是 for/do while循環, 推薦你們優先考慮使用它. ( Firefox瀏覽器因爲對forEach循環作了底層優化, 效率接近native,不在咱們考慮範圍內 ).
想要進一步優化循環效率, 推薦您閱讀下篇 《詳解JS做用域鏈及閉包》.
聲明: 本文全部數據均爲單機測試, 不免存在偏差, 若是發現本文測試數據不對之處, 歡迎批評斧正.
本文就討論這麼多內容,你們有什麼問題或好的想法歡迎在下方參與留言和評論.
本文做者: louis
本文連接: louiszhai.github.io/2015/12/18/…
參考文章