之前在 变量提升和函数提升 中主要是通过编译构成和变量的生命周期来进行分析的.但是并没有总结一下他们的使用的差异
也就说,let和const的到来给js带来哪些变化
一、差异
1. 块级作用域
循环中的var
for(var i = 0; i < 5; i++) {
console.log(i) // 0, 1, 2, 3, 4
setTimeout(function() {
console.log(i) // 5 5 5 5 5, 执行5 次 ps: 上面的同步任务执行完毕之后,马上就执行这里的,然后大约间隔1000ms输出一次
}, i * 1000)
}
console.log(i) // 5
为了解决这个问题,在ES6之前,是使用 立即执行函数来生成块级作用域处理这个问题.
;(function(i){
setTimeout(function() {
console.log(i) // 5 5 5 5 5 ps: 上面的同步任务执行完毕之后,
//马上就执行这里的,然后大约间隔1000ms输出一次
}, i * 1000)
})()
类似于如下:
for(var i = 0; i < 5; i++) {
foo(i)
}
function foo(i) {
setTimeout(function() {
console.log(i) // 0, 1, 2, 3, 4 ps: 上面的同步任务执行完毕之后,马上就执行这里的,然后大约间隔1000ms输出一次
}, i * 1000)
}
循环中的let
这玩意出来之后就方便太多了. 不用 立即执行函数, 直接讲var改为let就可以了.
for (let i = 0; i > 5; i++) {
setTimeout(() => {
console.log(i) // 0 , 1, 2, 3, 4
}, 0)
}
console.log(i) // a is not defined
循环中的const
它就有所不同. 还需要需要分情况来处理该问题,不同的循环会有不同的行为
for(const i =0; i < 5; i++) {} // 直接报错哦. 因为是绑定谁,const不能重复更改
在for in和for of中 还是可以使用的, 迭代不会修改已有绑定.
let array = []
for (const i in array) {}
for (const i fo array) {}
2. 临时性死区(Temporal Dead Zone)
在之前的文章已经尝试过解释这一问题了. 下面是它表现的形式:
{
console.log(typeof a) // undefined
var a = 1
}
不会报错
console.log(typeof a) // ReferenceError: Cannot access 'a' befre initialization
let a = 1
禁止重复声明
var a = 1
var a = 2
console.log(a) // 2
let a = 1
let a = 2
console.log(a) // SyntaxError: Identifier 'a' has alread y been declared
3. 不允许修改绑定
这一条是单独指const的.
const a = 1
a = 2 // 直接报错, 说不给你修改
但是有一个众所周知的细节, 对于引用类型的话,const就无能为力了:
const a = [1, 2, 3]
a[0] = 10
a.push(4)
console.log(a) // [10, 2, 3, 4]
也就是说const只能绑定 栈内存中的值,对于 堆内存的变量无能为力
4. 全局块作用域的绑定
对于这一点,可能是现在的var仍然能够存在的一点点点点🤏意义
其实是代码代码混乱的根源,之前认知不到位
var say = 'hoo'
console.log(window.say) // 'hoo'
如上代码所示, var能够是say直接绑定到了全局变量当中. 这个在浏览器中的iframe或跨window通信是有所作用的.
而换成到var或者const. 是会屏蔽全局变量的赋值.不会破坏全局作用域.
let a = 'foo'
console.log(window.a === a ) // false
const b = 'too'
console.log('too' in window) // false
二、意义:let/const 的设计哲学与工程价值
这些差异给我们写代码的时候真正带来了什么价值和意义呢?
前文对比了 var 与 let/const 的语法差异,但更深层的价值在于:通过约束提升代码质量。let/const 看似提高了开发"门槛",实则通过以下机制根治了 var 时代遗留的工程痛点:
5.1 块级作用域:变量泄露的终结者
// var 的陷阱:循环变量泄露为全局污染源
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i)) // 输出 5 次 5
}
// let 的块级作用域隔离变量生命周期
for (let j = 0; j < 5; j++) {
setTimeout(() => console.log(j)) // 正确输出 0-4
}
工程意义:
- 避免变量意外污染外层作用域(如循环计数器泄露)
- 天然支持闭包安全(循环内异步回调无需额外 IIFE 封装)
5.2 暂时性死区(TDZ):消灭变量提升的幽灵
console.log(a); // var 允许访问但值为 undefined → 隐藏的 Bug 风险
var a = 10;
console.log(b); // let 直接抛出 ReferenceError → 强制开发者规范声明顺序
let b = 20;
工程意义:
- 杜绝因变量提升导致的逻辑错乱(如误读未初始化值)
- 强制要求「先声明后使用」,符合直觉的代码阅读顺序
5.3 const 不可变性:契约式编程的基石
const MAX_RETRY = 3;
MAX_RETRY = 5; // ❌ TypeError:拒绝意外篡改核心配置
const user = { name: 'Alice' };
user.name = 'Bob'; // ✅ 允许:const 仅约束引用地址,需配合 Object.freeze 深度冻结
工程意义:
- 明确标识常量,减少因误操作导致的状态异常(如全局配置被覆盖)
- 与 TypeScript
readonly或Object.freeze组合可实现深度不可变数据
5.4 全局纯净性:避免 window 对象污染
var legacyVar = '旧全局变量';
console.log(window.legacyVar); // '旧全局变量' → 可能引发命名冲突
const modernVar = '模块化变量';
console.log(window.modernVar); // undefined → 符合 ES Module 隔离规范
工程意义:
- 在模块化开发中,天然避免全局命名空间污染
- 与
Webpack/Vite等构建工具配合时,确保代码封装性
总结:从"人治"到"自治"的演进
let/const 并非简单的语法迭代,而是通过语言层面的强约束,将以往依赖开发者自觉(如「避免修改变量」「手动隔离作用域」)的规范,转变为引擎强制执行的规则。这种转变使得:
- 代码更可预测:块级作用域 + TDZ 确保执行逻辑符合直观
- 协作更安全:const 的不可变契约降低多人维护的认知成本
- 架构更健壮:全局作用域隔离契合现代模块化工程实践