技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

一看就懂的 var、let、const 三者区别

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/73-一看就懂的var、let、const三者区别
本文目录9 个章节
  1. 一、var 的函数作用域与提升
  2. 二、let 是块级声明
  3. 三、const 是不可重新赋值的绑定
  4. 四、暂时死区(TDZ)
  5. 五、全局脚本、模块与 Node.js 的差异
  6. 六、循环中的 var 与 let
  7. 七、如何选择
  8. 小结
  9. 参考资料

一看就懂的 varletconst 三者区别

varletconst 都可以声明变量,但它们在作用域、初始化时机、重复声明、全局对象属性和重新赋值方面不同。原文写于 ES2015 早期,下面保留原来的学习顺序,并把“变量提升”和“暂时死区”的说法改成更准确的现代表述。

一、var 的函数作用域与提升

var 的作用域通常是最内层的函数体;普通的 ifforwhile 或独立代码块不会为 var 创建块级作用域。如果代码不在函数中,它会属于当前脚本或模块;Node.js 的 CommonJS 文件还会被模块函数包装,所以文件顶层的 var 不等于浏览器页面的 window 属性。

function person(status) {
  if (status) {
    var value = '蛙人'
  } else {
    console.log(value) // undefined
  }

  console.log(value) // undefined
}

person(false)

可以把上面的声明部分近似理解为被处理到了函数作用域的前面,但只有声明被提前处理,初始化赋值不会提前执行

function person(status) {
  var value

  if (status) {
    value = '蛙人'
  } else {
    console.log(value) // undefined
  }

  console.log(value) // undefined
}

这就是通常所说的 var 提升(hoisting)。它不是引擎真的把源代码文本移动了,而是声明实例化和赋值执行发生在不同阶段。

function readValue() {
  console.log(value) // undefined
  var value = '蛙人'
  console.log(value) // 蛙人
}

readValue()

var 允许同一作用域内重复声明;这也是旧代码中容易出现隐蔽覆盖的原因:

var count = 1
var count = 2
console.log(count) // 2

除非维护旧代码或需要兼容早期脚本,否则现代代码通常优先使用 constlet

二、let 是块级声明

let 的作用域是最近的代码块、switch、循环、函数或模块作用域。它不能在同一作用域中被另一个 letconstclassvarimport 重复声明。

function person(status) {
  if (status) {
    let value = '蛙人'
    console.log(value) // 蛙人
  } else {
    // console.log(value) // ReferenceError:这里没有名为 value 的可访问绑定
  }

  // console.log(value) // ReferenceError:value 只存在于 if 代码块
}

person(true)

下面两个声明属于不同的代码块,因此互不冲突:

let value = '外层'

if (true) {
  let value = '内层'
  console.log(value) // 内层
}

console.log(value) // 外层

同一作用域中重复声明会在解析阶段报错:

let value = '蛙人'
// let value = '另一个值' // SyntaxError: Identifier 'value' has already been declared

但父子代码块可以分别声明同名变量:

var oldValue = '外层'
let newValue = '外层'

if (true) {
  var oldValue = '被覆盖'
  let newValue = '内层'
  console.log(oldValue, newValue) // 被覆盖 内层
}

console.log(oldValue, newValue) // 被覆盖 外层

注意:varlet 如果最终属于同一个作用域,也会冲突;不能利用块的写法掩盖这一点:

let name = 'A'
// var name = 'B' // SyntaxError

var age = 1
{
  // let age = 2 // 这是子块,可以声明自己的 age
}

三、const 是不可重新赋值的绑定

constlet 一样是块级声明,也有暂时死区,不能重复声明;区别是 const 必须在声明时初始化,而且之后不能把变量绑定到另一个值:

const value = '蛙人'
// value = '另一个值' // TypeError: Assignment to constant variable.

// const age // SyntaxError: Missing initializer in const declaration

const 锁定的是绑定(binding),不是对象内部的所有状态。对象和数组仍然可以被修改:

const person = {
  name: '蛙人',
  age: 23,
}

person.age = 18 // 正常:修改对象内部属性
person.gender = 'unknown' // 正常:添加属性
// person = {} // TypeError:不能重新赋值

const list = []
list.push('JavaScript') // 正常
// list = ['另一个数组'] // TypeError:不能重新赋值

