1. 变量声明三剑客:var、let、const的本质差异
十年前我刚入行前端时,var是唯一的变量声明方式。直到ES6时代,let和const的出现彻底改变了JavaScript的作用域游戏规则。这三者的区别看似基础,却在面试中淘汰了无数候选人——因为大多数人只记住了表面语法,却说不清背后的设计哲学。
1.1 作用域:从函数囚笼到块级解放
var最反直觉的特性就是函数作用域。我曾调试过一个经典bug:循环中的setTimeout全部输出相同值。这是因为:
javascript复制for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // 输出5个5
}
而let的块级作用域才是符合人类直觉的设计。在if、for等代码块内部声明的变量就像被关在透明保险箱里——外部不可见但内部一览无余。这种设计让代码的可预测性大幅提升。
1.2 变量提升:隐式陷阱与显式约束
早期JavaScript引擎处理var声明时,会把声明部分(不包括赋值)提升到作用域顶部。这导致了一个诡异现象:
javascript复制console.log(hoisted); // undefined而非ReferenceError
var hoisted = "value";
let/const通过TDZ(Temporal Dead Zone)机制解决了这个问题。在声明前访问会直接报错,这种显式的约束反而降低了调试成本。我在团队代码规范中强制要求:永远在作用域顶部集中声明所有变量。
1.3 重新赋值:可变性与不变性的哲学
const的不可变性经常被误解。它实际保证的是绑定关系不变,而非值本身不可变。比如:
javascript复制const obj = { prop: 1 };
obj.prop = 2; // 允许
obj = {}; // TypeError
在React项目里,我常用const声明组件props,既避免意外覆盖又允许必要的属性更新。而对于计时器ID这类需要重新赋值的变量,let才是最佳选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频面试题深度剖析
2.1 闭包陷阱与解决方案
这是最经典的var面试题变体:
javascript复制function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result.push(function() { console.log(i); });
}
return result;
}
createFunctions().forEach(fn => fn()); // 输出3个3
解决方案除了改用let,还可以用IIFE创建新作用域:
javascript复制for (var i = 0; i < 3; i++) {
(function(j) {
result.push(function() { console.log(j); });
})(i);
}
2.2 const声明的对象属性可变性
面试官常会追问:"既然const声明的对象属性可以修改,如何实现真正不可变?" 这时候可以展示你对现代语法的掌握:
javascript复制const obj = Object.freeze({
prop: 1,
nested: { a: 1 }
});
obj.prop = 2; // 静默失败或TypeError
obj.nested.a = 2; // 仍然可以修改!
真正的不可变需要深层冻结或使用Immutable.js这样的库。
2.3 暂时性死区(TDZ)的边界情况
TDZ的报错时机是个容易忽略的知识点:
javascript复制function test() {
console.log(typeof undeclaredVar); // "undefined"
console.log(typeof declaredLet); // ReferenceError
let declaredLet;
}
这说明TDZ检查比普通的未声明检查优先级更高。在Babel转译的代码中,这种边界行为有时会和原生表现不一致。
3. 工程实践中的选择策略
3.1 声明风格指南
在我的团队中,我们遵循这些原则:
- 默认使用const - 除非明确需要重新赋值
- 需要重新赋值时用let - 如循环计数器
- 禁止使用var - 通过ESLint规则约束
- 集中声明原则 - 在作用域顶部声明所有变量
对应的ESLint配置:
json复制{
"rules": {
"no-var": "error",
"prefer-const": ["error", {
"destructuring": "all"
}]
}
}
3.2 性能优化的迷思
有候选人问:"let/const是否比var慢?" 现代JS引擎的优化已经消除了这个差异。V8引擎的TurboFan编译器会对不可变量进行特殊优化,const反而可能获得更好的性能。
3.3 模块化开发中的导出策略
在编写模块时,我倾向于:
javascript复制// 好的实践
export const API_URL = '...';
export let requestCount = 0; // 需要修改的统计量
// 不好的实践
export var deprecated = true; // var可能被意外提升
4. 进阶:从规范理解设计哲学
4.1 词法环境与变量环境
在ECMAScript规范中,执行上下文包含:
- LexicalEnvironment:处理let/const声明
- VariableEnvironment:处理var声明
这种双环境设计解释了为什么会有不同的提升行为。Babel转译let时会生成唯一的变量名来模拟块级作用域:
javascript复制// 转译前
let x = 1;
{
let x = 2;
}
// 转译后
var x = 1;
{
var _x = 2;
}
4.2 严格模式的影响
在严格模式下,一些var的怪异行为会更早暴露:
javascript复制'use strict';
delete x; // 非严格模式静默失败,严格模式报错
var x;
而let/const声明的变量本来就禁止delete操作,无论是否严格模式。
4.3 全局作用域的特殊性
在全局作用域中,var声明会成为window对象的属性,而let/const不会:
javascript复制var globalVar = 1;
let globalLet = 2;
console.log(window.globalVar); // 1
console.log(window.globalLet); // undefined
这个特性在微前端等需要隔离全局变量的场景尤为重要。
