JS數組循環的性能和效率分析(for、while、forEach、map、for of)

本文永久連接:github.com/HaoChuan942…html

前言

前端開發中常常涉及到數組的相關操做:去重、過濾、求和、數據二次處理等等。都須要咱們對數組進行循環。爲了知足各類需求,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])
}
複製代碼
  1. 第一種方法是最多見的方式,不解釋。node

  2. 第二種方法是將persons.length緩存到變量len中,這樣每次循環時就不會再讀取數組的長度。git

  3. 第三種方式是將取值與判斷合併,經過不停的枚舉每一項來循環,直到枚舉到空值則循環結束。執行順序是:github

    • 第一步:先聲明索引i = 0和變量person
    • 第二步:取出數組的第ipersons[i]賦值給變量person並判斷是否爲Truthy
    • 第三步:執行循環體,打印person
    • 第四步:i++

      當第二步中person的值再也不是Truthy時,循環結束。方法三甚至能夠這樣寫數組

      for (let i = 0, person; person = persons[i++];) {
        console.log(person)
      }
      複製代碼
  4. 第四種方法是倒序循環。執行的順序是:瀏覽器

    • 第一步:獲取數組長度,賦值給變量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.timeconsole.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瀏覽器下方法四 < 方法一 ≈ 方法二 ≈ 方法三,只有方法四體現出了小幅度的優化效果。

safari

小結

考慮到在不一樣環境或瀏覽器下的性能和效率:

推薦第四種i--倒序循環的方式。在奇舞團的這篇文章——嗨,送你一張Web性能優化地圖2.3 流程控制小節裏也略有說起這種方式。

不推薦第三種方式。主要是由於當數組裏存在非Truthy的值時,好比0'',會致使循環直接結束。

while循環以及ES6+的新語法forEachmapfor 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下,由上面的數據能夠很明顯的看出,forEachmapfor of 這些ES6+的語法並無傳統的for循環或者while循環快,特別是map方法。可是因爲map有返回值,無需額外調用新數組的push方法,因此在執行淺拷貝任務上,內存佔用很低。而for of語法在內存佔用上也有必定的優點。順便提一下:for循環 while循環 for of 循環是能夠經過break關鍵字跳出的,而forEach map這種循環是沒法跳出的。

可是隨着執行環境和瀏覽器的不一樣,這些語法在執行速度上也會出現誤差甚至反轉的狀況,直接看圖:

谷歌瀏覽器

谷歌

火狐瀏覽器

火狐

safari瀏覽器下

safari

能夠看出:

  1. 谷歌瀏覽器中ES6+的循環語法會廣泛比傳統的循環語法慢,可是火狐和safari中狀況卻幾乎相反。
  2. 谷歌瀏覽器的各類循環語法的執行耗時上差距並不大。但map特殊,速度明顯比其餘幾種語法慢,而在火狐和safari中卻出現了反轉,map反而比較快!
  3. 蘋果大法好

總結

以前有聽到過諸如「緩存數組長度能夠提升循環效率」或者「ES6的循環語法更高效」的說法。說者無意,聽者有意,事實究竟如何,實踐出真知。拋開業務場景和使用便利性,單純談性能和效率是沒有意義的。 ES6新增的諸多數組的方法確實極大的方便了前端開發,使得以往復雜或者冗長的代碼,能夠變得易讀並且精煉,而好的for循環寫法,在大數據量的狀況下,確實也有着更好的兼容和多環境運行表現。固然本文的討論也只是基於觀察的一種總結,並無深刻底層。而隨着瀏覽器的更新,這些方法的孰優孰劣也可能成爲玄學。目前發如今Chrome Canary 70.0.3513.0for of 會明顯比Chrome 68.0.3440.84快。若是你有更深刻的看法或者文章,也不妨在評論區分享,小弟的這篇文章也權當拋磚引玉。若是你對數組的其餘循環方法的性能和效率也感興趣,不妨本身動手試一試,也歡迎評論交流。

本文的測試環境:node v10.8.0Chrome 68.0.3440.84Safari 11.1.2 (13605.3.8)Firefox 60.0

相關文章
相關標籤/搜索