本文永久連結:github.com/HaoChuan942…
前言
前端開發中經常涉及到陣列的相關操作:去重、過濾、求和、資料二次處理等等。都需要我們對陣列進行迴圈。為了滿足各種需求,JS除了提供最簡單的for
迴圈,在ES6
和後續版本中也新增的諸如:map、filter、some、reduce
等實用的方法。因為各個方法作用不同,簡單的對所有涉及到迴圈的方法進行執行速度比較,是不公平的,也是毫無意義的。那麼我們就針對最單純的以取值為目的的迴圈進行一次效能和效率測試,用肉眼可見的方式,對JS中常見的這些陣列迴圈方式進行一次探討。
從最簡單的for迴圈說起
for
迴圈常見的四種寫法,不囉嗦,直接上程式碼
const persons = ['鄭昊川', '鍾忠', '高曉波', '韋貴鐵', '楊俊', '宋燦']
// 方法一
for (let i = 0; i < persons.length; i++) {
console.log(persons[i])
}
// 方法二
for (let i = 0, len = persons.length; i < len; i++) {
console.log(persons[i])
}
// 方法三
for (let i = 0, person; person = persons[i]; i++) {
console.log(person)
}
// 方法四
for (let i = persons.length; i--;) {
console.log(persons[i])
}
複製程式碼
-
第一種方法是最常見的方式,不解釋。
-
第二種方法是將
persons.length
快取到變數len
中,這樣每次迴圈時就不會再讀取陣列的長度。 -
第三種方式是將取值與判斷合併,通過不停的列舉每一項來迴圈,直到列舉到空值則迴圈結束。執行順序是:
-
第四種方法是倒序迴圈。執行的順序是:
- 第一步:獲取陣列長度,賦值給變數
i
- 第二步:判斷
i
是否大於0並執行i--
- 第三步:執行迴圈體,列印
persons[i]
,此時的i
已經-1
了從後向前,直到
i === 0
為止。這種方式不僅去除了每次迴圈中讀取陣列長度的操作,而且只建立了一個變數i
。
- 第一步:獲取陣列長度,賦值給變數
四種for
迴圈方式在陣列淺拷貝中的效能和速度測試
先造一個足夠長的陣列作為要拷貝的目標(如果i
值過大,到億級左右,可能會丟擲JS堆疊跟蹤的報錯)
const ARR_SIZE = 6666666
const hugeArr = new Array(ARR_SIZE).fill(1)
複製程式碼
然後分別用四種迴圈方式,把陣列中的每一項取出,並新增到一個空陣列中,也就是一次陣列的淺拷貝。並通過console.time和console.timeEnd記錄每種迴圈方式的整體執行時間。通過process.memoryUsage()比對執行前後記憶體中已用到的堆的差值。
/* node環境下記錄方法執行前後記憶體中已用到的堆的差值 */
function heapRecord(fun) {
if (process) {
const startHeap = process.memoryUsage().heapUsed
fun()
const endHeap = process.memoryUsage().heapUsed
const heapDiff = endHeap - startHeap
console.log('已用到的堆的差值: ', heapDiff)
} else {
fun()
}
}
複製程式碼
// 方法一,普通for迴圈
function method1() {
var arrCopy = []
console.time('method1')
for (let i = 0; i < hugeArr.length; i++) {
arrCopy.push(hugeArr[i])
}
console.timeEnd('method1')
}
// 方法二,快取長度
function method2() {
var arrCopy = []
console.time('method2')
for (let i = 0, len = hugeArr.length; i < len; i++) {
arrCopy.push(hugeArr[i])
}
console.timeEnd('method2')
}
// 方法三,取值和判斷合併
function method3() {
var arrCopy = []
console.time('method3')
for (let i = 0, item; item = hugeArr[i]; i++) {
arrCopy.push(item)
}
console.timeEnd('method3')
}
// 方法四,i--與判斷合併,倒序迭代
function method4() {
var arrCopy = []
console.time('method4')
for (let i = hugeArr.length; i--;) {
arrCopy.push(hugeArr[i])
}
console.timeEnd('method4')
}
複製程式碼
分別呼叫上述方法,每個方法重複執行12次,去除一個最大值和一個最小值,求平均值(四捨五入),最終每個方法執行時間的結果如下表(測試機器:MacBook Pro (15-inch, 2017) 處理器:2.8 GHz Intel Core i7 記憶體:16 GB 2133 MHz LPDDR3
執行環境:node v10.8.0
):
- | 方法一 | 方法二 | 方法三 | 方法四 |
---|---|---|---|---|
第一次 | 152.201ms | 156.990ms | 152.668ms | 152.684ms |
第二次 | 150.047ms | 159.166ms | 159.333ms | 152.455ms |
第三次 | 155.390ms | 151.823ms | 159.365ms | 149.809ms |
第四次 | 153.195ms | 155.994ms | 155.325ms | 150.562ms |
第五次 | 151.823ms | 154.689ms | 156.483ms | 148.067ms |
第六次 | 152.715ms | 154.677ms | 153.135ms | 150.787ms |
第七次 | 152.084ms | 152.587ms | 157.458ms | 152.572ms |
第八次 | 152.509ms | 153.781ms | 153.277ms | 152.263ms |
第九次 | 154.363ms | 156.497ms | 151.002ms | 154.310ms |
第十次 | 153.784ms | 155.612ms | 161.767ms | 153.487ms |
平均耗時 | 152.811ms | 155.182ms | 155.981ms | 151.700ms |
用棧差值 | 238511136Byte | 238511352Byte | 238512048Byte | 238511312Byte |
意不意外?驚不驚喜?想象之中至少方法二肯定比方法一更快的!但事實並非如此,不相信眼前事實的我又測試了很多次,包括改變被拷貝的陣列的長度,長度從百級到千萬級。最後發現:在node
下執行完成同一個陣列的淺拷貝任務,耗時方面四種方法的差距微乎其微,有時候排序甚至略有波動。
記憶體佔用方面:方法一 < 方法四 < 方法二 < 方法三,但差距也很小。
v8引擎
新版本針對物件取值等操作進行了最大限度的效能優化,所以方法二中快取陣列的長度到變數len
中,並不會有太明顯的提升。即使是百萬級的資料,四種for迴圈
的耗時差距也只是毫秒級,記憶體佔用上四種for迴圈方式也都非常接近。在此感謝YaHuiLiang、七秒先生、戈尋謀doxP、超級大柱子的幫助和指正,如果大佬們有更好的見解也歡迎評論留言。
同樣是v8引擎
的谷歌瀏覽器
,測試發現四種方法也都非常接近。
但是在火狐瀏覽器
中的測試結果:方法二 ≈ 方法三 ≈ 方法四 < 方法一,表明二三四這三種寫法都可以在一定程度上優化for迴圈
而在safari瀏覽器下
:方法四 < 方法一 ≈ 方法二 ≈ 方法三,只有方法四體現出了小幅度的優化效果。
小結
考慮到在不同環境或瀏覽器下的效能和效率:
推薦
:第四種i--
倒序迴圈的方式。在奇舞團的這篇文章——嗨,送你一張Web效能優化地圖的2.3 流程控制
小節裡也略有提及這種方式。
不推薦
:第三種方式。主要是因為當陣列裡存在非Truthy
的值時,比如0
和''
,會導致迴圈直接結束。
while
迴圈以及ES6+的新語法forEach
、map
和for of
,會更快嗎?
不囉嗦,實踐是檢驗真理的唯一標準
// 方法五,while
function method5() {
var arrCopy = []
console.time('method5')
let i = 0
while (i < hugeArr.length) {
arrCopy.push(hugeArr[i++])
}
console.timeEnd('method5')
}
// 方法六,forEach
function method6() {
var arrCopy = []
console.time('method6')
hugeArr.forEach((item) => {
arrCopy.push(item)
})
console.timeEnd('method6')
}
// 方法七,map
function method7() {
var arrCopy = []
console.time('method7')
arrCopy = hugeArr.map(item => item)
console.timeEnd('method7')
}
// 方法八,for of
function method8() {
var arrCopy = []
console.time('method8')
for (let item of hugeArr) {
arrCopy.push(item)
}
console.timeEnd('method8')
}
複製程式碼
測試方法同上,測試結果:
- | 方法五 | 方法六 | 方法七 | 方法八 |
---|---|---|---|---|
第一次 | 151.380ms | 221.332ms | 875.402ms | 240.411ms |
第二次 | 152.031ms | 223.436ms | 877.112ms | 237.208ms |
第三次 | 150.442ms | 221.853ms | 876.829ms | 253.744ms |
第四次 | 151.319ms | 222.672ms | 875.270ms | 243.165ms |
第五次 | 150.142ms | 222.953ms | 877.940ms | 237.825ms |
第六次 | 155.226ms | 225.441ms | 879.223ms | 240.648ms |
第七次 | 151.254ms | 219.965ms | 883.324ms | 238.197ms |
第八次 | 151.632ms | 218.274ms | 878.331ms | 240.940ms |
第九次 | 151.412ms | 223.189ms | 873.318ms | 256.644ms |
第十次 | 155.563ms | 220.595ms | 881.203ms | 234.534ms |
平均耗時 | 152.040ms | 221.971ms | 877.795ms | 242.332ms |
用棧差值 | 238511400Byte | 238511352Byte | 53887824Byte | 191345296Byte |
在node
下,由上面的資料可以很明顯的看出,forEach
、map
和for of
這些ES6+
的語法並沒有傳統的for
迴圈或者while
迴圈快,特別是map
方法。但是由於map
有返回值,無需額外呼叫新陣列的push
方法,所以在執行淺拷貝任務上,記憶體佔用很低。而for of
語法在記憶體佔用上也有一定的優勢。順便提一下:for迴圈 while迴圈 for of 迴圈
是可以通過break
關鍵字跳出的,而forEach map
這種迴圈是無法跳出的。
但是隨著執行環境和瀏覽器的不同,這些語法在執行速度上也會出現偏差甚至反轉的情況,直接看圖:
谷歌瀏覽器
火狐瀏覽器
safari瀏覽器下
可以看出:
- 谷歌瀏覽器中
ES6+
的迴圈語法會普遍比傳統的迴圈語法慢,但是火狐和safari中情況卻幾乎相反。 - 谷歌瀏覽器的各種迴圈語法的執行耗時上差距並不大。但
map
特殊,速度明顯比其他幾種語法慢,而在火狐和safari中卻出現了反轉,map
反而比較快! - 蘋果大法好
總結
之前有聽到過諸如“快取陣列長度可以提高迴圈效率”或者“ES6的迴圈語法更高效”的說法。說者無心,聽者有意,事實究竟如何,實踐出真知。拋開業務場景和使用便利性,單純談效能和效率是沒有意義的。 ES6新增的諸多陣列的方法確實極大的方便了前端開發,使得以往復雜或者冗長的程式碼,可以變得易讀而且精煉,而好的for
迴圈寫法,在大資料量的情況下,確實也有著更好的相容和多環境執行表現。當然本文的討論也只是基於觀察的一種總結,並沒有深入底層。而隨著瀏覽器的更新,這些方法的孰優孰劣也可能成為玄學。目前發現在Chrome Canary 70.0.3513.0
下for of
會明顯比Chrome 68.0.3440.84
快。如果你有更深入的見解或者文章,也不妨在評論區分享,小弟的這篇文章也權當拋磚引玉。如果你對陣列的其他迴圈方法的效能和效率也感興趣,不妨自己動手試一試,也歡迎評論交流。
本文的測試環境:
node v10.8.0
、Chrome 68.0.3440.84
、Safari 11.1.2 (13605.3.8)
、Firefox 60.0