关于作用域:javascript函数作用域和提升

Javascript function scoping and hoisting

我刚刚读了一篇关于Javascript范围界定和提升的伟大文章,其中BenCherry给出了以下示例:

1
2
3
4
5
6
7
8
9
10
var a = 1;

function b() {
    a = 10;
    return;

    function a() {}
}
b();
alert(a);

使用上述代码,浏览器将发出警报"1"。

我仍然不确定为什么它返回"1"。他说的一些事情浮现在脑海中,比如:所有函数声明都将被提升到顶部。您可以使用函数来确定变量的范围。仍然不点击我。


功能提升是指将功能移动到其范围的顶部。也就是说,

1
2
3
4
5
function b() {  
   a = 10;  
   return;  
   function a() {}
}

会被介入器重写成这个

1
2
3
4
5
function b() {
  function a() {}
  a = 10;
  return;
}

奇怪,嗯?

在这种情况下,

1
function a() {}

行为与

1
var a = function () {};

因此,本质上,这就是代码所做的:

1
2
3
4
5
6
7
8
var a = 1;                 //defines"a" in global scope
function b() {  
   var a = function () {}; //defines"a" in local scope
   a = 10;                 //overwrites local variable"a"
   return;      
}      
b();      
alert(a);                 //alerts global variable"a"


您必须记住的是,它在执行前解析整个函数并解析所有变量声明。所以…

1
function a() {}

真正成为

1
var a = function () {}

var a强制它进入局部作用域,而变量作用域是贯穿整个函数的,因此全局a变量仍然是1,因为您已经通过使其成为一个函数来声明a进入局部作用域。


功能a悬挂在功能b内:

1
2
3
4
5
6
7
8
var a = 1;
function b() {
   function a() {}
   a = 10;
   return;
}
b();
alert(a);

就像使用var一样:

1
2
3
4
5
6
7
8
var a = 1;
function b() {
   var a = function () {};
   a = 10;
   return;
}
b();
alert(a);

