变量提升

什么是变量提升

  1. 在介绍变量提升之前,先来看看什么是 JavaScript 中的声明和赋值;
    1. 变量的声明和赋值:
      var myname = '张三'
      
      1. 这段代码可以把它看成是两行代码组成的:
      2. 如下图所示:
    2. 函数的声明和赋值:
      function foo(){
        console.log('foo')
      }
      
      var bar = function(){
        console.log('bar')
      }
      
      1. 第一个函数 foo 是一个完整的函数声明,也就是说没有涉及到赋值操作;第二个函数是先声明变量 bar,再把 function(){console.log('bar')} 赋值给 bar
      2. 为了直观理解,可以参考下图:
  2. 所谓的 变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的 “行为”;变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined
  3. 为了模拟变量提升的效果,对代码做了以下调整,如下图:

JavaScript 代码的执行流程

编译阶段

  1. 那么编译阶段和变量提升存在什么关系呢?为了搞清楚这个问题,还是回过头来看上面那段模拟变量提升的代码,为了方便介绍,可以把这段代码分成两部分;

  2. 第一部分:变量提升部分的代码;

    var myname = undefined
    function showName() {
        console.log('函数 showName 被执行');
    }
    
  3. 第二部分:执行部分的代码;

    showName()
    console.log(myname)
    myname = '张三'
    
  4. 下面就可以把 JavaScript 的执行流程细化,如下图所示:

    1. 从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文 (Execution context) 和可执行代码;
    2. 执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等;
    3. 可以简单地把变量环境对象看成是如下结构:
      VariableEnvironment:
        myname -> undefined, 
        showName ->function : {console.log(myname)}
      
    4. 了解完变量环境对象的结构后,接下来再结合下面这段代码来分析下是如何生成变量环境对象的:
      showName()
      console.log(myname)
      var myname = '张三'
      function showName() {
          console.log('函数 showName 被执行');
      }
      
      1. 第 1 行和第 2 行,由于这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处理;
      2. 第 3 行,由于这行是经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个名为 myname 的属性,并使用 undefined 对其初始化;
      3. 第 4 行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆 (HEAP) 中,并在环境对象中创建一个 showName 的属性,然后将该属性值指向堆中函数的位置;
    5. 这样就生成了变量环境对象;接下来 JavaScript 引擎会把声明以外的代码编译为字节码;

执行阶段

  1. JavaScript 引擎开始执行 “可执行代码”,按照顺序一行一行地执行;

  2. 下面就来一行一行分析下这个执行过程:

    1. 当执行到 showName 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出 “函数 showName 被执行” 结果;
    2. 接下来打印 “myname” 信息,JavaScript 引擎继续在变量环境对象中查找该对象,由于变量环境存在 myname 变量,并且其值为 undefined,所以这时候就输出 undefined
    3. 接下来执行第 3 行,把 “张三” 赋给 myname 变量,赋值后变量环境中的 myname 属性值改变为 “张三”

代码中出现相同的变量或者函数怎么办

  1. 先看下面这样一段代码:

    function showName() {
        console.log('李四');
    }
    showName();
    function showName() {
        console.log('张三');
    }
    showName(); 
    
  2. 分析上述代码完整执行流程:

    1. 首先是编译阶段:
      • 遇到了第一个 showName 函数,会将该函数体存放到变量环境中;
      • 接下来是第二个 showName 函数,继续存放至变量环境中,但是变量环境中已经存在一个 showName 函数了,此时,第二个 showName 函数会将第一个 showName 函数覆盖掉;
      • 这样变量环境中就只存在第二个 showName 函数了;
    2. 接下来是执行阶段:
      • 先执行第一个 showName 函数,但由于是从变量环境中查找 showName 函数,而变量环境中只保存了第二个 showName 函数,所以最终调用的是第二个函数,打印的内容是 “张三”
      • 第二次执行 showName 函数也是走同样的流程,所以输出的结果也是 “张三”

特点总结

  1. JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译;

  2. 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数;

  3. 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的;

块级作用域

