刨析 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]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容