公司动态
JavaScript变量声明:var、let、const的区别与最佳实践
1. 从一段“诡异”的代码说起为什么我们需要let和const如果你写过一段时间的 JavaScript尤其是经历过 ES5 及之前的时代那你一定对var这个关键字又爱又恨。爱的是它简单声明变量全靠它恨的是它带来的种种“灵异”现象常常让代码行为超出你的预期。我们先来看一段经典的、让无数新手掉坑的代码for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }请问这段代码会输出什么如果你的直觉是0, 1, 2, 3, 4那么恭喜你你掉进了var的第一个大坑。实际输出是五个5。为什么因为var声明的i是函数作用域或者叫全局作用域而不是块级作用域。整个循环结束后i的值变成了5而setTimeout中的回调函数在循环结束后才执行它们访问的是同一个、已经变成5的i。这就是 ES6ES2015引入let和const最直接的动机之一提供块级作用域的变量声明让变量的生命周期和可见性限制在{}花括号之内从而写出更可预测、更安全的代码。今天我们就来彻底掰扯清楚var、let、const这三兄弟的特点、区别以及在实际项目中到底该怎么选。这不仅仅是语法差异更是编程思维和代码质量的一次升级。2.var的“遗产”函数作用域与变量提升要理解let和const带来的革新必须先摸透var的老底子。它的行为特性是历史遗留问题但在维护老代码或理解某些“奇怪”现象时至关重要。2.1 核心特点函数作用域与“变量提升”var声明的变量其作用域被限制在声明它的函数内部。如果不在任何函数内声明那就是全局变量。注意是函数不是代码块如if、for、while的{}。这就是开头那个例子问题的根源for循环的{}关不住var i。更“神奇”的是变量提升。JavaScript 引擎在执行代码前会有一个“编译”阶段在这个阶段它会找到所有var声明并把它们“提升”到当前作用域函数或全局的顶部。但提升的仅仅是声明赋值操作会留在原地。console.log(myVar); // 输出undefined而不是报错 var myVar 10; console.log(myVar); // 输出10上面的代码在引擎眼里相当于var myVar; // 声明被提升到作用域顶部 console.log(myVar); // 此时 myVar 已声明但未赋值值为 undefined myVar 10; // 赋值留在原地 console.log(myVar); // 10这种机制常常导致一个误区你以为变量在声明前不存在实际上它存在只是值为undefined。这容易引发难以察觉的 Bug比如在赋值前就使用了变量得到的是undefined而非预期的报错。2.2 可以重复声明一个潜在的混乱之源var允许你在同一作用域内多次声明同一个变量后面的声明不会覆盖前面的但如果伴有初始化则会覆盖值。var x 1; var x 2; // 允许不会报错 console.log(x); // 输出2这个特性在大型项目或多人协作中是个噩梦。你可能会无意中在某个深层函数里重新声明了一个全局变量导致其他依赖该变量的模块行为异常而且这种错误静默发生极难排查。2.3 在全局作用域声明会成为全局对象的属性在浏览器环境中全局作用域下用var声明的变量会自动成为window对象的属性。var globalVar I am global; console.log(window.globalVar); // 输出I am global这可能会造成全局命名空间污染与其他脚本库的变量发生冲突。注意虽然var有这么多“坑”但你必须了解它因为海量的遗留代码库还在使用它。在维护老项目时盲目地将所有var改为let/const可能会引入意想不到的副作用需要结合具体代码逻辑谨慎处理。3.let的革新块级作用域与暂时性死区let是 ES6 引入的用于声明变量的关键字旨在解决var的主要缺陷。它的设计更加严谨和直观。3.1 核心特点块级作用域let声明的变量其作用域被限制在声明它的代码块内。这个“块”指的是任何用{}包裹起来的语句比如if、for、while、switch或者单独的一个{}。用let重写开头的例子问题迎刃而解for (let i 0; i 5; i) { setTimeout(function() { console.log(i); // 输出0, 1, 2, 3, 4 }, 100); }这里每次循环迭代都会创建一个新的块级作用域每个i都是这个新作用域内的独立变量。setTimeout回调捕获的是各自迭代时的i值因此输出正确。3.2 暂时性死区更严格的声明前访问限制let也有“提升”但机制与var完全不同这引入了“暂时性死区”的概念。从代码块开始到let声明语句执行之前的这个区域称为该变量的暂时性死区。在 TDZ 内访问该变量会直接抛出一个ReferenceError。console.log(myLet); // ReferenceError: Cannot access myLet before initialization let myLet 10;这比var的undefined行为要好得多因为它将潜在的错误从运行时逻辑错误得到undefined提前到了语法/解析阶段直接报错更有利于早期发现和修复问题。3.3 禁止重复声明在同一作用域内let不允许重复声明同一个变量。let y 1; let y 2; // SyntaxError: Identifier y has already been declared这强制了代码的清晰性避免了因重复声明导致的意外覆盖。3.4 全局声明不会成为全局对象的属性在全局作用域下用let声明的变量不会自动成为window或global对象的属性。let letGlobal I am let global; console.log(window.letGlobal); // 输出undefined (在浏览器中) console.log(letGlobal); // 输出I am let global这有助于减少全局命名空间的污染。4.const的坚守常量与不可变绑定const也是 ES6 引入的用于声明常量。很多人误以为const代表“值不可变”其实更准确的说法是“绑定不可变”。4.1 核心特点声明时必须初始化且绑定不可变const声明的常量必须在声明的同时进行初始化且之后不能再被重新赋值。const PI 3.14159; PI 3.14; // TypeError: Assignment to constant variable. const MY_CONST; // SyntaxError: Missing initializer in const declaration这里的“绑定不可变”指的是变量名与那个初始值之间的“引用”关系被锁定了。对于基本类型数字、字符串、布尔、null、undefined、Symbol、BigInt值本身就存在变量里所以不能重新赋值就等同于值不可变。4.2 对于对象和数组绑定不变内容可变这是const最容易让人困惑的地方。当const声明一个对象或数组时不可变的是这个变量指向的内存地址引用而不是这个地址里存储的内容。const person { name: Alice }; person.name Bob; // 这是允许的修改对象属性 console.log(person); // { name: Bob } person { name: Charlie }; // TypeError: Assignment to constant variable. 禁止重新赋值即更换引用 const arr [1, 2, 3]; arr.push(4); // 允许修改数组内容 console.log(arr); // [1, 2, 3, 4] arr [5, 6]; // TypeError: Assignment to constant variable.所以const保证的是“你指着这个房子对象的手不能换指别的房子”但并没有限制你进入这个房子去重新装修修改属性。如果你需要对象也完全不可变需要使用Object.freeze()等更深层的冻结方法。4.3 同样具备块级作用域和暂时性死区const与let在作用域、暂时性死区、禁止重复声明、全局属性等方面行为完全一致。它们是一对亲兄弟只是const多了一个“必须初始化且不能重新赋值”的约束。5. 三者的横向对比与选型策略了解了各自特点后我们通过一个表格进行系统性的对比这能帮你快速决策。特性varletconst作用域函数作用域或全局作用域块级作用域块级作用域变量提升有提升声明初始值为undefined有在TDZ中访问会报错有在TDZ中访问会报错暂时性死区无有有重复声明允许不允许不允许声明时初始化可选可选必须重新赋值允许允许不允许全局声明成为window属性是否否5.1 现代 JavaScript 开发中的选型黄金法则在实际项目中如何选择我个人的经验是遵循一条简单的法则它能覆盖 99% 的场景默认使用const只在你知道这个变量需要被重新赋值时才使用let基本不用var。这条法则背后的逻辑是const优先大部分变量在声明后其引用是不需要改变的。使用const可以明确表达这个意图让代码更可读也防止了意外的重新赋值。即使它指向一个对象你依然可以修改其内部属性这通常是我们期望的行为。let用于变量当你明确知道这个值需要改变时比如循环计数器、状态标志、累积计算的结果等就使用let。它提供了块级作用域的安全性又保留了可变的灵活性。var的弃用在新的项目和模块中没有理由再使用var。它的特性函数作用域、提升、重复声明更像是语言的缺陷而非特性。只在维护无法改动的遗留代码时才需要与之打交道。5.2 实战中的细节与“坑”循环中的const你可能会惊讶for...of或for...in循环中可以使用const。const arr [a, b, c]; for (const item of arr) { console.log(item); // 输出 a, b, c } // 这里每次迭代都会创建一个新的块级作用域和一个新的常量 item绑定当次迭代的值。这是完全合法的。但是传统的for (let i0; i10; i)循环不能将let换成const因为i试图对i进行重新赋值会报错。let在for循环头部的特殊行为标准规定for循环(let i ...)的括号部分会创建一个父级作用域而循环体{}则在每次迭代中创建新的子作用域。这使得在循环体内创建的函数如setTimeout回调能捕获到当次迭代的i值。这是语言特意为let设计的行为用以解决历史问题。const与不可变数据如果你正在使用 Redux 或推崇函数式编程你会追求状态的不可变性。这时const是你的好朋友但它只保证了引用的不变。要真正实现深不可变你需要配合使用扩展运算符、Object.assign、Array.prototype.map等返回新对象/数组的方法或者使用 Immutable.js 这样的库。6. 从语法到思维编写更健壮的代码理解var、let、const的区别不仅仅是记住几条语法规则更是培养一种更严谨的编程思维。作用域意识块级作用域让你能更精细地控制变量的生命周期。你可以放心地在if、for内部声明临时变量而不用担心它们泄露到外部污染作用域。这使得代码的模块化更好不同逻辑块之间的耦合度更低。声明即意图看到const你就知道这个标识符指向的东西大体上稳定不变看到let你就预期它的值可能会随着逻辑流改变。这种代码自解释性大大提升了可读性和可维护性。错误前置暂时性死区的设计将“使用未初始化的变量”这类错误从潜在的运行时逻辑错误undefined导致的奇怪现象转变为明确的、在开发阶段就能捕获的引用错误极大地提升了调试效率。在我自己的项目实践中强制使用 ESLint 规则prefer-const和no-var是非常有帮助的。它们能自动检查代码提示你将可以改为const的let进行转换并禁止使用var。这相当于让工具来强制执行前面提到的黄金法则帮助团队形成一致的、更优的编码风格。从var到let/const的转变是 JavaScript 语言走向成熟和严谨的重要一步。拥抱块级作用域善用const声明意图你的代码会减少许多难以追踪的幽灵 Bug变得更加清晰和强壮。下次写let或const时不妨多想一秒这个值真的需要改变吗