var 缺陷以及为什么要引入 let 和 const

  1. 正是由于 JavaScript 存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷;

  2. 虽然 ES6 已经通过引入块级作用域并配合 letconst 关键字,来避开了这种设计缺陷,但是由于 JavaScript 需要保持向下兼容,所以变量提升在相当长一段时间内还会继续存在;

  3. 为了更好地理解和学习,这篇文章会先 “探病因” ——分析为什么在 JavaScript 中会存在变量提升,以及变量提升所带来的问题;然后再来 “开药方” ——介绍如何通过块级作用域并配合 letconst 关键字来修复这种缺陷;

作用域

  1. 为什么 JavaScript 中会存在变量提升这个特性,而其他语言似乎都没有这个特性呢?要讲清楚这个问题,就得先从作用域讲起;

  2. 作用域:是指在程序中定义变量的区域,该位置决定了变量的生命周期 (通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期)

  3. ES6 之前,ES 的作用域只有两种:

    1. 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期;
    2. 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问;函数执行结束之后,函数内部定义的变量会被销毁;
  4. ES6 之前,JavaScript 只支持这两种作用域,相较而言,其他语言则都普遍支持块级作用域;其代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁;

    //if 块
    if(1){}
    
    //while 块
    while(1){}
    
    // 函数块
    function foo(){
    
    //for 循环块
    for(let i = 0; i<100; i++){}
    
    // 单独一个块
    {}
    
  5. JavaC/C++ 不同,ES6 之前是不支持块级作用域的,因为当初设计这门语言的时候,并没有想到 JavaScript 会火起来,所以只是按照最简单的方式来设计;没有了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升;

变量提升所带来的问题

变量容易在不被察觉的情况下被覆盖掉

  1. 执行下面这段代码,打印出来的是 undefined

    var myname = "张三"
    function showName(){
        console.log(myname);
        if(0){
            var myname = "李四"
        }
        console.log(myname);
    }
    showName();
    
  2. 首先当刚执行到 showName 函数调用时,执行上下文和调用栈的状态如下:

  3. showName 函数的执行上下文创建后,JavaScript 引擎便开始执行 showName 函数内部的代码了;首先执行的是第三行代码:

    console.log(myname);
    
  4. 执行这段代码需要使用变量 myname,结合上面的调用栈状态图,可以看到这里有两个 myname 变量:一个在全局执行上下文中,其值是 “张三”;另外一个在 showName 函数的执行上下文中,其值是 undefined

  5. “当然是先使用函数执行上下文里面的变量啦!” 的确是这样,这是因为在函数执行过程中,JavaScript 会优先从当前的执行上下文中查找变量,由于变量提升,当前的执行上下文中就包含了变量 myname,而值是 undefined,所以获取到的 myname 的值就是 undefined

本应销毁的变量没有被销毁

  1. 接下来再来看下面这段让人误解更大的代码:在 for 循环结束之后,i 的值并未被销毁,所以最后打印出来的是 7

    function foo(){
        for (var i = 0; i < 7; i++) { }
        console.log(i); 
    }
    foo()
    
  2. 这同样也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁;

ES6 是如何解决变量提升带来的缺陷

  1. 为了解决上述问题,ES6 引入了 letconst 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域;

  2. 先参考下面这段存在变量提升的代码:

    function varTest() {
      var x = 1;
      if (true) {
        var x = 2;  // 同样的变量!
        console.log(x);  // 2
      }
      console.log(x);  // 2
    }
    
    1. 在这段代码中,有两个地方都定义了变量 x,第一个地方在函数块的顶部,第二个地方在 if 块的内部,由于 var 的作用范围是整个函数,所以在编译阶段,会生成如下的执行上下文:
    2. 从执行上下文的变量环境中可以看出,最终只生成了一个变量 x,函数体内所有对 x 的赋值操作都会直接改变变量环境中的 x 值;
    3. 所以上述代码最后通过 console.log(x) 输出的是 2,而对于相同逻辑的代码,其他语言最后一步输出的值应该是 1,因为在 if 块里面的声明不应该影响到块外面的变量;
  3. 改造上面的代码,让其支持块级作用:

    function letTest() {
      let x = 1;
      if (true) {
        let x = 2;  // 不同的变量
        console.log(x);  // 2
      }
      console.log(x);  // 1
    }
    
    1. 执行这段代码,其输出结果就和我们的预期是一致的;
    2. 这是因为 let 关键字是支持块级作用域的,所以在编译阶段,JavaScript 引擎并不会把 if 块中通过 let 声明的变量存放到变量环境中,这也就意味着在 if 块通过 let 声明的关键字,并不会提升到全函数可见;
    3. 所以在 if 块之内打印出来的值是 2,跳出语块之后,打印出来的值就是 1 了;

JavaScript 是如何支持块级作用域的

  1. 在同一段代码中,ES6 是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?

  2. 先看下面这段代码:

    function foo(){
        var a = 1
        let b = 2
        {
            let b = 3
            var c = 4
            let d = 5
            console.log(a)
            console.log(b)
        }
        console.log(b) 
        console.log(c)
        console.log(d)
    }   
    foo()
    
  3. 接下来一步步分析上面这段代码的执行流程:

    1. 第一步是编译并创建执行上下文,下面是 foo 执行上下文示意图,可以得出以下结论:
      • 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了;
      • 通过 let 声明的变量,在编译阶段会被存放到词法环境 (Lexical Environment) 中;
      • 在函数的作用域内部,通过 let 声明的变量并没有被存放到词法环境中;
    2. 第二步继续执行代码,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2,这时候函数的执行上下文就如下图所示:
      • 从图中可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在;
      • 其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构;
      • 再接下来,当执行到作用域块中的 console.log(a) 这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找;这样一个变量查找过程就完成了,可以参考下图:
      • 当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示:
    3. 通过上面的分析,想必已经理解了词法环境的结构和工作机制,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了;

声明变量关键字

  1. JavaScript 中,一共存在 3 种声明变量的方式:varletconst

  2. 之所以有 3 种方式,这是由于历史原因造成的,最初声明变量的关键字就是 var ,但是为了解决作用域的问题,所以后面新增了 letconst 的方式;

var 关键字

变量提升

  1. 在栈内存(作用域)形成,JS 代码自上而下执行之前,浏览器会把所有带 「VAR」「FUNCTION」 关键字进行提前声明或者定义;

    1. 声明(declare): var a (默认值是 undefinedfunction sum (值是 16 进制堆内存地址);
    2. 定义(defined): a = 12 (定义其实就是赋值)
  2. 变量提升只发生在当前作用域

    1. 加载页面的时候只对全局作用域下的变量进行变量提升,因为此时的函数中存储的都是 代码字符串 而已;
    2. 在全局作用域下、私有作用域下声明的函数或变量(带 VARFUNCTION 的才是声明);
    3. 浏览器很懒,做过的事情不会重复执行第二遍 (当代码遇到创建函数代码,直接跳过,因为在变量提升阶段已经 声明并定义 了)
    4. ES3/ES5 规范: 只有全局作用域和函数执行的私有作用域(栈内存),其他花括号不会形成栈内存;

带不带 VAR 的区别

  1. 全局作用域

    1. 带 var (本质是变量):在全局作用域下声明一个变量也相当于给 window 设置了一个属性,属性值为变量值,在变量提升阶段,就已经把变量设置成 window 的属性了(let 声明的变量不会给 window 设置该属性),全局变量修改 window 属性也修改,反之同样修改;
    2. 不带 var:本质是 window 下的属性,a = 12 相当于 window.a = 12
  2. 私有作用域

    1. 带 var (本质是变量):私有作用域带 var ,变量提升阶段,都声明为私有变量,和外界没有任何的关系;
    2. 不带 var:会向上级作用域查找,查看是否是上级的变量,若不是则一直查找到 window 为止,如果 window 也没有,相当于给 window 设置了一个属性(作用域链);

let 关键字

const 关键字

特点总结

  1. var 关键字

    1. 没有块级作用域的概念
    2. 有全局作用域、函数作用域的概念
    3. 不初始化值默认为 undefined
    4. 存在变量提升
    5. 全局作用域用 var 声明的变量会挂载到 window 对象下
    6. 同一作用域中允许重复声明
  2. let 关键字

    1. 有块级作用域的概念
    2. 不存在变量提升
    3. 暂时性死区
    4. 同一块作用域中不允许重复声明
  3. const 关键字

    1. let 特性一样,仅有 2 个差别
    2. 区别 1:必须立即初始化,不能留到以后赋值
    3. 区别 2:常量的值不能改变

面试题

看下面这样一段代码,能通过分析词法环境,得出来最终的打印结果吗

let myname= '李四'
{
  console.log(myname) 
  let myname= '张三'
}

// 解答
// VM6277:3 Uncaught ReferenceError: Cannot access 'myname' before initialization
// 进入块级作用域不会有编译过程,只不过通过let或者const声明的变量会在进入块级作用域的时被创建,但是在该变量没有赋值之前,引用该变量JavaScript引擎会抛出错误---这就是“暂时性死区”

输出 0-9

// 第一种
for (var i = 0; i < 10; i++) {
  // 每一轮都生成一个自执行函数,形成全新的执行上下文 EC
  // 并且把每一轮循环的 i 当做实参传给私有 上下文中的私有变量 i(形参变量)
  // 定时器触发执行用到的 i 都是私有 EC 中的保留下来的 i
  // 充分利用闭包的保存机制(闭包有保护和保存 2 个机制)来完成的,这样处理不太好,     
  //循环 多次就会产生多个不销毁的 EC
  ~function (i) {
    setTimeout(function () {
        console.log(i);
    }, 10);
  }(i);
}

// 第二种
// let 存在块级作用域,var 没有
for (let i = 0; i < 10; i++) {
  // 形成了 10 个块级作用域,每个块级作用域中都有一个私有变量 i
  setTimeout(function () {
    console.log(i);
  }, 10);
}

let const var 的区别?什么是块级作用域?如何用?

let const var 的区别?

  1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升;
  2. let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明;
  3. const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明;

什么是块级作用域?

  1. 最初在 JS 中作用域有:全局作用域、函数作用域,没有块作用域的概念;
  2. ES6 中新增了块级作用域,块作用域由 { } 包括,if 语句和 for 语句里面的 { } 也属于块作用域;

如何用?

  1. 在以前没有块作用域的时候,在 if 或者 for 循环中声明的变量会泄露成全局变量;其次就是 { } 中的内层变量可能会覆盖外层变量;
  2. 块级作用域的出现解决了这些问题;
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 变量提升
    1. 1.1. 什么是变量提升
    2. 1.2. JavaScript 代码的执行流程
      1. 1.2.1. 编译阶段
      2. 1.2.2. 执行阶段
    3. 1.3. 代码中出现相同的变量或者函数怎么办
    4. 1.4. 特点总结
  2. 2. 块级作用域
    1. 2.1. var 缺陷以及为什么要引入 let 和 const
    2. 2.2. 作用域
    3. 2.3. 变量提升所带来的问题
      1. 2.3.1. 变量容易在不被察觉的情况下被覆盖掉
      2. 2.3.2. 本应销毁的变量没有被销毁
    4. 2.4. ES6 是如何解决变量提升带来的缺陷
    5. 2.5. JavaScript 是如何支持块级作用域的
  3. 3. 声明变量关键字
    1. 3.1. var 关键字
      1. 3.1.1. 变量提升
      2. 3.1.2. 带不带 VAR 的区别
    2. 3.2. let 关键字
    3. 3.3. const 关键字
    4. 3.4. 特点总结
  4. 4. 面试题
    1. 4.1. 看下面这样一段代码,能通过分析词法环境,得出来最终的打印结果吗
    2. 4.2. 输出 0-9
    3. 4.3. let const var 的区别?什么是块级作用域?如何用?