该函数在本地声明,设置a只在本地范围内发生,而不是全局var。


  • 函数声明function a(){}首先被提升,其行为类似于var a = function () {};,因此在本地范围内创建了a
  • 如果有两个同名的变量(一个在全局,另一个在本地),则局部变量总是优先于全局变量。
  • 设置a=10时,设置的是局部变量a,而不是全局变量。
  • 因此,全局变量的值保持不变,您将收到警报1


    在这一小段代码中,争论的焦点是什么?

    案例1:

    function b的主体中包括function a(){}的定义,如下所示。logs value of a = 1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = 1;
    function b() {
      a = 10;
      return;

      function a() {}
    }
    b();
    console.log(a); // logs a = 1

    案例2

    function b的主体内排除function a(){}的定义,如下所示。logs value of a = 10

    1
    2
    3
    4
    5
    6
    7
    var a = 1;
    function b() {
      a = 10;  // overwrites the value of global 'var a'
      return;
    }
    b();
    console.log(a); // logs a = 10

    观察将帮助您认识到语句console.log(a)记录了以下值。

    案例1:A=1

    案例2:A=10

    定位

  • var a已在全球范围内进行了词汇定义和声明。
  • a=10此语句正在将值重新赋给10,它在词汇上位于函数b内。
  • 两种情况的解释

    由于function definition with name propertya与variable a相同。function body b内的variable a成为局部变量。前一行表示a的全局值保持不变,a的局部值更新为10。

    所以,我们要说的是下面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = 1;
    function b() {
      a = 10;
      return;

      function a() {}
    }
    b();
    console.log(a); // logs a = 1

    JS解释器解释如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a = 1;
    function b() {
      function a() {}
      a = 10;
      return;


    }
    b();
    console.log(a); // logs a = 1

    但是,当我们删除function a(){} definition时,在函数b之外声明和定义的value of 'a',该值将被覆盖,在情况2中更改为10。该值会被覆盖,因为a=10指的是全局声明,如果要在本地声明,我们必须编写var a = 10;

    1
    2
    3
    4
    5
    6
    7
    var a = 1;
    function b() {
      var a = 10; // here var a is declared and defined locally because it uses a var keyword.
      return;
    }
    b();
    console.log(a); // logs a = 1

    通过将function a(){} definition中的name property改为'a'以外的其他名称,我们可以进一步澄清我们的疑问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = 1;
    function b() {
      a = 10; // here var a is declared and defined locally because it uses a var keyword.
      return;

      function foo() {}
    }
    b();
    console.log(a); // logs a = 1

    SCPope&Closing&Rising(变量/功能)

  • scpope : the global var can be access in any place(the whole file
    scope), local var only can be accessed by the local
    scope(function/block scope)!
    Note: if a local variable not using
    var keywords in a function, it will become a global variable!
  • closure : a function inner the other function, which can access
    local scope(parent function) & global scope, howerver it's vars
    can't be accessed by others! unless, your return it as return value!
  • hoisting : move all declare/undeclare vars/function to the scope
    top, than assign the value or null!
    Note: it just move the declare,not move the value!
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var a = 1;                
    //"a" is global scope
    function b() {  
       var a = function () {};
       //"a" is local scope
       var x = 12;
       //"x" is local scope
       a = 10;
       //global variable"a" was overwrited by the local variable"a"  
       console.log("local a =" + a);
       return console.log("local x =" + x);
    }      
    b();
    // local a =10
    // local x = 12
    console.log("global a =" + a);
    // global a = 1
    console.log("can't access local x =
    "
    );
    // can't access local x =
    console.log(x);
    // ReferenceError: x is not defined


    提升是一个让我们更容易理解的概念。实际发生的是声明首先是针对它们的作用域进行的,然后分配将发生(而不是同时发生)。

    声明发生时,先声明var a,然后声明function b,在b范围内声明function a

    此函数a将隐藏来自全局范围的变量a。

    声明完成后,赋值开始,全局a得到1的值,内部a得到function b的值。当您执行alert(a)操作时,它将调用实际的全局范围变量。对代码的这一小改动将使它更加清晰

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
            var a = 1;

        function b() {
            a = 10;
            return a;

            function a() { }
        }

        alert(b());
        alert(a);


    function a() { }是一个函数语句,它在b函数的局部创建一个a变量。变量是在分析函数时创建的,不管是执行var还是函数语句。

    a = 10设置该局部变量。


    令人惊讶的是,这里的答案都没有提到范围链中执行上下文的相关性。

    javascript引擎将当前正在执行的代码包装在执行上下文中。基本执行上下文是全局执行上下文。每次调用一个新函数时,都会创建一个新的执行上下文并将其放到执行堆栈上。想想其他编程语言中位于调用堆栈上的堆栈帧。后进先出。现在,每个执行上下文在JavaScript中都有自己的变量环境和外部环境。

    我将使用下面的示例作为演示。

    1)首先,我们进入全局执行上下文的创建阶段。创造了词汇环境的外部环境和可变环境。全局对象被设置并放置在内存中,特殊变量"this"指向它。函数A及其代码和具有未定义值的变量myvar被放置在全局变量环境中的内存中。重要的是要注意,函数A的代码没有执行。它只是用函数A放在内存中。

    2)其次是执行上下文的执行阶段。myvar不再是未定义的值。它初始化为值1,该值存储在全局变量环境中。调用函数A并创建新的执行上下文。

    3)在函数A的执行上下文中,它经历了自己的执行上下文的创建和执行阶段。它有自己的外部环境和可变的环境,因此,它有自己的词汇环境。函数b和变量myvar存储在其变量环境中。此变量环境与全局变量环境不同。由于函数A在词汇上(物理上在代码中)与全局执行上下文位于同一级别,因此其外部环境是全局执行上下文。因此,如果函数a引用的变量不在其变量环境中,它将搜索范围链并尝试在全局执行上下文的变量环境中查找该变量。

    4)在函数A中调用函数B。创建新的执行上下文。因为它在词法上位于函数A中,所以它的外部环境是A。所以当它引用myvar时,因为myvar不在函数B的变量环境中,所以它将查找函数A的变量环境。它在那里找到了它,console.log打印了2。但是,如果变量不在函数A的变量环境中,那么由于函数A的外部环境是全局执行上下文,因此作用域链将继续在那里搜索。

    5)函数b和a执行完毕后,从执行堆栈中弹出。单线程JavaScript引擎在全局执行上下文中继续执行。它调用b函数。但是在全局变量环境中没有b函数,在全局执行环境中也没有其他外部环境可以搜索。因此,javascript引擎会引发异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function a(){
      function b(){
        console.log(myVar);
      }

      var myVar = 2;
      b();
    }

    var myVar = 1;
    a();
    b();
    > 2
    > Uncaught ReferenceError: b is not defined

    下面的示例显示了作用域链。在函数B的执行上下文的变量环境中,没有myvar。因此,它搜索它的外部环境,即函数A。函数A在其变量环境中也没有myvar。因此,引擎搜索函数A的外部环境,即全局执行上下文的外部环境,并在此定义myvar。因此,console.log将打印1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function a(){
      function b(){
        console.log(myVar);
      }

      b();
    }

    var myVar = 1;
    a();
    > 1

    对于执行上下文和与之相关联的词汇环境,包括外部环境和变量环境,可以在JavaScript中启用变量范围。即使您多次调用同一个函数,对于每个调用,它也将创建自己的执行上下文。因此,每个执行上下文在其变量环境中都有自己的变量副本。变量不共享。


    提升是JavaScript的行为概念。提升(比如移动)是一个概念,它解释了变量的声明方式和位置。

    在javascript中,变量可以在使用后声明,因为函数声明和变量声明总是被javascript解释器无形地移动("提升")到其包含范围的顶部。

    在大多数情况下,我们会遇到两种类型的提升。

    1.变量申报吊装

    让我们通过这段代码来理解这一点。

    1
    2
    3
    4
    5
     a = 5; // Assign 5 to a
     elem = document.getElementById("demo"); // Find an element
     elem.innerHTML = a;                     // Display a in the element
     var a; // Declare a
      //output-> 5

    在这里,变量A的声明将由javascript解释器在编译时以不可见的方式托管在顶部。所以我们可以得到a的值,但是不推荐使用这种变量声明方法,因为我们应该像这样声明变量到顶部。

    1
    2
    3
    4
     var a = 5; // Assign and declare 5 to a
     elem = document.getElementById("demo"); // Find an element
     elem.innerHTML = a;                     // Display a in the element
      // output -> 5

    考虑另一个例子。

    1
    2
    3
    4
      function foo() {
         console.log(x)
         var x = 1;
     }

    实际解释如下:

    1
    2
    3
    4
    5
      function foo() {
         var x;
         console.log(x)
         x = 1;
      }

    在这种情况下,x将是未定义的

    如果执行了包含变量声明的代码,则无所谓。考虑这个例子。

    1
    2
    3
    4
    5
    6
    7
      function foo() {
         if (false) {
             var a = 1;
         }
         return;
         var b = 1;
      }

    这个函数结果是这样的。

    1
    2
    3
    4
    5
    6
    7
    8
      function foo() {
          var a, b;
          if (false) {
            a = 1;
         }
         return;
         b = 1;
      }

    在变量声明中,只有变量定义提升机,而不是赋值。

  • 功能说明吊装
  • 与变量提升不同,函数体或指定值也将被提升。考虑这个代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     function demo() {
         foo(); // this will give error because it is variable hoisting
         bar(); //"this will run!" as it is function hoisting
         var foo = function () {
             alert("this would not run!!");
         }
         function bar() {
             alert("this will run!!");
         }
     }
     demo();

    现在我们已经了解了变量和函数提升,现在让我们来理解这段代码。

    1
    2
    3
    4
    5
    6
    7
    8
    var a = 1;
    function b() {
      a = 10;
      return;
       function a() {}
    }
    b();
    alert(a);

    这个代码会变成这样。

    1
    2
    3
    4
    5
    6
    7
    8
    var a = 1;                 //defines"a" in global scope
     function b() {  
       var a = function () {}; //defines"a" in local scope
        a = 10;                 //overwrites local variable"a"
        return;      
     }      
     b();      
     alert(a);

    函数a()将在b()内具有本地作用域。a()将在用其定义解释代码时移动到顶部(仅在函数提升的情况下),因此a现在将具有本地作用域,因此在函数b()中具有自己的作用域时不会影响a的全局作用域。


    这完全取决于变量"a"的范围。让我通过将范围创建为图像来解释。

    在这里,javascript将创建3个作用域。

    i)全球范围。ii)函数b()作用域。iii)函数a()作用域。

    enter image description here

    当您调用"alert"方法作用域时,它是明确的,因此它将只从全局作用域中选择变量"a"的值,即1。


    在javascript中提升意味着,变量声明在任何代码执行之前都是通过程序执行的。因此,在代码中的任何地方声明变量都等同于在开头声明变量。


    长篇文章!

    但它会净化空气!

    Java脚本的工作方式是,它涉及两个步骤:

  • 编译(也就是说)-这个步骤注册变量和函数声明及其各自的作用域。它不涉及评价函数表达式:var a = function(){}或变量表达式(如在var x =3;的情况下,将3赋给x,这只是R.H.S部分的评价)。

  • 口译员:这是执行/评估部分。

  • 检查以下代码的输出以获得理解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //b() can be called here!
    //c() cannot be called.
    console.log("a is" + a);
    console.log("b is" + b);
    console.log("c is" + c);
    var a = 1;
    console.log("Now, a is" + a);
    var c = function() {};
    console.log("Now c is" + c);

    function b() {
      //cannot write the below line:
      //console.log(e);
      //since e is not declared.
      e = 10; //Java script interpreter after traversing from this function scope chain to global scope, is unable to find this variable and eventually initialises it with value 10 in global scope.
      console.log("e is" + e) //  works!
      console.log("f is" + f);
      var f = 7;
      console.log("Now f is" + f);
      console.log("d is" + d);
      return;

      function d() {}
    }
    b();
    console.log(a);

    让我们打破它:

  • 在编译阶段,"a"将在全局范围内注册,值为"EDOCX1"(24)。"EDOCX1"(25)也是如此,此时的价值将是"EDOCX1"(24),而不是"EDOCX1"(27)。"b"将在全局范围内注册为函数。在b的范围内,"f将被注册为此时未定义的变量,并注册函数"EDOCX1〔31]。

  • 当解释器运行时,在解释器到达实际的表达式行之前,可以访问声明的变量和function()(而不是表达式)。因此,变量将被打印为"undefined",声明的匿名函数可以在前面调用。但是,在表达式初始化之前尝试访问未声明的变量会导致如下错误:

  • 1
    2
    console.log(e)
    e = 3;

    现在,当您有相同名称的变量和函数声明时会发生什么。

    答案是-函数总是在声明相同的名称变量之前被提升,如果声明相同的名称变量,它将被视为重复并被忽略。记住,秩序无关紧要。函数总是优先。但是在评估阶段,您可以将变量引用更改为任何内容(它存储了上次分配的内容),请查看以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var a = 1;
    console.log("a is" + a);

    function b() {
      console.log("a inside the function b is" + a); //interpreter finds                                'a' as function() in current scope. No need to go outside the scope to find 'a'.
      a = 3; //a changed
      console.log("Now a is" + a);
      return;

      function a() {}
    }
    var a; //treated as duplicate and ignored.
    b();
    console.log("a is still" + a +" in global scope"); //This is global scope a.


    这是我对答案的总结,还有更多的注解和一个可以玩的伴奏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // hoisting_example.js

    // top of scope ie. global var a = 1
    var a = 1;

    // new scope due to js' functional (not block) level scope
    function b() {
        a = 10; // if the function 'a' didn't exist in this scope, global a = 10
      return; // the return illustrates that function 'a' is hoisted to top
      function a(){}; // 'a' will be hoisted to top as var a = function(){};
    }

    // exec 'b' and you would expect to see a = 10 in subsequent alert
    // but the interpreter acutally 'hoisted' the function 'a' within 'b'
    // and in doing so, created a new named variable 'a'
    // which is a function within b's scope
    b();

    // a will alert 1, see comment above
    alert(a);

    https://jsfiddle.net/adjavaherian/fffpxjx7/


    发生这种情况的原因是变量名与函数名的含义"a"相同。因此,由于javascript的提升,它试图解决命名冲突,并返回a=1。

    在阅读"javascript提升"http://www.ufthelp.com/2014/11/javascript-lifting.html上的文章之前,我对这一点也感到困惑。

    希望它有帮助。