在 JavaScript 的性能优化传说中,流传着一个古老而著名的技巧:在 for 循环中缓存数组的 length 属性。你一定见过这样的代码:
// “优化”前的代码
for (let i = 0; i < someArray.length; i++) {
// ... do something
}
// 堆代码 duidaima.com
// “优化”后的代码
for (let i = 0, len = someArray.length; i < len; i++) {
// ... do something
}
这个建议的逻辑很简单:每次循环都去访问 someArray.length 会产生额外的开销,不如用一个局部变量 len 把它存起来,这样可以提高循环性能。那么,length 检查真的很慢吗?
对于标准数组,length 快得惊人
在现代 JavaScript 引擎中,访问 .length 的速度几乎是恒定的,因为它只是一个简单的属性读取,引擎内部直接存储了这个值。所以,对于标准数组,以下代码在性能上几乎没有区别,但后者明显更简洁、更易读,也更符合现代 JavaScript 的编码风格:
对于 DOM 集合的 length
既然普通数组的 length 很快,那这个流传已久的优化技巧到底是从何而来的呢?答案指向了浏览器环境中的一个特殊对象:DOM 集合,尤其是 NodeList 和 HTMLCollection。当我们使用 document.getElementsByTagName() 或 element.childNodes 这样的方法时,得到的不是一个真正的 JavaScript Array,而是一个实时的集合。
每当我们访问它的 length 属性时,浏览器必须重新去查询 DOM,计算符合条件的元素数量。现在,想象一下这个可怕的场景:
const divs = document.getElementsByTagName('div'); // 这是一个实时的 HTMLCollection
// 堆代码 duidaima.com
// 每次循环,浏览器都会重新去 DOM 树里数一遍有多少个 div
for (let i = 0; i < divs.length; i++) {
// 假设我们在这里创建并插入一个新的 div
const newDiv = document.createElement('div');
document.body.appendChild(newDiv);
}
在这个例子中,每次循环都会发生:
i < divs.length:浏览器重新查询 DOM,计算 divs.length
循环体执行,一个新的 <div> 被添加到 document.body 中
下一次循环,divs.length 的值变大了
这不仅会导致无限循环,还会在每次循环条件判断时,触发一次昂贵的 DOM 重查询。
正确姿势
现在我们知道了问题的根源。那么,正确的做法是什么?
将 DOM 集合转换为真正的数组
在对 DOM 集合进行复杂操作之前,先把它变成一个静态的、真正的 JavaScript 数组。既可避免 length 性能陷阱,还能使用所有数组方法(map, filter, 等)。
// 推荐!使用 Array.from()
const divs = Array.from(document.getElementsByTagName('div'));
// 或者使用展开语法
const divs = [...document.getElementsByTagName('div')];
// 现在 divs 是一个真正的数组了,可以安全、高效地遍历
divs.forEach(div => {
// ...
});
for (let i = 0; i < divs.length; i++) {
// 这里的 .length 访问非常快!
}
现代浏览器中,querySelectorAll() 返回的是静态的 NodeList,其 length 不会动态变化。但 getElementsByTagName 等仍然返回实时集合。为了统一和安全,将所有 DOM 集合在使用前转换为数组是一个好习惯。对于普通 Array 对象,不需要再手动缓存 length,优先选择 for...of 或高阶函数(如 forEach, map)来提升代码可读性;对于 DOM,先转数组。