先记住这个答案
var、let、const 都会提升,引擎在进入作用域时就把这些名字登记好了。区别在初始化:var 登记时就被赋成 undefined,所以声明前读取不报错,只拿到 undefined。let 和 const 登记后保持「未初始化」状态,要等执行到声明那一行才可用,在这之前读写都会抛 ReferenceError。这段不能访问的区间叫暂时性死区(TDZ)。
- 三种声明都会提升,只是初始化时机不同
var声明前读取得到undefinedlet和const声明前读取抛ReferenceError- 暂时性死区里连
typeof都会报错
声明前读取三种变量
在同一个函数里,把三种变量的读取都写在声明之前,看各自的表现。
var、let、constJavaScriptfunction inspect() {
console.log(a) // var:不报错
try {
console.log(b)
} catch (err) {
console.log(err.name) // let:抛错
}
try {
console.log(typeof c) // const:typeof 也救不了
} catch (err) {
console.log(err.name)
}
var a = 1
let b = 2
const c = 3
}
inspect()查看输出与解释
undefined
ReferenceError
ReferenceErrorvar a 在函数开始执行时就已经是 undefined,赋值 a = 1 还留在原位置,所以第一行打印 undefined。b 和 c 名字已经存在,但没初始化,读取直接报错。
第三个 try 值得多看一眼:很多人以为 typeof 永远不会抛错,对完全没声明过的变量确实如此,它返回字符串 'undefined'。但变量在暂时性死区里时,typeof 一样会抛 ReferenceError。
为什么说 let 也提升了
面试里常有人说「let 不会提升」,这不准确。最直接的证据是遮蔽:块里一旦声明了同名 let,从块的第一行开始,这个名字指的就是块里的变量,外层同名变量已经读不到了。
let 挡住了外层变量JavaScriptlet name = 'outer'
{
try {
console.log(name) // 这里读的是下面那个 name,还没初始化
} catch (err) {
console.log(err.name)
}
let name = 'inner'
console.log(name)
}查看输出与解释
ReferenceError
inner如果 let 真的不提升,第一次读取应该拿到外层的 'outer'。实际却抛错了,说明进入块的时候,内层的 name 就已经登记好了,只是还不能用。
- 暂时性死区是按时间算的
- 死区指的是「执行到声明之前」这段时间,不是代码里声明上面那几行。比如在函数里读取一个外面的
let变量,只要函数是在变量声明之后才调用的,就没问题,哪怕函数定义写在声明上面。反过来,声明之前就调用这个函数,就会报错。
函数声明和 class 怎么提升
函数声明是整体提升的,名字和函数体一起就位,所以在声明之前调用完全没问题,这也是很多老代码把工具函数写在文件末尾的原因。
函数表达式要看左边用什么声明。var fn = function () {} 提升的只是 var fn,声明前调用 fn() 得到的是 TypeError: fn is not a function,因为此时 fn 是 undefined。换成 const fn = ...,声明前调用就是 ReferenceError。
class 声明和 let 一样有暂时性死区。声明前写 new User() 会抛 ReferenceError,不会像函数声明那样提前可用。
容易答错的地方
- 说
let和const不会提升 - 它们会提升,只是没有初始化。能说出「登记和初始化是两步」,再用遮蔽的例子证明,这个问题就答全了。
- 以为
typeof能安全检测任何变量 typeof只对从没声明过的名字安全。同一作用域后面有let或const声明时,声明前的typeof会抛错。- 以为顶层
let也会挂到window上 - 在浏览器脚本的顶层,
var声明的变量会变成window的属性,let和const不会,它们放在一个单独的全局词法作用域里。在 ES 模块里,两者都不会挂到window上。
面试官还会怎么问?
暂时性死区有什么好处?
声明前误用变量会立刻报错,而不是悄悄拿到 undefined,然后在很远的地方出现诡异的计算结果。报错位置就是出问题的位置,排查快很多。
const 声明的对象能改属性吗?
能。const 只保证这个变量不能再指向别的值,对象内部的属性照样能改。想让对象不可修改,要用 Object.freeze,而且它只冻结第一层。
var 在 if 块里声明,块外能读到吗?
能。var 只认函数作用域,if 和 for 的花括号拦不住它。哪怕 if 条件是 false、那一行根本没执行,变量也已经提升到函数顶部,读到的是 undefined。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。