先记住这个答案
区别主要在三件事上。第一是作用域:var 属于最近的函数,if、for 这些花括号挡不住它;let 和 const 属于最近的一对花括号,出了块就访问不到。第二是提升:三者都会提升,但 var 提升后先被初始化成 undefined,let 和 const 在声明那一行执行之前处于暂时性死区,访问就抛 ReferenceError。第三是赋值:let 可以重新赋值,const 声明时必须赋值、之后不能再赋值,但它指向的对象内容仍然可以改。
var只认函数边界,块挡不住它let和const只在所在的{}里有效- 声明前访问
let会抛ReferenceError,var得到undefined const锁的是变量,不是对象内容
用一个函数看清三者的边界
在 if 块里分别声明三个变量,再到块外面去读,同时试试在声明前访问。
function demo() {
console.log(early) // var 已提升,值是 undefined
try {
console.log(late)
} catch (err) {
console.log(err.name) // let 还在暂时性死区
}
let late = 'let'
if (true) {
var early = 'var'
let inner = 'let in block'
const config = { on: false }
config.on = true // 改属性可以
console.log(inner, config.on)
}
console.log(early) // var 出了 if 仍然可见
console.log(typeof inner) // let 出了块就没有了
}
demo()查看输出与解释
undefined
ReferenceError
let in block true
var
undefinedearly 是在 if 块里声明的,但它属于整个 demo 函数,函数一开始就存在,只是值为 undefined,赋值后块外也能读到。late 在声明前读取直接报错,这段报错的区间就叫暂时性死区。inner 离开 if 块就不存在了,typeof 对未声明的变量返回字符串 undefined。config 不能被重新赋值成别的对象,但改它的属性没问题。
暂时性死区到底挡住了什么
暂时性死区是指从作用域开始到 let 或 const 声明语句执行之前的那一段,变量已经存在,但不允许读写。它的意义是把「先用后声明」这种写法从静默的 undefined 变成明确的报错。
- 遮蔽和死区一起出现的坑
- 外层有一个
let x = 1,内层块里写console.log(x); let x = 2,很多人以为会打印外层的 1,实际会报ReferenceError。因为内层块里声明了自己的x,整个块里的x都指向这个还没初始化的内层变量,外层的x已经被遮住了。 - 循环里的区别
for (let i = 0; ...)每一轮循环都会创建一个新的i,所以循环里的异步回调各自拿到自己那一轮的值。var整个函数只有一个i,回调执行时读到的都是循环结束后的值。这也是现在循环变量一律用let的原因。
容易答错的地方
- 说
let和const不会提升 - 它们会提升,作用域一开始变量就已经存在,所以才会遮住外层的同名变量。只是没有被初始化,声明前访问会报错。说「不提升」解释不了上面遮蔽的那个例子。
- 说
const声明的对象不可变 const限制的是变量不能指向别的值,对象本身的属性、数组的元素都能改。要让对象不可改,用Object.freeze,而且它只冻结一层,嵌套对象还要递归处理。- 说
var没有作用域 var有作用域,是函数作用域。函数里声明的var,函数外面访问不到。它只是不受if、for这类块的限制。
面试官还会怎么问?
同一个作用域里重复声明会怎样?
var 重复声明不报错,后面的赋值覆盖前面的。let 和 const 重复声明是语法错误,代码在解析阶段就失败,一行都不会执行。var 和 let 同名混用也会报错。
项目里该怎么选?
默认用 const,需要重新赋值的变量(计数器、累加结果、循环变量)用 let,新代码不再用 var。这样读代码的人看到 const 就知道这个变量不会再指向别的东西。
顶层的 var 和 let 有什么区别?
在普通脚本顶层,var 声明的变量会成为全局对象的属性,window.x 能读到;let 和 const 声明的也是全局变量,但不挂在 window 上。在 ES 模块里,顶层声明都只属于这个模块,不会变成全局变量。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。