understanding javascript execution context, what the hell the variable is?

If you are a C programmer, you must know you can not declare a variable using the same name as the function formal parameter in the same scope:

void foo(int a)
{
    int a=10;
    for(int i=0;i<2;i++)
    {

        qDebug()<<a;
    }

}

int main(int argc, char *argv[])
{

    foo(20);
    return 0;
}

The compiler will complain the following error:

error: declaration of ‘int a’ shadows a parameter

But in javascript, you can:

<script>

function foo(a){
  var a = 10;
  for(var i=0;i<2;i++)
  {
      console.log(a);//10
  }

}
foo(20) 
</script>

You may think, that is because the local variable overrides the formal parameter as in the following C program:

void foo(int a)
{

    for(int i=0;i<2;i++)
    {
        int a=10;
        qDebug()<<a;
    }

}

int main(int argc, char *argv[])
{

    foo(20);
    return 0;
}

However, this kind of override in C only happens for variables in lower level of scope, i.e., the variable in lower level of scope overrides the variable with the same name in the upper level of scope. The variables with the same name in different scopes occupy different pieces of memory. The code references different memory addresses. You can not override variable in the same scope, otherwise the compiler would complain re-declaration error.

In Javascript, however, you can re-declare variable in the same scope.

function foo(a){
  var a = 10;
  var a=40;
  for(var i=0;i<2;i++)
  {
    console.log(a);
    var a=30;
  }
  console.log(a);
}
foo(20) ;
</script>

Note that the {} in for statement does not construct a new scope as in C. All the variable a‘s are belong to the same scope, the scope formed by the function. You can imagine a scope as an object variable, in which each declared variable is a member(property), and the variable name is the property name. So the function’s formal parameter a, and all 3 other a‘s reference the same property:a. The object is called Variable Object,which is created at the entrance of the function(the time the function is to be executed soon but not yet executed). The Variable Object(VO) is constructed in the following order:

  1. create the arguments property
  2. create the formal parameter properties with values set to the ones passed to the function.
  3. for every function declared in this scope, create a property whose name is the function name. If there’s already a property with that name, overwrite it.
  4. for every variable declared in this scope, create a property with the same name and set its value to undefined, if there does not exist a property with the same name, otherwise, skip it.

After the VO is created, the function code is executed and some variables get their value updated. Now we know that the 3 declarations of a in the function do not have any effect actually because a property with the same name a already exists, and that property is actually created from the formal parameter. If you do not believe this, you can run the following code:

function foo(a){
  console.log(a);
  var a = 10;
  var a=40;
  for(var i=0;i<2;i++)
  {
    console.log(a);
    var a=30;
  }
  console.log(a);
}
foo(20) ;
</script>

It will print the value of a before it is updated by other statements. You will see the first output is 20, which is the value of the function parameter.We can also verify the VO creation rule 3 by running the following code:

function foo(a){
  console.log(a);
  var a = 10;
  var a=40;
  for(var i=0;i<2;i++)
  {
    console.log(a);
    var a=30;
  }
  console.log(a);
  function a(){}
}
foo(20) ;
</script>

The first line of output is “function a()”, then a is updated with numbers by the other assignment statements.

From the talk above,we know that multiple declarations of the same variable/function name in the same scope are allowed in javascript, and they actually refer to the same memory address. This may cause confusion and is not encouraged. But it is reasonable to use the same name in different scopes. For example, we often use i as an index variable in all functions. We definitely do not want the same name in different scopes causes confusion for our program. When our program uses a variable, we must be clear which one in which scope is used. This is exactly why scope is introduced. The most used variable lookup rule is: look it up in the same scope, if not found, look it up in the parent scope, repeat this process till the global scope.

What is the definition of parenting relationship for scopes? The definition is different for different programming language.

For C, it is pretty simple because C does not allow function definition in another function. The scopes are determined in compiling time. The function scope is the child of the global scope, the statement block scope formed by {} is the child of the external scope that contains that statement block.

