zoukankan      html  css  js  c++  java
  • js 面试的坑:变量提升

    全局中的解析和执行过程

    预处理:创建一个词法环境(LexicalEnvironment,在后面简写为LE),扫描JS中的用声明的方式声明的函数,用var定义的变量并将它们加到预处理阶段的词法环境中去。

    一、全局环境中如何理解预处理

    比如说下面的这段代码:

    复制代码
    var a = 1;//用var定义的变量,以赋值
    var b;//用var定义的变量,未赋值
    c = 3;//未定义,直接赋值
    function d(){//用声明的方式声明的函数
        console.log('hello');
    }
    var e = function(){//函数表达式
        console.log('world');
    }
    复制代码

    在预处理时它创建的词法作用域可以这样表示:

    复制代码
    LE{        //此时的LE相当于window
        a:undefined
        b:undefined
        没有c
        d:对函数的一个引用
        没有e
    }
    复制代码

    强调:1、预处理的函数必须是JS中用声明的方式声明的函数(不是函数表达式),看例子:

    复制代码
    d();
    e();
    function d(){//用声明的方式声明的函数
        console.log('hello');
    }
    var e = function(){//函数表达式
        console.log('world');
    }
    复制代码

    输出结果分别是:hello;报错e is not a function
    2、是用var定义的变量,看例子:

    复制代码
    console.log(a);//undefined
    console.log(b);//undefined
    console.log(c);//报错:c is not defined
    var a = 1;
    var b;
    c = 3;
    复制代码

    二、命名冲突的处理

    来看下面的代码:你觉得输出结果是什么?

    console.log(f);
    var f = 1;
    function f(){
        console.log('foodoir');
    }
    console.log(f);
    function f(){
        console.log('foodoir');
    }
    var f = 1;
    console.log(f);
    var f = 1;
    var f = 2;
    复制代码
    console.log(f);
    function f(){
        console.log('foodoir');
    }
    function f(){
        console.log('hello world');
    }
    复制代码

    你可能跟我开始一样,觉得输出的是foodoir,这样你就错了,你应该继续看看前面讲的预处理的问题。第一段代码输出的结果应该是:function f(){console.log('foodoir');}。

    看到第二段代码,你可能想都没想就回答结果是1,并且你还告诉原因说javascript里面的函数没有传统意义的重载。是的javascript里面的函数是没有重载,但是第二段代码的运行结果仍然是:function f(){console.log('foodoir');}。(原因后面作解释)

    如果你还觉得第三段代码的结果是2或者是1,那么我建议你回到前面看看关于预处理的例子。第三段的结果为:undefined。

    第四段代码的结果为function f(){console.log('hello world');}

    原因:处理函数声明有冲突时,会覆盖;处理变量声明有冲突时,会忽略。在既有函数声明又有变量声明的时候,你可以跟我一样像CSS中的权重那样去理解,函数声明的权重总是高一些,所以最终结果往往是指向函数声明的引用。

    三、全局函数的执行

    来看下面的例子:

    复制代码
     1 console.log(a);
     2 console.log(b);
     3 console.log(c);
     4 console.log(d);
     5 var a = 1;
     6 b = 2;
     7 console.log(b);
     8 function c(){
     9     console.log('c');
    10 }
    11 
    12 var d = function(){
    13     console.log('d');
    14 }
    15 console.log(d);
    复制代码

    1、我们先分析全局预处理的情况,结果如下:

    复制代码
    LE{
        a:undefined
        没有b
        c:对函数的一个引用
        d:undefined
    }
    复制代码

    此时,我们可以得到前四行代码得到的结果分别为:
      undefined
      报错
      function c(){console.log('c');
      undefined

    2、当执行完预处理后,代码开始一步步被解析(将第二行报错的代码注释掉)

    在第6行代码执行完,LE中的a的值变为1;

    复制代码
    LE{
        a:1
        没有b
        c:对函数的一个引用
        d:undefined
    }
    复制代码

    第7行代码执行完,LE中就有了b的值(且b的值为2,此时b的值直接变为全局变量);

    复制代码
    LE{
        a:1
        b:2
        c:对函数的一个引用
        d:undefined
    }
    复制代码

    第10行代码执行完,

    复制代码
    LE{
        a:1
        b:2
        c:指向函数
        d:undefined
    }
    复制代码

    第14行代码执行完,此时

    复制代码
    LE{
        a:1
        b:2
        c:指向函数
        d:指向函数
    }
    复制代码

    关于b变为全局变量的例子,我们在控制台中输入window.b,可以得到b的结果为2。结果如图:

    补充:运用词法的作用域,我们可以很好的解释一个带多个参数的函数只传递一个参数的例子。

    function f(a,b){
        
    }
    f(1);

    它的词法作用域可以这样解释:

    LE{
        a:1
        b:undefined
    }

    函数中的解析和执行过程

    函数中的解析和执行过程的区别不是很大,但是函数中有个arguments我们需要注意一下,我们来看下面的例子:

    复制代码
    function f(a,b){
        alert(a);
        alert(b);
        
        var b = 100;
        function a(){}
    }
    f(1,2);
    复制代码

    我们先来分析函数的预处理,它和全局的预处理类似,它的词法结构如下:

    复制代码
    LE{
        b:2
        a:指向函数的引用
        arguments:2
    }
    //arguments,调用函数时实际调用的参数个数
    复制代码

    再结合之前的那句话:处理函数声明有冲突时,会覆盖;处理变量声明时有冲突,会忽略。
    故结果分别为:function a(){}和2

    当传入的参数值有一个时:

    复制代码
    function f(a,b){
        alert(a);
        alert(b);
        
        var b = 100;
        function a(){}
    }
    f(1);
    复制代码

    这个时候的词法结构如下:

    LE{
        b:undefined
        a:对函数的一个引用
        arguments:1
    }

    故结果分别为:function a(){}和undefined
    还有一个需要注意的地方有:如果没有用var声明的变量,会变成最外部LE的成员,即全局变量

    复制代码
    function a(){
        function b(){
            g = 12;
        }
        b();
    }
    a();
    console.log(g);//12
    复制代码

    控制台结果:

    有了前面的基础,我们就可以对JS的作用域和作用域链进行深入的了解了。

    关于JS作用域和作用域链

    复制代码
    console.log(a);//undefined
    console.log(b);//undefined
    console.log(c);//c is not defined
    console.log(d);//d is not defined
    
    var a = 1;
    if(false){
        var b = 2;
    }else{
        c = 3;
    }
    function f(){
        var d = 4;
    }
    复制代码

    有了前面的基础我们很容易就可以得到前三个的结果,但是对于第四个却很是有疑问,这个时候,你就有必要看一看关于javascript作用域的相关知识了。
      在编程语言中,作用域一般可以分为四类:块级作用域、函数作用域、动态作用域、词法作用域(也称静态作用域)

    块级作用域

    在其它C类语言中,用大括号括起来的部分被称为作用域,但是在javascript并没有块级作用域,来看下面一个例子:

    for(var i=0;i<3;i++){
        //
    }
    console.log(i);

    它的结果为3,原因:执行完for循环后,此时的i的值为3,在后面仍有效

    函数作用域

    没有纯粹的函数的作用域

    动态作用域

    来看下面的例子:

    复制代码
    function f(){
        alert(x);
    }
    function f1(){
        var x = 1;
        f();
    }
    function f2(){
        var x = 1;
        f();
    }
    f1();
    f2();
    复制代码

    如果说存在动态作用域,那么结果应该是分别为1、2,但是最终结果并不是我们想要的,它的结果为:x is not defined。所以javascript也没有动态作用域

    词法作用域(也称静态作用域)

    我们可以在函数最前面声明一个x=100

    复制代码
    var x=100;
    function f(){
        alert(x);
    }
    function f1(){
        var x = 1;
        f();
    }
    function f2(){
        var x = 1;
        f();
    }
    f1();
    f2();
    复制代码

    结果为分别弹出两次100。说明javascript的作用域为静态作用域 ,分析:

    复制代码
    function f(){
        alert(x);
    }
    // f [[scope]]  == LE ==  window
    //创建一个作用域对象f [[scope]],它等于创建它时候的词法环境LE(据前面的知识我们又可以知道此时的词法环境等于window)
    
    function f1(){
        var x = 1;
        f();//真正执行的时候(一步一步往上找)LE  ->f.[[scope]]  ==  window
    }
    复制代码

    在词法解析阶段,就已经确定了相关的作用域。作用域还会形成一个相关的链条,我们称之为作用域链。来看下面的例子:

    复制代码
    function f(){    //f.scope == window
        var x = 100;//f.LE == {x:100,g:函数}
        
        var g = function(){//g.scope = f.LE        
            alert(x);
        }
        g();//在执行g的时候,先找g.scope,没有的话再找f.LE,还没有的话找f.scope……一直往上找window    
    }
    f();
    复制代码

    最终结果为:100
    来看一个经典的例子:

    复制代码
    //定义全局变量color,对于全局都适用,即在任何地方都可以使用全局变量color
    var color = "red";
    
    function changeColor(){
        //在changeColor()函数内部定义局部变量anotherColor,只在函数changeColor()里面有效
        var anotherColor = "blue";
        
        function swapColor(){
            //在swapColor()函数内部定义局部变量tempColor,只在函数swapColor()里面有效
            var tempColor = anotherColor;
            anotherColor = color;
            color = tempColor;
            
            //这里可以访问color、anotherColor和tempColor
            console.log(color);                //blue
            console.log(anotherColor);        //red
            console.log(tempColor);            //blue
        }
        
        swapColor();
        //这里只能访问color,不能访问anotherColor、tempColor
        console.log(color);                //blue
        console.log(anotherColor);        //anotherColor is not defined
        console.log(tempColor);            //tempColor is not defined
    }
    
    changeColor();
    //这里只能访问color
    console.log(color);                //blue
    console.log(anotherColor);        //anotherColor is not defined
    console.log(tempColor);            //tempColor is not defined
    复制代码

    还需要注意的是:new Function的情况又不一样

    复制代码
    var x= 123;
    function f(){
        var x = 100;
        //g.[[scope]]  == window
        var g = new Function("","alert(x)");
        g();
    }
    f();
    //结果为:123
    复制代码

    小结:

    以f1{ f2{ x}}为例,想得到x,首先会在函数里面的词法环境里面去找,还没找到去父级函数的词法环境里面去找……一直到window对象里面去找。
    这时候,问题来了。。。。

    问题1:到这里看来如果有多个函数都想要一个变量,每次都要写一个好麻烦啊,我们有什么方法可以偷懒没?

    方法:将变量设置为全局变量

    问题2:不是说要减少全局用量的使用么?因为在我们做大项目的时候难免要引入多个JS库,变量间的命名可能会有冲突,且出错后不易查找,这个时候我们该怎么办呢?

    方法:将变量设置在一个打的function中,比如下面这样:

    复制代码
    function(){
        var a = 1;
        var b = 2;
        function f(){
            alert(a);
        }
    }
    复制代码



    问题3:照你的这种方法我们在外面又访问不到了,怎么办?

    方法:我们使用匿名函数的方法,示例如下:

    复制代码
    (function(){
        var a = 1,
            b = 2;
        function f(){
            alert(a);
        }
        window.f = f;
    })();
    f();
    //结果为:1
     
  • 相关阅读:
    洛谷P3382 【模板】三分法(三分)
    BZOJ2194: 快速傅立叶之二(NTT,卷积)
    UOJ#206. 【APIO2016】Gap(交互,乱搞)
    洛谷P4245 【模板】MTT(任意模数NTT)
    快速数论变换(NTT)小结
    UOJ#34. 多项式乘法(NTT)
    BZOJ3864: Hero meet devil(dp套dp)
    POJ 1094 Sorting It All Out (拓扑排序)
    invesments 第三章 上
    51 EEPROM操作模板
  • 原文地址:https://www.cnblogs.com/libin-1/p/5979303.html
Copyright © 2011-2022 走看看