Scope

  1. Scope is the set of variables, functions, or arguments that JavaScript has access to at a given point in execution.

    In JavaScript, there are 3 kinds of scopes:

    Global

    Variables declared outside all functions have a global scope and are accessible anywhere in the program. They can be views as being at the highest level of scope or at the last link of the scope chain.

    Local

    Variables declared within a function have a local scope and are only accessible within that function as well as any nested functions.

    Eval

    Variables declared within the eval() function adopt the current scope in which eval() was invoked. For instance, if eval() was called within the global scope, any variable declared within the function will have a global scope. However, if eval() was called within a function, any variable declared within the function will have the local scope of that function. (Not sure if this is correct)

    The following code demonstrates all 3 scopes:
     var a = 'outer';            // global
     eval("var c = 'outer'");    // global
    
     function foo() 
     {
         var a = 'inner';            // local
         eval("var b = 'inner'");    // local
    
         a;  // 'inner'
         b;  // 'inner'
         c;  // 'outer'
     }
    
     a;      // 'outer'
     b;      // [1] error
     c;      // 'outer'
    
     /*  Footnotes:
         [1] b is not defined
     */
    

    var a is defined both outside as well as within foo. The variable a defined outside of foo is global, and the variable a defined within foo is local to foo.

    Moreover, notice that var c can be referenced both outside as well as within foo. Because c is defined in a call to eval that is made in the global scope, c also inherits a global scope. Similarly, var b is defined in a call to eval made within foo's local scope, making b also local to foo.



  2. Unlike languages such as Java or C++, JavaScript has no block scoping.

     // JavaScript
    
     var a = 'outer';
    
     {
         var a = 'inner';
         a;   // 'inner'
     }
    
     a;       // 'inner'         // changed
    
    Compare this with Java:
     // Java
    
     String a = 'outer';
    
     {
         String a = 'inner';
         a;      // 'inner'
     }
    
     a;      // 'outer'          // unchanged
    

    In JavaScript, variables within a block ({ ... }) have the same scope as variables directly outside of the block.



  3. Forgetting to declare a variable with a var keyword creates a global variable, regardless of where the variable is declared. It is best practice to always include the var keyword.

     function foo() {
         var a = 'local';
         a;  // 'local'
    
         b = 'global';
         b;  // 'global'
     }
    
     a;      // error - a is not defined
     b;      // 'global'
    

    Notice that even though b was defined locally in foo, it is accessible in the global scope. This is bad for several reasons. First, it makes your code much more unclear. It is not difficult to mistaken b as a local variable when you are, in reality, dealing with a global variable. Second, you can easily override existing global variables within the code. Third, creating global variables in and of itself is bad practice, as your code will likely be in the presence of other programs—programs not written by you—and using true blue global variables will likely lead to naming conflicts and collisions with other existing code.



  4. The stack of currently accessible scopes, from the most immediate context to the global context, is called the scope chain.
     function foo4() {
         function foo3() {
             function foo2() {
                 function foo1() {
                     ...
                 }
             }
         }
     }
    
    foo1's scope chain would look something like:
     global
       \
       foo4
         \
         foo3
           \
           foo2
             \
             foo1
    



  5. When a variable is referenced in an execution context, JavaScript performs an identifier resolution within the context's scope chain. First, the JavaScript engine looks at the current scope for the variable, and if it finds it, it returns the variable. However, if it cannot find the variable, JavaScript bubbles up to the outer scope, and looks for it there. It continues to do this until it reaches the end of the chain. If it still cannot find the variable, it returns undefined. This is similar to the process JavaScript goes through when looking for an object's property within the prototype chain.

    Consider the following:
     var a = 'global';
    
     function foo() {
         var a = 'local';
         a;  // [1] 'local'
     }
    
     a;      // 'global'
    

    When [1] is reached, var a is referenced, and the JavaScript engine looks for a within the current scope (local scope foo). Upon finding the line var a = 'local';, it then returns a, which is why we get the value 'local'.

    Now, consider the following:
     var a = 'global';
    
     function foo() {
         var b = 'local';
         a;  // [1] 'global'
     }
    

    This time, at [1], when var a is called, the JavaScript engine looks at the current scope (local scope foo) for a and finds nothing. It then bubbles up to the outer scope and finds var a = 'global'; and returns it, leaving us with the value 'global'.



  6. The scope of a variable is determined based on the location of a function during definition, not during invocation. This means that the scope chain is created before any variable or function is referenced or invoked, allowing us to create closures.

    Closures are functions that refer to independent (or free) variables. In other words, the function defined in the closure "remembers" the environment in which it was created even after the function has returned.

    Closure example #1:
     var foo = (function () {
         var bob = "You have referenced bob.";
         return function () {return bob}
     })();
    
     bob;        // error - bob is not defined
    
     foo();      // "You have referenced bob."
    
    Closure example #2:
     var foo = (function () {
         var foo2 = function () {return "Running foo2()..."};
         return foo2;
     })();
    
     foo2();     // error - foo2 is not defined
    
     foo();      // "Running foo2()..."
    
    Closure example #3:
     var foo = (function () {
         var counter = 0;
         return function () {return "counter is now at " + (counter += 1)}
     })();
    
     counter;    // error - counter is not defined
    
     foo();      // "counter is now at 1"
     foo();      // "counter is now at 2"
     foo();      // "counter is now at 3"
    
    The following code is equivalent to the code written above:
     function foo1 () {
         var counter = 0;
         function foo2 () {return "counter is now at " + (counter += 1)}
         return foo2;
     }
    
     var foo3 = foo1();      // foo1 is invoked, returning foo2 into foo3
     var foo4 = foo1();      // foo1 is invoked, returning foo2 into foo3
    
     foo3();     // "counter is now at 1"
     foo3();     // "counter is now at 2"
    
     foo4();     // "counter is now at 1"
     foo4();     // "counter is now at 2"
     foo4();     // "counter is now at 3"
    
     foo3();     // "counter is now at 3"
    

    Since foo2 is nested within foo1 at the time of definition, it has access to all of foo1's variables no matter where it is in the program. When we call function foo1, it returns function foo2 as a result, and we store it into var foo3 (foo3 is now a function). Because foo2 still has access to foo1's counter variable, foo3 has access to it as well. As a result, each time we invoke foo3, it remembers counter's state, even though counter technically should not be able to be accessed outside of function foo1. This is possible because foo1 is within the scope chain of foo3:

     global
        \
       foo1  ------>  { counter = ... }
          \
         foo2
            \
           foo3
    

    What's actually happening under the hood is that the JavaScript engine creates an execution context in memory for every function instantiation, in this case, for var foo3 and var foo4. Each execution context remembers the state of foo3 and foo4 as well as their scope chains, respectively. I will dive deeper into this topic next.


    Understanding the Execution Context


  7. All JavaScript code is associated and executed within an execution context. JavaScript always starts out in the global execution context. Once a function is called, a new execution context associated with that function is created, and JavaScript enters into that context. If another function is called within the current function, another execution context is created, and JavaScript enters that context for the duration of the function call. After that function returns, JavaScript re-enters the previous execution context. You can see JavaScript's flow of execution as a stack of execution contexts, where only one context can be executed at a given time, and the current execution context is always at the top (similar to a stack data structure).

    This code is copied and pasted from an earlier example:
     function foo1 () {
         var counter = 0;
         function foo2 () {return "counter is now at " + (counter += 1)}
         return foo2;
     }
    
     var foo3 = foo1();
     var foo4 = foo1();
    
     foo3();     // [1]
     foo4();     // [2]
    
    The stack of execution contexts would look something like:
                         [1]                               [2]
                      *========*                        *========*
                      |  foo3  |                        |  foo4  |
     *========*       *========*       *========*       *========*       *========*
     | global |  -->  | global |  -->  | global |  -->  | global |  -->  | global |
     *========*       *========*       *========*       *========*       *========*
    

    When a function returns, its execution context gets popped off of the stack, and JavaScript returns to the stack below it.



  8. Several things take place when an execution context is created:

    JavaScript creates an Activation object associated with the function being called. This object has no defined prototype and cannot be directly referenced by us.

    JavaScript also creates an arguments object and assigns it as a property of the Activation object. This object resembles an array and collects any arguments that might be passed to the function.

    The following function returns its own arguments object:
     var foo = function() { return arguments; };
    
     foo();                  // [] 
     foo(1, 2, 3);           // [1, 2, 3]
     foo(1, 2, 3, "four");   // [1, 2, 3, "four"]
    
    The actual arguments object returned by foo(1, 2, "three") would look something like:
     // arguments object of foo(1, 2, "three")
    
     Object {
         0: 1,
         1: 2,
         2: "three",
         callee: function () {},
         length: 3,
         Symbol(Symbol.iterator): function ArrayValues() {},
         __proto__: Object
     }
    

    NOTE: Although the arguments object acts like an Array object, it is not actually an instance of Array—it is an instance of Object.

     var foo = function() {return typeof arguments};
     foo();      // "object"
    

    The execution context is then assigned a scope. The scope of a function is simply a property of the function, consisting of a list of objects that the function has access to. The Activation object associated with the function is placed at the top of the list. These objects make up the function's scope chain. Every function has this internal scope property (denoted as [[scope]]), and just like the Activation object, it, too, is not accessible to us.

    Here is a diagram of a function along with its [[scope]] property and Activation object:
                               // Scope Chain (list)
                                       |
                                       V
     function foo() {        |===================|
         [[scope]]: -------> | Activation object | ------@
                             |===================|       |
            ...              |    some object    |       |
                             |===================|       |
     }                       |        ...        |       |
                             |===================|       |
                             |    some object    |       |
                             |===================|       |
                                                         |
                                                         |
             @-------------------------------------------@
             |
             V
     // Activation object
    
     Object {
         arguments: Object { ... },      // arguments object
         ...
     }
    

    NOTE: Notice that the arguments object mentioned earlier is a property of the Activation object, and the Activation object is at the top of the [[scope]] property list.

    Next, the JavaScript engine creates new properties within the Activation object that correspond with the function's formal parameters, as well as any inner functions and local variables defined within the function.

     function foo(a, b, c) {         // a, b, c  -> formal parameters
         var d = 0;                  // d        -> local variable
         function foo2() { ... }     // foo2     -> inner function
     }
    
    These values are added as properties to the Activation object:
     // Activation object
    
     Object {
         arguments: Object {...},
         a: [reference to a],
         b: [reference to b],
         c: [reference to c],
         d: [reference to d],
         foo2: [reference to foo2],
         ...
     }
    

    This is why a function can access all data members within its scope chain.

results matching ""

    No results matching ""