最近一个js项目中使用了for(let i in arr) {} 循环,for in的好处就是被遍历的对象可以是数组,可以是对象,就算是null和undefined都没有问题,不会报错,所以被大量使用,而且当一个无序的数组中更是不会遍历空数据。如下:
let a = []; a[5] = 5; console.log(a); // [empty × 5, 5] for (let i in a) { console.log(i); } // 5
可是当客户在使用时使用了一个第三方插件,插件中使用了Array.prototype自定义方法,结果项目开始报错,最后发现问题出现在for in的时候会遍历枚举对象属性,包括prototype中的enumerable为true的对象属性,所以就出现问题了。
刚开始我找问题,发现给Array增加自定义方法可以用以下2种办法:
Array.prototype.last = function () { console.log('do last'); } Object.defineProperty(Array.prototype, 'first', { value: function () { console.log('do first'); } }); for (let idx in arr) { if(idx == 'last') { console.log("error idx",idx); } if(idx == 'first') { console.log("error idx",idx); } } //error idx last
然后用for in遍历的时候只发现了last,使用defineProperty是默认了enumerable:false,可是插件不是都使用了
defineProperty方法,如果我们把第三方的插件中的方法enumerable属性改为false是否可以呢
Object.defineProperty(Array.prototype,'last', { enumerable : false }); for (let idx in arr) { if(idx == 'last') { console.log("error idx",idx); } if(idx == 'first') { console.log("error idx",idx); } }
结果都没有了,这样是可以解决问题的,但是我们不可能把所有使用Array.prototype的都去设置一下,继续找找别的办法吧,继续发现了通过hasOwnProperty判断是是否为自有属性,2种写法如下:
for (let idx in arr) { if (!arr.hasOwnProperty(idx)) { continue; } let b = arr[idx]; } for (let idx in arr) { if (!Object.prototype.hasOwnProperty.call(arr, idx)) { continue; } let b = arr[idx]; }
这样感觉多了几行代码判断改起来麻烦,后面发现通过Array.keys()方法可以获取所有自有属性为数组,再使用es6的for of去遍历,而且兼容对象,再改改:
for (let idx of Object.keys(arr)) { let b = arr[idx]; }
这样就没有问题了,但是还是发现之前兼容的null和undefined还是会报错,再修改一下,就完美了,改动比用hasOwnProperty判断小。
for (let idx of Object.keys(arr || {})) { let b = arr[idx]; }
最后,这样会不会增加性能问题呢,我们先测试一下吧:
//满数组 let count = 100000; for(let i = 0;i < count;i++) { //半数组时使用 //if(i % 2 == 0) { // continue; //} arr[i] = i; } console.time("for in"); for (let idx in arr) { let b = arr[idx]; } console.timeEnd("for in"); console.time("for of"); for (let idx of arr) { let b = idx; } console.timeEnd("for of"); console.time("for keys"); for (let idx of Object.keys(arr || {})) { let b = arr[idx]; } console.timeEnd("for keys"); console.time("for hasOwnProperty"); for (let idx in arr) { if (!arr.hasOwnProperty(idx)) { continue; } let b = arr[idx]; } console.timeEnd("for hasOwnProperty");
结果如下:
//满数组1w for in: 0.57421875 ms for of: 0.210693359375 ms for keys: 0.56005859375 ms for hasOwnProperty: 0.668212890625 ms //满数组10w for in: 10.117919921875 ms for of: 2.534912109375 ms for of keys: 11.2568359375 ms for in hasOwnProperty: 9.299072265625 ms //满数组100w for in: 112.973876953125 ms for of: 17.890869140625 ms for keys: 120.429931640625 ms for hasOwnProperty: 122.72509765625 ms //半数组1w for in: 0.31103515625 ms for of: 0.288818359375 ms for keys: 0.303955078125 ms for hasOwnProperty: 0.349853515625 ms //半数组10w for in: 5.828125 ms for of: 4.82275390625 ms for keys: 7.498046875 ms for hasOwnProperty: 5.253173828125 ms //半数组100w for in: 56.394775390625 ms for of: 13.29296875 ms for keys: 95.276123046875 ms for hasOwnProperty: 57.758056640625 ms
如果可以看出在小量数据下使用keys方法改动比较小,性能上影响也比较小。当然使用for of肯定是最好的,但是这个只能用于数组,而且还需要顺序数组。所以在写代码的时候还是使用顺序数组比较好,直接使用for of,对象再用for in,当你实在是分不清的时候可以尝试使用上面的方法处理。
在最后,想想enumerable:false可以解决for in的问题,那么我把用户定义的属性改变一下是否就可以了呢?例用for in可以读出可枚举属性方式去遍历一个空数组,可以得到所有自定义的属性,再把自定义属性enumerable改为false,但是自定义的属性还是可以使用的。如下:
for (let i in []) { Object.defineProperty(Array.prototype, i, { enumerable: false }); }
再去运行for in 数组,就不会有问题了,这样也不失为一个好的解决办法。
评论