JavaScript预解析(变量提升)问题 (大杂烩)

问题背景

遇到了个小问题,问大家都说是作用域的问题.我觉得不对啊.于是做个笔记记录下来.

本文涉及到 JavaScript执行机制 / 作用域 / 作用域链 / Js预解析 / this指向 / 改变this指向 / var重复声明.菜比较多.

首先先牵扯到的是JavaScript是单线程语言.代码的执行顺序是自上而下的执行代码.

我的理解就像是高速公路收费站一样,一次只能过一辆车.但是如果真的完全是这样的话,就会出问题.好比说:如果调用函数在函数声明前执行,那么就会报错.

  • 所以.聪明的JavaScript的执行机制其实是:
    • 第一步 : 创建
    • 第二步 : 执行
      • 变量赋值 / 函数引用等

以上执行机制内容来自JavaScript闭包 - Web前端工程师面试题讲解

下图来自掘金:

两个内容的差异是名词叫法的不同.

为了简短文字.下文描述JavaScript预解析阶段,只描述创建步骤只描述创建变量对象这一步.

代码

代码一

    // console.log(a); 这个时候b是等于undefined的.并没有进行赋值.
        var a = 66;
        function test1(a) {
            console.log(a); // function a(){};
            function a() { };
            var a = 5;
        }
        test1(a);

这段代码的console.log(a)的结果是 function a(){};

  • 首先是全局作用域(在浏览器下全局作用域是window)的预解析
    • 解析出 var a (这个时候b是等于undefined的.并没有赋值) 和 function test1
  • 之后是执行代码.执行到test(a)代码的时候.给参数a赋值66(参数就是一个局部变量).
  • 当执行test(a)的时候.复制全局变量a=66传递给 function test1(a)中的啊;
  • 进入fuction test1()的作用域.执行function的预解析.
    • 解析出 function a(){} 和 var a.
    • 这个时候如果函数名称和变量名称的变量名是一样的话.那么该变量名会指向函数.和代码执行顺序没有关系

代码二

        var b = 66;
        function test2(b) {
            console.log(b); // 输出 66
            var b = 5;
        }
        test2(b);


这段代码的console.log(b)的结果是 66.

  • 首先是全局作用域的预解析 var b. function test()
  • 代码执行阶段:
    • 把全局变量的b复制给test(b)中的b(局部变量).传递给函数test().
  • 执行function test2()的预解析.
    • var b.因为已经存在一个相同的局部变量(函数传递进来的b),那么就不会再执行这次的预解析 var b (参考博文 :为什么var可以重复声明)
    • 执行阶段:
      console.log(b)的结果是 66 .这个66是通过函数进来的66

而 :

  var b = 66;
function test2(b) {
    var b = 5;
    console.log(b); // 输出 5
}
test2(b);

输出的结果就会是5.因为 var b = 5 把 局部变量给覆盖掉了.进行了一次重新赋值

重复声明时:首先编译器对代码进行分析拆解,从左至右遇见var a,则编译器会询问作用域是否已经存在叫a的变量了。如果不存在,则招呼作用域声明一个新的变量a;若已经存在,则忽略 var 继续向下编译,这时 a = 2被编译成可执行的代码供引擎使用。

参考博文 : 为什么var可以重复声明

代码三

        function test3(b) {
            console.log(b); // 输出 undefined
            var b = 5;
        }
        test3();

这段代码的console.log(a)的结果是 undefined

  • 首先是全局作用域(在浏览器下全局作用域是window)初始化. function test3
  • 之后执行代码阶段. 执行test3()
    • 而在函数中test3(b)需要一个局部变量b.函数并没有传递进来参数.此刻函数中的参数b为undefined.
  • 之后执行test3()局部作用域的预解析阶段.
    • 开始预解析 var a .但是此刻局部作用域中已经存在变量名等于 b的局部变量.所以忽略此次.
    • 函数的执行阶段. console.log(b); 此刻的b是等于undefined的.输出undefined

代码四

        var a = 100;
        function fns() {
            console.log(a); //undefined
            var a = 200;
            console.log(a); //200
        }
        fns();
        console.log(a); //100
        var a;
        console.log(a); //100
        var a = 300;
        console.log(a); //300

