JS遍历循环方法性能对比:for/while/for in/for of/map/foreach/every
这周codeReview例会,又遇到map与foreach到底谁问题。单独图方便,我会选择用map一个函数搞定一切。但是从语义的角度来讲,如果只是单纯遍历,还是推荐选择foreach。其实formap 与foreach,性能相差不大(个人测试数据在10000000,最后有测试案例)。如果用foreach 去实现map的效果,性能上就会比map差(因为需要操作另外一个数组).
使用for,变量提前声明,性能会有一丢丢提升。如果循环变量i挂在全局变量上,也会造成性能损耗
如果i是挂在全局上的,因为他每次loop完都要从全局中找回i值,i++ 和 判断
而封装在 function里面的,对比与在全局里找i,单单在function 里找起来比较快
从性能上考量,我从eslint上禁止 for in。
之前在gem代码重构的过程中,讲了很多次 for in for map foreach等遍历情况,但是没有过系统性地解析。
这次决定 把之前看的东西,东拼西凑地再来一篇总结。
遍历数组性能分析
对数组的遍历大家最常用的就是for循环,ES5的话也可以使用forEach,ES5具有遍历数组功能的还有map、filter、some、every、reduce、reduceRight等,只不过他们的返回结果不一样。
如果都做同样的遍历,他们的性能是怎么样的呢?
{ name: 'time-While', value: 18 },
{ name: 'time-ForFilter', value: 123 },
{ name: 'time-ForEvery', value: 139 },
{ name: 'time-ForSome', value: 140 },
{ name: 'time-ForOf', value: 158 },
{ name: 'time-ForEach', value: 174 },
{ name: 'time-ForMap', value: 190 },
{ name: 'time-For', value: 544 },
{ name: 'time-ForIn', value: 6119 }
结果是 while 是最快的(理论上,感觉for与while应该是等效的)。 formap等es5 函数快于 for,formap 快于foreach . for in 最慢
为什么for in 这么慢?
使用for in会遍历数组所有的可枚举属性,包括原型。例如上栗的原型方法method和name属性
解释器遇到for...in 循环时,在后台需要为对象建立一个枚举器(enumerator),这是一个昂贵的操作!
for in 注意事项
-
index索引为字符串型数字,不能直接进行几何运算
-
遍历顺序有可能不是按照实际数组的内部顺序
for in遍历的是数组的索引(即键名),而for of遍历的是数组元素值。 所以for in更适合遍历对象,不要使用for in遍历数组。
for in 遍历顺序问题
关于for in 属性问题,可以看下面两段代码
1 | const arr = [100, 'B' , 4, '5' , 3, 'A' , 0];<br> for (const key in arr) {<br> console.log(`index:${key} value:${arr[key]}`);<br>}<br>console.log( '________\n' );<br> function Foo() {<br> this [100] = 100;<br> this .B = 'B' ;<br> this [4] = 4;<br> this [ '5' ] = '5' ;<br> this [3] = 3;<br> this .A = 'A' ;<br> this [0] = 0;<br>}<br>const bar = new Foo();<br> for (const key in bar) {<br> console.log(`index:${key} value:${bar[key]}`);<br>} |
在ECMAScript规范中定义了 「数字属性应该按照索引值⼤⼩升序排列,字符 串属性根据创建时的顺序升序排列。」
V8内部,为了有效地提升存储和访问这两种属性的性能,分别使⽤了两个 线性数据结构来分别保存排序 属性和常规属性,具体结构如下图所⽰:
对象中的数字属性称为 「排序属性」,在V8中被称为 elements,字符串属性就被称为 「常规属性」, 在V8中被称为 properties。
在elements对象中,会按照顺序存放排序属性,properties属性则指向了properties对 象,在properties对象中,会按照创建时的顺序保存了常规属性。关于 for in 与 for of更详细的,参看 https://zhuanlan.zhihu.com/p/161892289
for ..in 与 for..of区别
一句话概括:for in是遍历(object)键名,for of是遍历(array)键值——for of 循环用来获取一对键值对中的值,而 for in 获取的是 键名。
-
for in 循环出的是key(并且key的类型是string),for of 循环出的是value。
-
for of 是es6引新引入的特性,修复了es5引入的for in 的不足。
-
for of 不能循环普通的对象,需要通过Object.keys搭配使用。
对于他们的区别,一般就看下面一段代码就可:
1 | {<br> const b = [1, 2, 3, 4]; // 创建一个数组<br> b.name = '小明'; // 给数组添加一个属性<br> Array.prototype.age = 12; // 给数组的原型也添加一个属性<br> console.log('for in ---------------');<br> for (const key in b) {<br> console.log(key);<br> }<br> console.log('for of ---------------');<br> for (const key of b) {<br> console.log(key);<br> }<br> console.log('forEach ---------------');<br> b.forEach((item) => {<br> console.log(item);<br> });<br>}<br>console.log('______________\n');<br>{<br> const b = { a: 1, b: 2 }; // 创建一个对象<br> b.name = '小明'; // 给对象添加一个属性<br> Object.prototype.age = 12; // 给对象的原型也添加一个属性<br> console.log('for in ---------------');<br> for (const key in b) {<br> console.log(key);<br> }<br> console.log('forEach ---------------');<br> Object.keys(b).forEach((item) => {<br> console.log(item);<br> });<br>} |
可以通过hasOwnProperty限制for..in 遍历范围。
for...in
for...in 循环只遍历可枚举属性(包括它的原型链上的可枚举属性)。这个代码是为普通对象设计的,不适用于数组的遍历
JavaScript中的可枚举属性与不可枚举属性
在JavaScript中,对象的属性分为可枚举和不可枚举之分,它们是由属性的enumerable值决定的。可枚举性决定了这个属性能否被for…in查找遍历到。
像 Array和Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性,例如 String 的 indexOf() 方法或 Object的toString()方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。
枚举性属性的影响
-
for in (遍历所有可枚举属性,不仅是 own properties 也包括原型链上的所有属性)
-
Object.keys(只返回对象本身具有的可枚举的属性)
-
JSON.stringify() (只读取对象本身可枚举属性,并序列化为JSON字符串)
-
Object.assign() (复制自身可枚举的属性,进行浅拷贝)
引入enumerable的最初目的,就是让某些属性可以规避掉for...in操作。比如,对象原型的toString方法,以及数组的length属性,就通过这种手段,不会被for...in遍历到。
for...of
for of 是es6引新引入的特性,修复了es5引入的for in 的不足。
for...of 只可遍历可迭代对象,for...of 语句在可迭代对象(包括Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
什么数据可以for of遍历
一个数据结构只要部署了 Symbol.iterator 属性, 就被视为具有 iterator接口, 就可以使用 for of循环。
些数据结构部署了 Symbol.iteratoer属性了呢?
只要有 iterator 接口的数据结构,都可以使用 for of循环。
-
数组 Array
-
Map
-
Set
-
String
-
arguments对象
-
Nodelist对象, 就是获取的dom列表集合
-以上这些都可以直接使用 for of 循环。 凡是部署了 iterator 接口的数据结构也都可以使用数组的 扩展运算符(...)、和解构赋值等操作。
for of不可以遍历普通对象,想要遍历对象的属性,可以用for in循环, 或内建的Object.keys()方法。
for循环与ES5新增的foreach/map 等方法有何区别?
forEach 不支持在循环中添加删除操作,因为在使用 forEach 循环的时候数组(集合)就已经被锁定不能被修改。(改了也没用)
在 for 循环中可以使用 continue,break 来控制循环和跳出循环,这个是 forEach 所不具备的。【在这种情况下,从性能的角度考虑,for 是要比 forEach 有优势的。 替代方法是 filter、some等专用方法。
遍历对象性能分析
遍历对象,之前用for in,我现在一般用Object.keys来获取值数组。再来遍历对象。他们的性能对比如何?
{ name: 'Object.keys.map', value: 21 },
{ name: 'forIn', value: 30 }
Object.keys来遍历对象,也比for in 要快
数组测试代码
1 | const size = 10000000;<br><br> let times = [];<br>{<br> const arrFor = new Array(size).fill(1);<br> let timeFor = + new Date();<br> console.time( 'arrFor' );<br> for ( let i = 0;i < arrFor.length;i++) {<br> const b = arrFor[i];<br> //<br> }<br> console.timeEnd('arrFor');<br> timeFor = new Date().getTime() - timeFor;<br> times.push({ name: 'time-For', value: timeFor });<br>}<br><br>{<br> const arrWhile = new Array(size).fill(1);<br> let timeWhile = +new Date();<br> console.time('timeWhile');<br> let i = arrWhile.length - 1;<br> while (i > -1) {<br> const b = arrWhile[i];<br> i--;<br> }<br> console.timeEnd('timeWhile');<br> timeWhile = new Date().getTime() - timeWhile;<br> times.push({ name: 'time-While', value: timeWhile });<br>}<br><br>{<br> const arrForOf = new Array(size).fill(1);<br> let timeForOf = +new Date();<br> console.time('timeForOf');<br> for (const item of arrForOf) {<br><br> }<br> console.timeEnd('timeForOf');<br> timeForOf = new Date().getTime() - timeForOf;<br> times.push({ name: 'time-ForOf', value: timeForOf });<br>}<br>{<br> const arrForIn = new Array(size).fill(1);<br> let timeForIn = +new Date();<br> console.time('timeForIn');<br> for (const key in arrForIn) {<br> // 注意key不是<br> }<br> console.timeEnd('timeForIn');<br> timeForIn = new Date().getTime() - timeForIn;<br> times.push({ name: 'time-ForIn', value: timeForIn });<br>}<br>{<br> const arrForEach = new Array(size).fill(1);<br> let timeForEach = +new Date();<br> console.time('timeForEach');<br> arrForEach.forEach((item, index) => {<br><br> });<br> console.timeEnd('timeForEach');<br> timeForEach = new Date().getTime() - timeForEach;<br> times.push({ name: 'time-ForEach', value: timeForEach });<br>}<br>{<br> const arrForMap = new Array(size).fill(1);<br> let timeForMap = +new Date();<br> console.time('timeForMap');<br> arrForMap.map((item, index) => {<br><br> });<br> console.timeEnd('timeForMap');<br> timeForMap = new Date().getTime() - timeForMap;<br> times.push({ name: 'time-ForMap', value: timeForMap });<br>}<br>{<br> const arrForEvery = new Array(size).fill(1);<br> let timeForEvery = +new Date();<br> console.time('timeForEvery');<br> arrForEvery.every((item, index) => true);<br> console.timeEnd('timeForEvery');<br> timeForEvery = new Date().getTime() - timeForEvery;<br> times.push({ name: 'time-ForEvery', value: timeForEvery });<br>}<br><br>{<br> const arrForEvery = new Array(size).fill(1);<br> let timeForEvery = +new Date();<br> console.time('timeForSome');<br> arrForEvery.some((item, index) => false);<br> console.timeEnd('timeForSome');<br> timeForEvery = new Date().getTime() - timeForEvery;<br> times.push({ name: 'time-ForSome', value: timeForEvery });<br>}<br>{<br> const arrForEvery = new Array(size).fill(1);<br> let timeForEvery = +new Date();<br> console.time('timeForFilter');<br> arrForEvery.filter((item, index) => false);<br> console.timeEnd('timeForFilter');<br> timeForEvery = new Date().getTime() - timeForEvery;<br> times.push({ name: 'time-ForFilter', value: timeForEvery });<br>}<br>times = times.sort((a, b) => a.value - b.value);<br>console.log(times); |
不知道这个测试代码是否可以改进。
foreach与map获得一个新数组
1 | const size = 10000000;<br><br> let times = [];<br><br>{<br> const arrForEach = new Array(size).fill(1);<br> let timeForEach = + new Date();<br> console.time( 'timeForEach' );<br> const arr1 = [];<br> arrForEach.forEach((item, index) => {<br> arr1.push(item + 1);<br> });<br> console.timeEnd( 'timeForEach' );<br> timeForEach = new Date().getTime() - timeForEach;<br> times.push({ name: 'time-ForEach' , value: timeForEach });<br>}<br>{<br> const arrForMap = new Array(size).fill(1);<br> let timeForMap = + new Date();<br> console.time( 'timeForMap' );<br> const arr1 = arrForMap.map((item, index) => item + 1);<br> console.timeEnd( 'timeForMap' );<br> timeForMap = new Date().getTime() - timeForMap;<br> times.push({ name: 'time-ForMap' , value: timeForMap });<br>}<br>times = times.sort((a, b) => a.value - b.value);<br>console.log(times); |
因为map直接返回了。foreach需要操作另外一个数组,造成性能损耗。我猜的哈。
for变量提前声明与while性能对比
1 | const size = 10000000;<br><br> let times = [];<br>{<br> const arrFor = new Array(size).fill(1);<br> let timeFor = + new Date();<br> console.time( 'arrFor0' );<br> for ( let i = 0 ;i < arrFor.length;i++) {<br> const b = arrFor[i];<br> //<br> }<br> console.timeEnd('arrFor0');<br> timeFor = new Date().getTime() - timeFor;<br> times.push({ name: 'time-For0', value: timeFor });<br>}<br>{<br> const arrFor = new Array(size).fill(1);<br> let timeFor = +new Date();<br> console.time('arrFor');<br> for (let i = size - 1;i > -1;i--) {<br> const b = arrFor[i];<br> //<br> }<br> console.timeEnd('arrFor');<br> timeFor = new Date().getTime() - timeFor;<br> times.push({ name: 'time-For', value: timeFor });<br>}<br>{<br> const arrFor = new Array(size).fill(1);<br> let timeFor = +new Date();<br> console.time('arrFor1');<br> let i = 0;<br> for (;i < size;i++) {<br> const b = arrFor[i];<br> //<br> }<br> console.timeEnd('arrFor1');<br> timeFor = new Date().getTime() - timeFor;<br> times.push({ name: 'time-For1', value: timeFor });<br>}<br>{<br> const arrFor = new Array(size).fill(1);<br> let timeFor = +new Date();<br> console.time('arrFor2');<br> let i = size - 1;<br> for (;i > -1;i--) {<br> const b = arrFor[i];<br> //<br> }<br> console.timeEnd('arrFor2');<br> timeFor = new Date().getTime() - timeFor;<br> times.push({ name: 'time-For2', value: timeFor });<br>}<br>{<br> const arrWhile = new Array(size).fill(1);<br> let timeWhile = +new Date();<br> console.time('timeWhile');<br> let i = size - 1;<br> while (i > -1) {<br> const b = arrWhile[i];<br> i--;<br> }<br> console.timeEnd('timeWhile');<br> timeWhile = new Date().getTime() - timeWhile;<br> times.push({ name: 'time-While', value: timeWhile });<br>}<br>times = times.sort((a, b) => a.value - b.value);<br>console.log(times); |
测试结果:
{ name: 'time-For2', value: 11 },
{ name: 'time-While', value: 11 },
{ name: 'time-For', value: 14 },
{ name: 'time-For1', value: 14 },
{ name: 'time-For0', value: 18 }
对象测试代码
1 | const size = 100000;<br><br> let times = [];<br>{<br> const arrFor = Array.from( new Array(size), (n, index) => [index, index + 1]);<br> let timeFor = + new Date();<br> const obj = Object.fromEntries(arrFor);<br> console.time( 'forIn' );<br> for (const key in obj) {<br> const item = obj[key];<br> }<br> console.timeEnd( 'forIn' );<br> timeFor = new Date().getTime() - timeFor;<br> times.push({ name: 'forIn' , value: timeFor });<br>}<br>{<br> const arrFor = Array.from( new Array(size), (n, index) => [index, index + 1]);<br> let timeFor = + new Date();<br> const obj = Object.fromEntries(arrFor);<br> console.time( 'Object.keys.map' );<br> Object.keys(obj).map((key) => {<br> const item = obj[key];<br> });<br> console.timeEnd( 'Object.keys.map' );<br> timeFor = new Date().getTime() - timeFor;<br> times.push({ name: 'Object.keys.map' , value: timeFor });<br>}<br>times = times.sort((a, b) => a.value - b.value);<br>console.log(times); |
先这样吧
后面再来整理一下。
参考文章:
Js中for in 和for of的区别 https://juejin.cn/post/6844903601261772808
for…in和for…of的用法与区别 https://segmentfault.com/a/1190000022348279
[JavaScript] for、forEach、for...of、for...in 的区别与比较 https://blog.csdn.net/csdn_yudong/article/details/85053698
for in 和 for of 的区别? https://zhuanlan.zhihu.com/p/282961866
百度前端面试题:for in 和 for of的区别详解以及为for in的输出顺序 https://zhuanlan.zhihu.com/p/161892289
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了