为什么说在循环中反复访问属性应该先赋值给局部变量

访客 性能优化 1

本文目录导读:

  1. 核心原因:属性访问 vs. 变量访问
  2. 循环的放大效应
  3. 更深层次的原因:避免副作用和原型链查找
  4. 现实中的例子:DOM 操作
  5. 现代 JavaScript 引擎的优化

这是一个非常好的问题,它涉及 JavaScript(以及其他许多语言)中一个重要的性能优化原则,核心原因在于减少属性查找的代价

在循环中反复访问对象属性,就像每次都要重新查找一个文件的位置;而先赋值给局部变量,就像把这个文件直接放在手边,后者自然快得多。

下面从几个层面来详细解释:

核心原因:属性访问 vs. 变量访问

  • 对象属性访问(如 obj.prop:在 JavaScript 引擎执行时,这是一个相对“昂贵”的操作,引擎需要:

    1. 获取对象 obj 的引用。
    2. 在对象的属性列表(或隐藏类、哈希表等内部结构)中查找名为 prop 的属性。
    3. 返回该属性的值。
    • 这个过程涉及查找、比较、以及可能的原型链遍历
  • 局部变量访问(如 prop:这是最快速的操作之一,局部变量通常存储在栈内存或引擎的寄存器中,访问时几乎不需要任何查找开销,直接读取即可。

循环的放大效应

循环的每一次迭代都会重复上述的属性查找过程,如果循环次数是 100 万次,那么属性查找也会重复 100 万次,这累积起来的性能损耗非常可观。

// 低效版本:每次循环都查找 obj.length
for (let i = 0; i < obj.length; i++) {
    // 每一轮迭代都要在 obj 身上查找 'length' 属性
    doSomething(obj[i]);
}
// 高效版本:将 obj.length 缓存到局部变量 len
const len = obj.length; // 只查找一次
for (let i = 0; i < len; i++) {
    doSomething(obj[i]);
}

更深层次的原因:避免副作用和原型链查找

  • 避免重复计算:如果属性是一个 getterget 访问器),那么每次访问它都会执行一次 getter 函数,这可能是一个有副作用的、昂贵的计算,将其结果缓存到局部变量可以确保只执行一次。

    const obj = {
        get expensiveProp() {
            console.log('执行了昂贵计算');
            return Math.random();
        }
    };
    // 低效:每次循环都触发 getter 函数
    for (let i = 0; i < 1000; i++) {
        console.log(obj.expensiveProp); // 打印 1000 次 "执行了昂贵计算"
    }
    // 高效:getter 只执行一次
    const cachedProp = obj.expensiveProp;
    for (let i = 0; i < 1000; i++) {
        console.log(cachedProp); // 不再触发 getter
    }
  • 避免原型链查找:如果属性不是对象自身的属性,而是从原型链上继承来的,引擎需要沿着原型链向上查找,直到找到或到达 Object.prototype,这个过程在深层嵌套的原型链中成本更高,缓存到局部变量就避免了这种遍历。

现实中的例子:DOM 操作

这是前端开发中最经典的场景。document.getElementByIdelement.style 等都是非常昂贵的 DOM API。

// 非常低效:每次循环都查询 DOM
for (let i = 0; i < 100; i++) {
    document.getElementById('myDiv').innerHTML += i;
}
// 高效:先将 DOM 元素缓存到局部变量
const myDiv = document.getElementById('myDiv');
let html = '';
for (let i = 0; i < 100; i++) {
    html += i;
}
myDiv.innerHTML = html; // 最终只操作一次 DOM

在这个例子中,我们不仅缓存了属性访问document.getElementById 的返回值),还缓存了DOM 对象本身,但原理是相通的:避免在循环中重复进行昂贵的查找或操作。

现代 JavaScript 引擎的优化

现代 JavaScript 引擎(如 V8、SpiderMonkey)非常聪明,它们会进行内联缓存等优化,对于简单的、反复访问的同一对象的同一属性,引擎可能会自动推断其位置并“,从而加速后续访问。

依赖引擎优化是不安全的:

  • 引擎优化有前提条件:如果对象的结构发生变化(比如在循环中给对象添加或删除属性),引擎的优化会失效(即“去优化”),性能反而下降。
  • 并非所有情况都能被优化:复杂的嵌套属性、getter、原型链等情况下,引擎很难或无法优化。
  • 最佳实践仍然是显式优化:写出清晰、可预测、对引擎友好的代码,比依赖黑盒优化更可靠,赋值给局部变量这个习惯,在任何情况下都不会降低性能(对于简单属性,可能微乎其微或没有区别,但绝不会更差),而在复杂情况下能带来显著的性能提升。
操作类型 成本 原因
对象属性访问 obj.prop 较高 需要查找、比较,可能遍历原型链,触发 getter。
局部变量访问 prop 极低 直接读取栈/寄存器上的值,几乎零开销。

核心建议是:

在循环等需要高频访问某属性的场景中,总是先将其值赋值给一个局部变量。

这是一个简单但极其有效的微观优化技巧,尤其适用于:

  • 遍历数组时缓存 length
  • 访问 DOM 属性的值
  • 访问 getter 属性
  • 访问深层嵌套的属性 (config.api.endpoint.user 可以缓存为 const endpoint = config.api.endpoint.user)

养成这个习惯,你的代码会在高性能场景下表现得更好。

标签: 局部变量

抱歉,评论功能暂时关闭!