博客 / 詳情

返回

刨析 JS 中的forEach、for in、for of三類循環原理和性能

大家好,我是林一一,這是一篇比較 JS 中三類循環的原理和性能的文章,希望能給你帶來點幫助 😁

性能比較

for 循環和 while 循環的性能對比

let arr = new Array(999999).fill(1)

console.time('forTime')
for(let i = 0; i< arr.length; i++){}
console.timeEnd('forTime')

console.time('whileTime')
let i = 0
while(i< arr.length){
    i ++ 
}
console.timeEnd('whileTime')
/* 輸出
* forTime: 4.864990234375 ms
* whileTime: 8.35107421875 ms
*/
  • 使用 let 聲明下的循環,由於 for 中塊級作用域的影響,內存得到釋放,運行的運行的速度會更快一些。
  • 使用 var 聲明時因為for while 的循環都不存在塊級作用域的影響,兩者運行的速度基本一致。

forEach(callback, thisArg) 循環數組

callback 函數每一輪循環都會執行一次,且還可以接收三個參數(currentValue, index, array)index, array 也是可選的,thisArg(可選) 是回調函數的 this 指向。
  • 遍歷可枚舉的屬性

    let arr = new Array(999999).fill(1)
    console.time('forEachTime')
    arr.forEach(item =>{} )
    console.timeEnd('forEachTime')
    // forEachTime: 25.3291015625 ms
  • 函數式編程的 forEach 性能消耗要更大一些。

思考:在 forEach 中使用 return 能中斷循環嗎?

[1,2,4,5].forEach((item, index) => {
    console.log(item, index)
    return
})
// 1 0
// 2 1
// 4 2
// 5 3
從上面看出 forEach 中使用 return 是不能跳出循環的。
那麼如何中斷 forEach 的循環
  • 可以使用 try catch
  • 或使用其他循環來代替,比如 用 every 和some 替代 forEach,every 中內部返回 false是跳出,some 中內部是 true 時 跳出

模擬實現 forEach

Array.prototype.myForEach = function (callback, context) {
    let i = 0,
        than = this,
        len = this.length;
    context = context ? window : context;
    for (; i < len; i++) {
        typeof callback === 'function' ? callback.call(context, than[i], i, than) : null
    }
}

let arr = [0, 1, 5, 9]
arr.myForEach((item, index, arr) => {
    console.log(item, index, arr)
})

//0 0 (4) [0, 1, 5, 9]
// 1 1 (4) [0, 1, 5, 9]
結果準確無誤。關於 this 指向或 call 的使用的可以看看 JS this 指向 和 call, apply, bind的模擬實現

for in 循環

for in 的循環性能循環很差。性能差的原因是因為:for in 會迭代對象原型鏈上一切 可以枚舉的屬性。
let arr = new Array(999999).fill(1)
console.time('forInTime')
for(let key in arr){}
console.timeEnd('forInTime')
// forInTime: 323.08984375 ms
  • for in 循環主要用於對象

    let obj = {
      name: '林一一',
      age: 18,
      0: 'number0',
      1: 'number1',
      [Symbol('a')]: 10
    }
    
    Object.prototype.fn = function(){}
    
    for(let key in obj){
    //    if(!obj.hasOwnProperty(key)) break 阻止獲取原型鏈上的公有屬性 fn
      console.log(key)
    }
    /* 輸出
     0
     1
     name
     age
     fn
    */
  • (缺點) for in 循環主要遍歷數字優先,由小到大遍歷
  • (缺點) for in 無法遍歷 Symbol屬性(不可枚舉)。
  • (缺點) for in 會將公有(prototype) 中可枚舉的屬性也遍歷了。可以使用 hasOwnProperty來阻止遍歷公有屬性。

    思考

    1. 怎麼獲取 Symbol 屬性

    使用 Object.getOwnPropertySymbols(),獲取所有 Symbol 屬性。
    let obj = {
      name: '林一一',
      age: 18,
      0: 'number0',
      1: 'number1',
      [Symbol('a')]:  10
    }
    
    Object.prototype.fn = function(){}
    
    let arr = Object.keys(obj).concat(Object.getOwnPropertySymbols(obj))
    console.log(arr)    //["0", "1", "name", "age", Symbol(a)]

for of 循環