For javascript, the scope of a function is the child of the scope of the function or global scope that creates the function. Notice the difference between “create” and “call”. For functions that are declared with “function fun(…)”, the creating time is when it is going to execute the function that contains this function declaration, or at the beginning of the execution of the script(for top level functions). For functions that is declared as an expression, the creating time is when that expression is evaluated(executed).

 

function fun1()
{
      var fun2=function(){};
      function fun3(){}
}

In the above example, fun1 is created at the beginning of the execution of the script, fun3 is created when the execution enters into fun1, and fun2 is created last, when executing the assignment statement. The parent scope of both fun2 and fun3 is the scope of fun1, whose parent scope is the global scope.

Some programming languages use another kind of parenting relationship for scopes. In Bash, the parent scope of the scope of a function is the scope of the function that calls (NOT creates) that function. This is because there is no such concept of “function creation” in Bash. Functions are not created before they are called.  Until a function is actually called does it come into the sight of the Bash interpreter . Javacript is somewhat more advanced for it can scan the code inside a function to create its children function before its code is actually executed.

Knowing what the scope is and the parenting relationship of scopes, we can now consider how to implement the lookup of variables. It is natural to not look up variables in the sidling scopes and limit the lookup to the current scope and all its predecessors. We can allocate a block of memory for every function in the script to store variables in its scope, and create a tree structure to store the relations of all scopes. But it is a little bit wasting because some  functions may  never be called and their memory blocks for scope are useless. Even all functions are called, their variables may not be necessary to  stay in the memory for all the execution time of the script. Since the execution of javascript code is single-threaded, and we only want to look up variables in the chain of parenting scopes, a stack data structure is very suitable for this situation, which is exactly what javascript uses.  At the beginning of the execution of a function, an execution context is created for this function and pushed to the top of the execution context stack. When it function returns, its execution context is popped up from the stack. The execution context includes a chain of (the references of )VOs(the VO of the function itself is at the beginning of the chain while the VOs of all its predecessors are appended). The variable lookup starts from the beginning VO of the chain till the last VO in the chain. We’ve known how a function creates the VO of itself, but how to get the predecessors’ VOs to create this VO chain? Well, the  predecessors’ VO chain is stored to the function when it is created. It is very easy to get this because the VO chain in current (when creating the function) execution stack top is exactly the predecessors’  VO chain, just copy it and save it to the function definition. When it enters into the function, this chain will be pre-pended by the function’s own VO to form the full VO chain. Note that the VO chain in the execution context is just the references of VOs so when the execution context is pop up, the actual VOs may not be destroyed if there are other references to them.  This explains why a function can use the variables of its parent function even the parent function has already exited. That is because the child function still has references (in its execution context and its definition) to its parent’s VO.

Let’s check how well we master the variable lookup and the VO concept by analyzing the following example:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

In the first iteration of the for loop, we call an anonymous  function(denoted as outeran0) and assign the returned value to an array element data[0]. To call outeran0, the outeran0 expression must be evaluated so outeran0 is created. Immediately after outeran0 is created, it is called with the parameter passed as 0. Although outeran0 has reference to the i in its parent’s VO, it uses the formal parameter i of its own VO. During the execution of outeran0, another anonymous function(denoted as inneran0) expression is evaluated so created(but not called), and it has reference to the VO of  outeran0. Then outeran0 exits but its VO remains in memory which contains the variable i and the value of i is 0. So when inneran0 is called later(data[0]();), it finds i in the VO of its parent(outeran0) and prints its value 0. Similarly, in the second iteration of the for loop, outeran1(different from outeran0) and inneran1(different function from inneran0) are created, outeran1 exits and the reference to inneran1 is stored in data[1]. “data[1]();” will print 1 and “data[2]();” will print 2.

Comments are closed, but trackbacks and pingbacks are open.