首页 > 解决方案 > 为什么在 JavaScript 中比较一个数组和一个数字这么慢?它在做什么?

问题描述

我不小心将一个大数组和一个数字与 进行比较<,并且 JavaScript 锁定超过 5 秒。这种比较的预期行为是什么?它是否遍历整个数组?MDN没有说明情况。

作为一个具体的例子,这个代码片段需要超过 5 秒的时间来打印done

var m = [];
m[268435461] = -1;
console.log('start');
if (m < 0) { }
console.log('done');

标签: javascriptarrays

解决方案


Javascript“数组”(那些带有Array原型,而不是类型数组)只是对象,因此这个

var m = [];
m[268435461] = -1;

完全一样

var m = {
    "268435461": -1
}

除了在第一种情况下,m具有Array原型和特殊length属性。

但是,在Array.prototype(like forEachor join) 中定义的方法试图隐藏这一事实并“模拟”顺序数组,因为它们存在于其他语言中。当迭代他们的“this”数组时,这些方法会获取它的length属性,将循环计数器从0upto增加length-1,并对键下的值做一些事情String(i)(或者undefined如果没有这样的键)

// built-in js array iteration algorithm

for (let i = 0; i < this.length - 1; i++) {
     if (this.hasOwnProperty(String(i))
         do_something_with(this[String(i)])
     else
         do_something_with(undefined)

现在,length数组不是其中的元素数量,顾名思义,而是它的键的最大数值 + 1,所以在你的情况下,length将是268435462(检查它!)

当你这样做时m < 0,也就是说,将一个非数字与一个数字进行比较,JS 将它们都转换为字符串,并Array.toString调用Array.join,这反过来又使用上面的循环将元素转换为字符串并在其间插入一个逗号:

// built-in js Array.join algorithm

target = '';

for (let i = 0; i < this.length - 1; i++) {
    let element = this[String(i)]

    if(element !== undefined)
        target += element.toString()

    target += ','
}

插图:

m  = [];
m[50] = 1;
console.log(m.join())

这涉及大量内存分配,这就是导致延迟的原因。

(经过更多测试,分配不是这里的决定因素,“空心”循环会导致同样的减速:

console.time('small-init')
var m = [];
m[1] = -1;
console.timeEnd('small-init')

console.time('small-loop')
m.forEach(x => null)
console.timeEnd('small-loop')

console.time('big-init')
var m = [];
m[1e8] = -1;
console.timeEnd('big-init')

console.time('big-loop')
m.forEach(x => null);
console.timeEnd('big-loop')

话虽如此,我不认为现代 JS 引擎有那么愚蠢,并且完全按照上面描述的方式实现迭代。他们确实有特定于数组的优化,但这些优化是针对“好的”顺序数组,而不是像这样奇怪的边缘情况。底线:不要那样做!


推荐阅读