js中for in碰到Array.prototype的问题

技术 · 2023-04-12 · 3684 人浏览

最近一个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 数组,就不会有问题了,这样也不失为一个好的解决办法。

for in Array.prototype