输出结果见上面的注释.

  • 首先是全局作用域的预解析阶段:
    • var a ; function fns ;
  • 之后是执行全局作用域的代码.
  • 执行 fns()
    • 局部作用域 fns()的预解析.
      • 解析出 var a.
    • fns()执行阶段
      • console.log(a). 此刻的a只是预解析.并没有进行赋值.所以第一个console.log(a)输出undefined
      • 等到函数体内第二次执行 console.log(a)的时候.此刻上一行代码已经赋值给a一个200.此刻输出是200
  • fns()代码执行结束.函数体内的局部变量被销毁.再次执行console.log(a);此刻输出的是100.当再次声明var a;之后,再进行输出仍然是100.因为此刻全局作用域已经存在名称为a的全局变量了.并且没有进行赋值.JavaScript会直接跳过去(在Chrome中Debug下.此刻代码直接跳过去,并未执行.).接着执行 var a = 300; 此刻赋值给全局作用域中的a一个300.当再次 console.log(a),a等于300

代码五

        var a = 12;
        var a; // 因为值没有覆盖
        console.log(a); // 12

赋值时:引擎遇见a=2时同样会询问在当前的作用域下是否有变量a。若存在,则将a赋值为2(由于第一步编译器忽略了重复声明的var,且作用域中已经有a,所以重复声明会发生值的覆盖而不会报错);若不存在,则顺着作用域链向上查找,若最终找到了变量a则将其赋值2,若没有找到,则招呼作用域声明一个变量a并赋值为2(这就是为什么第二段代码可以正确执行且a变量为全局变量的原因,当然,在严格模式下JS会直接抛出异常:a is not defined)。
重复声明时:首先编译器对代码进行分析拆解,从左至右遇见var a,则编译器会询问作用域是否已经存在叫a的变量了。如果不存在,则招呼作用域声明一个新的变量a;若已经存在,则忽略 var 继续向下编译,这时 a = 2被编译成可执行的代码供引擎使用。

来自 https://blog.csdn.net/DurianPudding/article/details/87953939

代码六

    
        var num1 = 55;
        var num2 = 66;

        function fn_2(num, num2) {
            console.log(num);// 66
            num = 100; // 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在num.num在此作用域中存在(函数的参数).所以将100赋值给局部作用域中的num变量.
            num1 = 100; // 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在num1.num1在此作用域中不存在.顺着作用域链在全局作用域中找到了num1.所以将100赋值给全局作用域中的num变量.
            num2 = 100;// 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在num2.num2在此作用域中存在.所以将100赋值给局部作用域中的num2变量.
            numall = 888; // 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在numall.numall在此作用域中不存在.顺着作用域链在全局作用域中寻找是否存在numall.全局作用域中也不存在numall.所以会在全局作用域中创建一个属性.也就是window.numall.
            console.log(num);// 100
            console.log(num1); //100
            console.log(num2); // 100
        }
        fn_2(num1, num2);
        console.log(num1); // 100
        console.log(num2); // 66
        //console.log(num); // 报错 这行代码会报错.因为全局作用域中不存在num
        console.log(numall); // 100
        console.log(window)

      

首先执行全局作用域的预解析. var num1 ; var num2 ; function fn_2; 此刻函数表达式是可以立即执行的,但是变量是等于undefined.

读取到fn_2(num1, num2); 函数fn_2()需要两个局部函数 num和num2. 在调用fn_2的时候(fn_2(num1,num2)). 传递进去(赋值)的参数实际上是全局变量的num1与num2.

  • 执行函数fn_2(num,num2).
    • 预解析fn_2(num,num2)的局部作用域里的内容.
      • 并没有var 和 函数表达式. 预解析完成.
      • (我猜是是存在var变量的,会先预解析函数表达式中的两个参数(局部变量).之后最先执行赋值把num1 与 num2 传递给 num, num2)
    • 执行代码.
      • console.log(num). 此刻输出是值是66
      • 之后执行下面四行赋值语句.(如果变量前面有加var的话,并且顺着作用域链找到此变量的话,那么会在全局对象下创建一个属性.(也有人说是全局变量))具体参见 https://www.cnblogs.com/liuna/p/6140901.html
        而 num = 1;
        事实上是对属性赋值操作。首先,它会尝试在当前作用域链(如在方法中声明,则当前作用域链代表全局作用域和方法局部作用域etc。。。)中解析 num; 如果在任何当前作用域链中找到num,则会执行对num属性赋值; 如果没有找到num,它才会在全局对象(即当前作用域链的最顶层对象,如window对象)中创造num属性并赋值。
        []()