如果需要限制对象自身属性被修改,应根据需求使用 Object.freeze()、不可变数据结构或复制策略;const 本身不提供深冻结:

const config = Object.freeze({
  port: 3000,
})

// config.port = 4000 // 严格模式下抛出 TypeError;非严格模式下可能静默失败

Object.freeze() 默认也是浅冻结,嵌套对象仍需单独处理。

四、暂时死区(TDZ)

letconstclass 的绑定从作用域开始到声明初始化完成之前处于暂时死区(Temporal Dead Zone,TDZ)。它们不是“完全没有提升”:声明在作用域建立时就已被登记,但在执行到声明前不能读取。

console.log(typeof varValue) // 'undefined'
var varValue = 1

// console.log(typeof letValue) // ReferenceError:Cannot access 'letValue' before initialization
let letValue = 2

typeof 只有在操作未声明标识符时通常能避免 ReferenceError;如果标识符已经有 let/const/class 绑定但仍在 TDZ 中,typeof 也会抛错:

console.log(typeof neverDeclared) // 'undefined'

{
  // console.log(typeof declaredLater) // ReferenceError
  let declaredLater = 1
  console.log(typeof declaredLater) // 'number'
}

函数闭包的创建位置和调用位置也要注意 TDZ:

{
  const read = () => value
  // read() // ReferenceError:调用仍处在 value 的 TDZ 中
  let value = 3
  console.log(read()) // 3:此时已经离开 TDZ
}

原文中“引擎把变量放到 TDZ,执行完声明后移出”的说法是便于理解的模型;规范层面更准确的说法是:词法绑定已经存在,但在初始化前读取会触发 ReferenceError

五、全局脚本、模块与 Node.js 的差异

在浏览器的经典脚本中,顶层 var 通常会成为全局对象(一般是 window,更通用地写作 globalThis)的属性;顶层 letconst 不会成为全局对象属性:

<script>
  var scriptVar = 'var'
  let scriptLet = 'let'
  const scriptConst = 'const'

  console.log(globalThis.scriptVar) // 'var'
  console.log(globalThis.scriptLet) // undefined
  console.log(globalThis.scriptConst) // undefined
</script>

这段行为只针对浏览器经典脚本。<script type="module"> 的顶层声明属于模块作用域;Node.js 的 CommonJS 文件和原生 ECMAScript 模块也有各自的模块作用域,不能把它们直接等同于 window

使用 globalThis.someName = value 才是在明确地写全局对象属性,但应用代码通常应避免无必要的全局状态。

六、循环中的 varlet

letfor 循环中可以为每次迭代创建独立的绑定,因此异步回调常能得到预期的迭代值;var 通常只有一个函数作用域绑定:

for (var i = 0; i < 3; i += 1) {
  setTimeout(() => console.log('var:', i), 0)
}
// var: 3
// var: 3
// var: 3

for (let j = 0; j < 3; j += 1) {
  setTimeout(() => console.log('let:', j), 0)
}
// let: 0
// let: 1
// let: 2

这不是因为定时器“记住了当时的值”,而是因为闭包捕获了不同的词法绑定。若必须使用 var,也可以通过立即调用函数或显式传参创建新的作用域,但现代代码使用 let 更清晰。

七、如何选择

场景推荐
变量后续不会重新绑定const
变量需要重新赋值,如循环计数器let
需要维护的旧脚本、依赖函数作用域的遗留代码var,并明确其提升与作用域
需要修改对象内部状态可以用 const,但要理解它不冻结对象
需要跨模块共享状态优先使用模块导出、明确的状态容器或依赖注入,避免隐式全局变量

const 并不要求变量所指向的值是 primitive;let 也不等于对象一定可变。它们控制的是变量绑定,数据是否可变由对象自身、冻结策略和程序设计决定。

小结

  • var 主要是函数作用域,声明会提升,初始化不会提升,可以重复声明;
  • let 是块级作用域,可以重新赋值,但不能同作用域重复声明;
  • const 是块级作用域,必须初始化,不能重新赋值;
  • letconstclass 在初始化前处于 TDZ,连 typeof 也不能绕过;
  • const 只保证绑定不变,不保证对象或数组内部不可变;
  • 浏览器经典脚本中的顶层 var 与全局对象属性有关,模块、CommonJS 和浏览器模块不要混淆;
  • 新代码通常优先 const,需要重新绑定时使用 let,尽量避免新增 var

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS