前端系统化学习【JS篇】:(十二)JS中的变量提升机制

422 阅读9分钟

前言

  • 细阅此文章大概需要 10分钟\color{red}{10分钟}左右
  • 本篇中讲述了:
      1. 变量提升机制
      1. ES6新语法中的变量提升机制
      1. 匿名函数具名化
  • 如果有任何问题都可以留言给我,我看到了就会回复,如果我解决不了也可以一起探讨、学习。如果认为有任何错误都还请您不吝赐教,帮我指正,在下万分感谢。希望今后能和大家共同学习、进步。
  • 下一篇会尽快更新,已经写好的文章也会在今后随着理解加深或者加入一些图解而断断续续的进行修改。
  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!
  • 欢迎转载,注明出处即可。

JS中的变量提升机制

  • 声明和定义

- 声明【declare】:
    + 声明是创建变量的过程
        ```javascript
            var a;
        ```
- 定义【defined】:
    + 定义是赋值的过程【将变量和值关联在一起】
        ```javascript
            a = 10;
        ```

【ES3】变量提升:

  • 在当前上下文中(无论是在全局/私有/块级),JS代码 自上而下执行之前,浏览器会提前处理一些事情【可理解为在词法解析阶段的一个环节】, 会把当前上下文中【带var/Funtion关键字的进行提前声明或定义】
    1. var的只会提前声明

    2. function的会提前声明加定义【函数声明的方式创建的函数】

      • 声明函数名,并创建堆内存(堆内存中存储代码字符串)与之相关联
      • 注意: 若使用函数表达式【又称函数字面量】创建函数,则会根据前面的var/let/const关键字来确定如何声明该函数,建议使用此方法创建函数,在变量提升阶段就只会声明函数名,不会创建堆内存并将地址与函数名关联,就不会出现在函数创建之前可以被执行的情况
    3. 【ES6】中的LET和const不会进行变量提升

      • 在代码执行阶段,在let定义一个变量之前就使用它,浏览器会先看看下面的代码中有没有let声明该变量
        • 若声明了则会报错【不能再let声明之前使用该变量】
        • 否则报错【未定义】
    4. 在变量提升阶段声明过的变量,在代码执行阶段不会再重复声明。

    5. 基于VAR或FUNCTION,在【全局上下文】中声明的变量【全局变量】 ,会映射到GO(全局对象window)上一份,作为他的属性,而接下来对相应的进行修改时,一个修改另一个也会被修改。【严格模式也是如此】

          var a = 10;//var 创建全局变量
          console.log(a);//=>10
          console.log(window.a);//=>10,会被映射到window上一份a属性
          a = 20;// 修改全局变量
          console.log(a);//=>20
          console.log(window.a);//=>20被映射到window上的属性a也被修改
          window.a = 30;
          console.log(a);// =>30对应的全局变量也会修改
      
    6. 在上下文中的变量提升阶段,若【在条件判断当中】,无论条件是否成立,都要进行变量提升

      • FUNCTION在旧版本浏览器中【IE10以内】,会提前声明加定义
      • FUNCTION在新版本浏览器中【IE10以上】,只会提前声明,不会再提前定义了
             var foo = 1;
              function bar(){
                  if(!foo){//在函数执行时,!undefined=>转boolean类型再取反=>true
                  	console.log(foo);
                      var foo = 10;//变量提升阶段,私有上下文中,声明私有变量foo=>undefined
                  }
                  console.log(foo);//=>私有foo=10
      
                  }
                  bar();//执行函数时创建私有上下文,先进行变量提升
      
    7. 【注意】自执行函数/匿名函数在当前上下文中不进行变量提升,仅在其执行时,在其私有上下文中进行变量提升