代码七


        fn3();
        console.log(c); // 9
        console.log(b); // 9
        console.log(a); // 报错

        function fn3() {
            var a = b = c = 9;
            /*
            以上代码相当于
                c = 9;
                b = c;
                var a = b;
            */
            console.log(a);//9
            console.log(b);//9
            console.log(c);//9
        }
      
  • 首先执行全局作用域的预解析: function fn3()
  • 执行代码.遇到fn3(). 执行函数fn3
  • 执行函数fn3()的预解析. var a
  • 执行代码:
    • c = 9;b=c;var a = b;
    • c = 9 : 首先执行c = 9.会查看当前所在的局部作用域中是否存在c:c不存在.沿着作用域链向上查找.查找全局作用域(window)中是否存在c:c也是不存在.所以会在全局作用域中创建一个c的属性.window.c = 9
    • b = c : 首先会查找c.c是否存在当前作用域:c不存在.c是否存在全局作用域:c存在.之后取得其值.然后查找b.b是否存在当前局部作用域:不存在.b是否存在全局作用域:不存在.之后会在全局作用域下创建一个window.b的属性.window.b的值等于window.c的值
    • var a = b: 首先会查找b的值.b是否存在当前作用域:b不存在.b是否存在全局作用域:b存在.之后取得其值.接着会查找a是否存在于当前作用域:a存在.之后把window.b的值赋给var a.

javaScript预解析

js赋值语句执行顺序 // js赋值语句执行:自右向左(仅指简单的赋值.如上).

代码八

       var n = 0;
        function a() {
            var n = 10;
            function b() {
                n++;
                console.log(n);
                return n;
            }
            b();
            return b;
        }
        var c = a(); // 11
        var now = c(); //12
        console.log(n); // n == 0 上图是一个闭包
        console.log(now) // 12

这是一个闭包. var c 在引用 function b(){...}.

概念性的东西写起来太长.推荐看书去理解.

代码九

  var x = 10;

        function fn() {
            console.log(x); // 答案是10
           
            console.log(this); //window
        }
        function show(f) {
            var obj = { x: 20 };
            f();
        }
        show(fn);

全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
作用域与作用域链

        var x1 = 10;

       function fn1() {
           console.log(this.x1);
       }
       function show1(f) {

           let obj = { x1: 66 };
           var x1 = 20;
           f();//10
           f.call(obj); // 使用call改变了this指向 =>66
       }
       show1(fn1);

代码十

        var fn9 = function () {
            console.log(fn9);
        }

        var objs = {
            fnn: function () {
                console.log(fn9);
                console.log(this.fn9);
                console.log(this); // this指向obj.
            }
        }

        objs.fnn();


代码十一


        console.log(test_haha); // 函数声明的优先级是高于var的
        function test_haha() { };
        var test_haha = 66;

经过测试,在预解析阶段,函数声明的优先级是高于var变量的. console.log(test_haha);输出的是函数体,尽管var在最下面.

代码十二

var objs = {
            fn9: function () {
                console.log(fn9); // 输出      var fn9 = function () {console.log(fn9);}
                console.log(this.fn9); // 输出 objs.fn9
                console.log(this); // this指向obj.
            },
        }

        objs.fn9();

谁调用this,this指向谁.

js里只有全局作用域与函数作用域.在ES6中出现了块级作用域.还没有学到.
深入理解JavaScript作用域和作用域链

代码十三

        var a;
        if (true) {
            a = 5;
            function a() {
                console.log('我没有被楼下的骚a给霸占了');
            };
            a = 0
            console.log(a);
        }
        console.log(a)

这段代码在chrome与ie下的输出结果是不一致的.
不要在判断语句里定义函数.




参考

猜你喜欢

转载自www.cnblogs.com/gtscool/p/12636616.html