本文永久連接:github.com/HaoChuan942…html
前端開發中常常涉及到數組的相關操做:去重、過濾、求和、數據二次處理等等。都須要咱們對數組進行循環。爲了知足各類需求,JS除了提供最簡單的for
循環,在ES6
和後續版本中也新增的諸如:map、filter、some、reduce
等實用的方法。由於各個方法做用不一樣,簡單的對全部涉及到循環的方法進行執行速度比較,是不公平的,也是毫無心義的。那麼咱們就針對最單純的以取值爲目的的循環進行一次性能和效率測試,用肉眼可見的方式,對JS中常見的這些數組循環方式進行一次探討。前端
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])
}
複製代碼
第一種方法是最多見的方式,不解釋。node
第二種方法是將persons.length
緩存到變量len
中,這樣每次循環時就不會再讀取數組的長度。git
第三種方式是將取值與判斷合併,經過不停的枚舉每一項來循環,直到枚舉到空值則循環結束。執行順序是:github
第四種方法是倒序循環。執行的順序是:瀏覽器
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()比對執行先後內存中已用到的堆的差值。post
/* 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