本文目录导读:
这是一个非常好的问题,它涉及 JavaScript(以及其他许多语言)中一个重要的性能优化原则,核心原因在于减少属性查找的代价。
在循环中反复访问对象属性,就像每次都要重新查找一个文件的位置;而先赋值给局部变量,就像把这个文件直接放在手边,后者自然快得多。
下面从几个层面来详细解释:
核心原因:属性访问 vs. 变量访问
-
对象属性访问(如
obj.prop):在 JavaScript 引擎执行时,这是一个相对“昂贵”的操作,引擎需要:- 获取对象
obj的引用。 - 在对象的属性列表(或隐藏类、哈希表等内部结构)中查找名为
prop的属性。 - 返回该属性的值。
- 这个过程涉及查找、比较、以及可能的原型链遍历。
- 获取对象
-
局部变量访问(如
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]);
}
更深层次的原因:避免副作用和原型链查找
-
避免重复计算:如果属性是一个 getter(
get访问器),那么每次访问它都会执行一次 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.getElementById 和 element.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)
养成这个习惯,你的代码会在高性能场景下表现得更好。
标签: 局部变量