【ES6】块级作用域

  • 在ES6中,基于let/const/function...创建变量
  • 如果是出现在 【非函数】【非对象】 的大括号中 【大括号中出现let/const/function...都会被认为是块级作用域】 ,则这个大括号的范围内相当于一个块级作用域【在执行时会创建私有上下文】,
    • {}中存在FUNCTION这个关键词会产生块级作用域也是新版浏览器才加入的
  • 不止是在IF中,新版浏览器为了保证语义的准确性,如果函数出现在【除函数/对象】的大括号{}中,形成了块级作用域,则在【全局上下文中在变量提升阶段,对于此FUNCTION只声明不定义】
  • 即使在大括号中同时出现LET\CONST\FUNCTION 和VAR,虽然会产生块级作用域,但是对VAR不生效
    • 【在块级作用域中VAR声明定义的变量会影响上级上下文,(上级上下文已存在的【被function/var声明 定义的】变量会被修改,不存在的则会被同步创建一个)】
    	/* 循环中的块级上下文 */
        for(let i = 0; i<5;i++){
            console.log(i);
        }
        //此时For循环的大括号就是一个块级作用域,
        //这个for就相当于拥有自己的私有上下文
        //当循环结束,一共产生了六个私有上下文/块级作用域
        //for{}由于有let,所以本身成为了一个,然后循环了五次每次又各创建了一个
    
  • 因为【新版本浏览器】要兼容ES3/ES6,在【遇到if(块级作用域)】中存在function时会形成块级作用域【再执行函数本身时依然会有自己的私有上下文】

    1. 在变量提升阶段,在EC(G)当中,只声明了该function a,【没有定义创建堆内存】【因为是新版本浏览器对于在if中的function a只声明不定义,而在【老版本浏览器】中在EC(G)下对函数声明+定义】

    2. 而在代码执行时,进入到块级作用域的私有上下文时,也会进行变量提升,此时会对function a声明加定义【创建堆内存并与变量a关联】

    3. 而在这个块级作用域的私有上下文的代码执行阶段,则不会在处理此行代码【做过的事情不会重复做】

    4. 【重点】【只是在全局下的块级作用域,再多一层就没有映射了,...】:

    • 但是浏览器会把在这个块级作用域的私有上下文中【===此行代码之前的【包含此行代码】===】 所有对变量a的操作,映射给全局一份,以此来兼容ES3,而【===此行代码之后的【不包含此行代码】===】所有对变量a的操作,就只是对块级作用域的私有上下文中的a的操作了
          var a = 0;
          if(true){
              a = 1;
              function a(){};
              a = 21;
              console.log(a);// 21
          }
          console.log(a);// 1
          // 块级作用域的私有上下文变量提升时映射给全局,全局a = 函数;
          // 接着私有上下文代码执行,a = 1;
          // 在function之前对a的所有操作,映射给全局一份,于是此时全局a = 1;
          // 接着私有a等于函数,私有上下文变量提升阶段搞过了不再重复,于是对全局a的影响到此为止,不会再映射。
          // 接下来对a的操作都只是对私有的a进行操作
    

       {
           function foo(){};
           foo = 1;
       }
       console.log(foo); //=> function foo
       // 全局上下文中仅声明了foo
       // 真正使全局foo = 函数的,是在块级作用域的私有上下文中,变量提升时,对私有foo声明加定义,映射给全局的
       // 而之后的foo = 1;完全时对私有进行操作,和全局再也没有关系了。
    

        {
            console.log(foo);//函数foo【私有已变量提升】
            console.log(window.foo);//undefined
            //【直到在代码执行时运行到最后一次function foo结束,才会将之前的操作映射给全局】,【代码执行到此时,还没进行映射】
            function foo(){};
            foo = 1;
            function foo(){};// 执行完这句,后面的才不会映射给全局
            console.log(foo);// 1
        }
        console.log(foo);// 1
        //全局上下文中仅声明了foo
        //真正使全局foo = 函数的,是在块级作用域的私有上下文中,变量提升时,对私有foo声明加定义,(所以处理后的在执行时不会再处理)
        //映射给全局的【按照在私有上下文的变量提升中function foo的最后一次算】
        //而之后私有上下文代码执行foo = 1;在function语句之前,映射给全局一份,全局a = 1;
        //于是对全局a的影响到此为止,之后完全时对私有进行操作,和全局再也没有关系了。
    

        var a = 12;//函数a【划掉】//13【最终】
        if(true){
            console.log(a);//函数a //块级作用域私有上下文变量提升,函数声明+定义
            a = 13;
            console.log(a);//13
            function a(){};//之前映射给全局
            a = 14;
            console.log(a);//14
        }
        console.log(a);//13【全局】
        
    

  • 进阶

        //EC(G)
        //变量提升
        //VAR X
        //FUNC = 函数
        var x = 1;
        function func (x,y = function anonymous1(){x = 2}){
            //EC(func)私有
            //作用域链<EC(func),EC(G)>
            //形参赋值x=5 y = 函数anonymous1【作用域EC(func)】
            //变量提升--
            x = 3;
            y();
                //EC(anonymous1)私有
                //作用域链<EC(anonymous1),EC(func)>
                //形参赋值--
                //变量提升--
                //x用的是上级上下文的
            console.log(x);// 2
        }
        func(5);
        console.log(x);// 1
    

    	//【函数体的大括号形成块级上下文的情况】
        //EC(G)
        //变量提升
        //VAR X
        //FUNC = 函数
        var x = 1;
        function func(x,y =function anonymous1(){x = 2}){
            //EC(func)私有
            //作用域链<EC(func),EC(G)>
            //形参赋值x=5 y = 函数anonymous1【作用域EC(func)】
            //变量提升--
                
            //=================================
            // 发现满足函数大括号形成块级上下文的条件(当中有var)
            //单独多形成一个私有的块级上下文(把函数体{}当作新的块级上下文)
            //EC(Block)私有
            //块级上下文的作用域链<EC(BLOCK),EC(FUNC)>
            //变量提升 var x  会把函数私有上下文中的形参赋值的值传递进来,赋值给声明的私有变量x = 5
            var x = 3;//EC(Block)把块级作用域中的x改为3
            y();//EC(Block)中没有私有变量y,向上查找执行EC(Func)中的y
                //EC(anonymous1)私有
                //作用域链<EC(anonymous1),EC(func)>
                //形参赋值--
                //变量提升--
                //x用的是上级上下文EC(func)的
            console.log(x);//3 =>但此处输出是在块级上下文中执行,输出的是块级上下文中的x
        }
        func(5);
        console.log(x);// 1
    

    //【函数体的大括号形成块级上下文的情况】
        //EC(G)
        //变量提升
        //VAR X
        //FUNC = 函数
        var x = 1;
        function func(x,y =function anonymous1(){x = 2}){
            //EC(func)私有
            //作用域链<EC(func),EC(G)>
            //形参赋值x=5 y = 函数anonymous1【作用域EC(func)】
            //变量提升--
            
            //=================================
            // 发现满足函数大括号形成块级上下文的条件
            //单独多形成一个私有的块级上下文(把函数体{}当作新的块级上下文)
            //EC(Block)私有
            //块级上下文的作用域链<EC(BLOCK),EC(FUNC)>
            //变量提升 var x var y  
            //形参赋值 x = 5 y =函数anonymous1() 把函数私有上下文中的形参赋值的值传递进来,赋值给声明的私有变量
            var x = 3;//EC(Block)把块级作用域中的x改为3
            var y = function anonymous2(){x = 4};//EC(Block)把块级作用域中的y改为函数anonymous2
            y();//执行EC(Block)中y指向的anonymous2,
                //EC(anonymous2)块级
                //作用域链<EC(anonymous2),EC(BLOCK)>
                //形参赋值--
                //变量提升--
                //x用的是块级上下文EC(BLOCK)的
                //x = 4
            console.log(x);//4=>此处输出是在块级上下文中执行,输出的是块级上下文中的x
        }
        func(5);
        console.log(x);// 1
    

  • 函数体的大括号在某些情况下也会形成块级上下文

    • 当一个函数 同时满足两个条件,则在本身执行时会形成私有上下文的同时,会再形成一个私有块级上下文【将函数体{}包起来的看作】。【函数执行就有两个上下文了】
      1. 有形参赋值默认值
      2. 函数体中有声明过自己的私有变量【仅限VAR/LET/CONST】【形参赋值和私有声明的变量不同也会生成】
        • FUNCTION只有在声明的名字和形参中的名字相同时,才会单独产生块级上下文】
        • VAR/LET/CONST】无论形参和私有变量声明是否相同都会生成
    • 【新形成的块级上下文】【上级上下文】【函数的私有上下文】,且会把【函数私有上下文】中的形参赋值传递进来,赋值给声明的私有变量

  • 匿名函数具名化:

    • 在使用函数表达式创建函数时,把原本作为值的【函数表达式匿名函数】“具名化”。
    • (虽说是起了名字,但是这个名字在创建函数的上下文以及更外部的上下文中访问不到=>也就是说不会在创建该函数的上下文中声明这个名字)
    • 当函数执行后,在函数形成的 私有上下文中,会把这个“具名化”的名字作为 私有上下文中的所声明的一个变量其值就是这个函数本身来处理
         //如
         var func = function AAA(){
            console.log(AAA);//=>ƒ AAA(){}【当前函数】
         };    
        console.log(AAA);//AAA is not defined