大家好,我是林一一,這是一篇比較 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]