5行代码证明 let, const存在变量提升

1,682 阅读5分钟

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:
    console.log(aVar); // undefined
    console.log(aLet); // causes ReferenceError: aLet is not defined
    var aVar = 1;
    let aLet = 2;
    
    从结果上看,第二行没有找到aLet导致程序报错,表明let声明的变量并没有提升



  • 例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推出的新规范,即是 在没有声明变量前,不允许访问



  • 例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 才对啊!

3.变量创建的三个阶段

变量创建的三个阶段:
1、创建(声明)变量,开辟内存空间
2、初始化,变量赋值为undefined
3、给变量赋值


let 的「创建」过程被提升了,但是初始化没有提升。(1步)
var 的「创建」和「初始化」都被提升了。(12步)
function 的「创建」「初始化」和「赋值」都被提升了。(123步)

看完上面的问题,再回过头来看 变量创建的三个阶段,就会自然得出结论了:

  • 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 不存在变量提升而已。这只是个表象。



  • 参考文章