1.啥也不说,先看这5行代码
let a = 1
{
console.log(a) // 问:这里会输出什么?
let a = 2
}
2.正片开始
- 一般情况下,我们会认为 上面代码
console.log(a)会输出1,因为let不存在变量提升,而去读取到全局变量中的let a = 1。 - 但是事实是,当你把代码 跑一遍的时候会发现,这里并不会输出
1,而是Uncaught ReferenceError: Cannot access 'a' before initialization - 这是为什么呢?
- 在开始之前,先做一个科普
变量创建的三个阶段: 1、创建(声明)变量,开辟内存空间 2、初始化,变量赋值为undefined 3、给变量赋值
别着急,先看这几个例子
请在浏览器环境运行,node环境结果不一样
浏览器里的报错Cannot access 'a' before initialization
在node 环境下 都会成为a is not defined
- 例1:
从结果上看,第二行没有找到aLet导致程序报错,表明let声明的变量并没有提升console.log(aVar); // undefined console.log(aLet); // causes ReferenceError: aLet is not defined var aVar = 1; let aLet = 2;
- 例2:
- 1.如果访问一个没声明过的变量,会直接报错
console.log(a) // Uncaught ReferenceError: a is not defined - 2.在块级作用域内 也是一样
{ console.log(a) // Uncaught ReferenceError: a is not defined } - 3.但是,如果这样写 报错就完全不一样了
{ console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization let a } - 发生了什么?
- 这里遇到了**
暂时性死区** 的问题。
- 这里遇到了**
- 那么什么是 暂时性死区 呢?
- 这是ES6推出的新规范,即是
在没有声明变量前,不允许访问
- 这是ES6推出的新规范,即是
- 1.如果访问一个没声明过的变量,会直接报错
- 例3:
- 1.我们先看看
let重复定义的报错是怎样的{ let a = 1 let a = 2 } // Uncaught SyntaxError: Identifier 'a' has already been declared - 2.我们声明一个变量,再访问这个变量
let a = 1 console.log(a) // 1 - 3.声明一个变量,在子作用域里 访问这个变量
let a = 1 { console.log(a) // 1 } - 4.但是,如果我们在里面这样 加一句
let a = 1 { console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization let a = 2 } - 这样看来,总让人很疑惑:
- 他就会直接报错了。这里也遇到了
暂时性死区的问题 - 但是,仔细想想,这里直接报错了,真的合理吗?
- 问题1:为啥这里不能读取得到 外层作用域的
let a = 1?而是直接给我报错了? 我真的错了吗? - 问题2:这里也不属于
let重复定义时 报的那种错误提示,这也不是重复定义同一变量的错误 - 问题3:如果
let变量真的不存在变量提升,那它就该能够访问到 外层作用域的let a = 1才对啊!
- 问题1:为啥这里不能读取得到 外层作用域的
- 他就会直接报错了。这里也遇到了
- 1.我们先看看
3.变量创建的三个阶段
变量创建的三个阶段:
1、创建(声明)变量,开辟内存空间
2、初始化,变量赋值为undefined
3、给变量赋值
let 的「创建」过程被提升了,但是初始化没有提升。(1步)
var 的「创建」和「初始化」都被提升了。(1、2步)
function 的「创建」「初始化」和「赋值」都被提升了。(1、2、3步)
看完上面的问题,再回过头来看 变量创建的三个阶段,就会自然得出结论了:
-
let、const 的「创建」过程被提升了,但是初始化没有提升。
-
而且ES6规定,在 let、const 变量声明之前 不允许访问(暂时性死区)
-
所以,上面的 5行代码实际上 等价于
let a = 1 { console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization let a = 2 } // ↓↓↓↓ 等价于 ↓↓↓↓ let a = 1 { let a // a 变量 的「创建」过程被提升了,但「初始化」没被提升, a 还没被赋值为 undefined,还不允许访问(暂时性死区) console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization a = 2 // a 的「初始化」和「赋值」都被执行完毕,可以访问了 }
4.总结
- 综上所述,综合了那么多的矛盾,总结得出合理的解释,就是 let, const存在变量提升
5.题外话
- 其实,let, const 到底是否存在变量提升,这是一个比较有争议性的话题。即使在英文外网环境下,这个问题也是 比较有争议性的,因为没法得出一个
大家都信服的、一眼就能辨别真假的、不可以被质疑的、实打实的证据,来证明这个问题的真伪。
6.底层原理
以下是个人理解,不喜勿喷
-
由于 let、const 这种变量声明,他的创建过程是走三步的,「创建」「初始化」和「赋值」
-
ES5 中的 var 由于「创建」和「初始化」都被提升了,所以在任何地方都可以被访问
- 在没有「赋值」前,访问得到的是
undefined - 「赋值」后访问的就是 赋给它的值
- 在没有「赋值」前,访问得到的是
-
但是,由于 let 的「创建」过程被提升了,但是初始化没有提升。
- 意思就是说 虽然系统 给 let 开辟了内存空间(创建),但是没有「初始化」
- 所以 在「创建」到「初始化」这个过程之间,如果你访问了,又不能给你返回什么。如果实在要返回,
那就只能给你返回 开辟出的内存空间地址了 - 但是,在JS引擎中,你拿着
内存空间地址有什么意义? - 没意义!所以,逼不得已,ES6规范中,只能强行规定了
暂时性死区这个概念,来规避这种情况。如果你在暂时性死区的范围内范围了这个变量,就会直接报错。 - 所以,综上所述,
let、const这种变量声明,事实上是有变量提升,只不过,JS引擎 不能给你返回内存地址,所以看起来let、const不存在变量提升而已。这只是个表象。
-
参考文章