let arr = new Array(999999).fill(1)
console.time('forOfTime')
for(const value of arr){}
console.timeEnd('forOfTime')
// forOfTime: 33.513916015625 ms
for of 循環的原理是按照是否有迭代器規範來循環的,所有帶有 Symbol.iterator 的都是實現了迭代器規範,比如數組一部分類數組,Set,Map...對象沒有實現 Symbol.iterator 規範,所以不能使用for of循環。
  • 使用 for of 循環,首先會先執行 Symbol.iterator 屬性對應的函數且返回一個對象
  • 對象內包含一個函數 next() 循環一次執行一次 next()next() 中又返回一個對象
  • 這個對象內包含兩個值分別是 done:代表循環是否結束,true 代表結束;value:代表每次返回的值

    // Symbol.iterator 內部機制如下
    let arr = [12, 23, 34]
    arr[Symbol.iterator] = function () {
      let self = this,
          index = 0;
      return {
          next() {
              if(index > self.length-1){
                  return {
                      done: true,
                      value: undefined
                  }
              }
              return {
                  done: false,
                  value: self[index++]
              }
          }
      }
    }

    思考,如何讓普通的類數組可以使用 for of 循環

    類數組被需具備和數組類試的結果屬性名從0, 1, 2...開始,且必須具備length 屬性
    let obj = {
      0: 12,
      1: '林一一',
      2: 'age18',
      length: 3
    }
    // 
    obj[Symbol.iterator] = Array.prototype[Symbol.iterator]
    for (const value of obj) {
      console.log(value)   
    }
  • 12
  • 林一一
  • age18
    */

    > 只需要給類數組對象添加`Symbol.iterator`接口規範就可以了。

(附加)將argument實參集合變成真正的數組

arguments 為什麼不是數組?

  • arguments 是類數組(其實是一個對象)屬性從0開始排,依次為0,1,2... 最後還有 callee和length 屬性,arguments__proto__ 直接指向基類的 object,不具備數組的方法。

    方式一 使用 call(),[].slice/Array.prototype.slice()

    let array = [12, 23, 45, 65, 32]
    function fn(array){
      var args = [].slice.call(arguments)
      return args[0]
    }
    fn(array)   // [12, 23, 45, 65, 32]

    上面的 slice 結合 call 為什麼可以在改變 this 後可以將 arguments 轉化成數組?我們來模擬手寫實現一下 slice,就知道里面的原理了

    Array.prototype.mySlice = function(startIndex=0, endIndex){
      let array = this    // 通過 this 獲取調用的數組
      let thisArray = []
      endIndex === undefined ? (endIndex = array.length) : null
      for(let i = startIndex; i< endIndex; i++){      // 通過 `length` 屬性遍歷
          thisArray.push(array[i])
      }
      return thisArray
    }
    
    // 測試一下沒有問題
    let arr = [1, 3, 5, 6, 7, 23]
    let a 
    a = arr.mySlice()   // [1, 3, 5, 6, 7, 23]
    a = arr.mySlice(2, 6)   // [5, 6, 7, 23]
    通過 this 獲取調用 mySlice 的數組,再通過 length 屬性遍歷形成一個新的數組返回。所以改變this 指向 arguments 再通過 arguments.length 遍歷返回一個新的數組,便實現了將類數組轉化成數組了。

來思考一下字符串可以轉化成數組嗎?

let a = [].slice.call('stringToArray')
console.log(a)  // ["s", "t", "r", "i", "n", "g", "T", "o", "A", "r", "r", "a", "y"]
同樣也是可以的,理由同上。至於字符串(值類型)為什麼被 this 指定,可以來看看這篇文章 [面試 | call,apply,bind 的實現原理和麪試題]()

方式二 使用 ES6 的擴展運算符 ...

function fn(array){
    var args = [...arguments]
    return args
}
fn(12, 23, 45, 65, 32)   // [12, 23, 45, 65, 32]

方式三 Array.from()

function fn(array){
    return Array.from(arguments)
}
fn(12, 23, 45, 65, 32)   // [12, 23, 45, 65, 32]
user avatar jidongdehai_co4lxh 頭像 yaofly 頭像 coderleo 頭像 flymon 頭像 _raymond 頭像 uncletong_doge 頭像 buxia97 頭像 yilezhiming 頭像
8 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.