let和const的到来带来了什么?

219 阅读5分钟

之前在 变量提升和函数提升 中主要是通过编译构成和变量的生命周期来进行分析的.但是并没有总结一下他们的使用的差异

也就说,letconst的到来给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 infor 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 readonlyObject.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 的不可变契约降低多人维护的认知成本
  • 架构更健壮:全局作用域隔离契合现代模块化工程实践