Often, we use variables outside of the function in a natural way, without thinking about how we can use variables outside the function directly but not inside the function. Buddha says that everything has a cause, and this cause is the scope chain. Before we understand how a scope chain works, we should know the scope and variable objects that are closely related to it.

Scope (Scope)

When our code is executing, where do we look for variables for that particular code? How do we find these variables? The answer is scope, which is to say that scope determines how variables are stored in certain places and how to find them later. Lexical Scope, also known as static Scope, is used in JS.

Static scope

For static scopes, the scope of a function is directly related to where the code is written, which means that the scope is determined at function definition time. So how do you determine that? This is lexical nesting: the environment inside the function refers to the environment outside the function, and the environment outside the function can refer to the environment outside it, and so on, all the way to the global environment. Like the onion ring below, the global environment is the outermost layer of the onion, within which each layer (the functional environment) is nested.

Consider the following example:

  var name = 'lily';

  function getName() {
    console.log('name: ', name);
  }

  function getMyName() {
      var name = 'lucy';
      getName();
  }

  getMyName();
Copy the code

What is the output? For static scope, when the function getName is executed, it first looks for variables inside the function. If not, it looks for variables in the upper layer. Obviously, its upper layer environment has been determined when it is defined, so it finds Lily and prints name: lily.

Dynamic scope

The opposite of static scope is dynamic scope, where the scope of a function is determined at the time the function is called. With dynamic scoping (such as bash), when the function getMyName is executed, the value of the variable name has changed to Lucy, so the getName function is called and the output is name: Lucy. As shown below, executing bash./test.sh produces name: Lucy.

#test.sh
name='lily';

function getName() {
  echo name: $name;
}

function getMyName() {
  local name='lucy';
  getName;
}

getMyName;
Copy the code

Earlier, we said that scope determines how to store variables at certain locations, which are variable objects, and how to find those variables requires a scope chain.

Variable Object (VO)

We know that variable objects determine how variables are stored, so we need to understand how variable objects are created. The process is roughly as follows:

  • Create arguments objects, check the arguments for the current environment, and initialize properties and property values.
  • Check the function declaration. For every function found in the current environment, create an attribute in VO with the function name to reference the function. Override this property if the function name exists.
  • Check variables. For each variable found in the current environment, create an attribute in VO with the name of the variable and initialize its value to undefined. If the variable name is present, nothing is done (note that this is in the create phase; the execution phase is assigned) and the check continues.

When the code is executed, the variable Object in the function environment becomes an Active Object (AO). Before it becomes an Active Object, its internal attributes cannot be accessed. For a global environment, the variable object is the Window object itself, with direct access to its internal properties. It is important to note that the concepts of variable objects and active objects are incorporated into the lexical Environments model in ES5 (Environment records and references to external environments: Outer Reference), new concepts (Realms, jobs, etc.) have been introduced for ES8 since ES5. Consider the following example:

function calcArea(r) { var width = 20; var squareArea = function squareArea() { return width * width; }; Function circleArea() {return 3.14 * r * r; }; return circleArea() + squareArea(); } calcArea(10);Copy the code

Create a snapshot of the phase execution environment when calcArea(10) is called as follows:

calcAreaExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 10, length: 1 }, r: 10, width: undefined, squareArea: undefined, circleArea: pointer to function circleArea() }, this: { ... }}Copy the code

As you can see, during the creation phase, only the name of the variable is defined, and no value is assigned to the variable. Once the creation is complete, the variable is assigned to the execution phase. The snapshot of the execution environment is as follows:

calcAreaExecutionContext = {
  scopeChain: { ... },
  variableObject: {
    arguments: {
      0: 10,
      length: 1
    },
    r: 10,
    width: 20,
    squareArea: pointer to function squareArea(),
    circleArea: pointer to functioncircleArea() }, this: { ... }}Copy the code

This makes it easier to understand the variable promotion, as shown in the following example

console.log(hello); // [Function: hello]
function hello() { console.log('how are u') }
var hello = 10;
Copy the code

Function: hello = Function: hello = Function: hello = Function: hello = Function: hello = Function: hello Let’s look at the execution flow of the above code

  • First enter the global environment creation phase, check the function declaration, put the function Hello into the variable object (the global environment is the Window object).
  • Check the variable declaration and find that the variable Hello already exists, skip.
  • Enter the execution phase and execute the codeconsole.log(hello)“, looks for hello in the variable object of the global environment, and finds the function Hello.

The environment snapshot is as follows:

globalExecutionContext = {
  scopeChain: { ... },
  VO: window,
  this: window
}
Copy the code

The scope chain

When code executes in an environment, a scope chain of variable objects is created, which consists of a series of variable objects from the current environment and the upper environment, ensuring orderly access by the current execution environment to variables and functions that qualify for access rights. Find a variable in the execution environment. If the variable is not a local variable (including a local function or parameter), the variable is called a free variable. To find a free variable, you need a scope chain.

Consider the following example:

var firstName = 'Michael';
function getName() {
  var middleName = 'Jeffrey';
  function fullName() {
    var lastName = 'Jordan';
    return firstName + middleName + lastName;
  }
  return fullName();
}

getName();
Copy the code

The above code creates three execution environments, global, getName, and fullName, with variable objects VO(global), VO(getName), and VO(fullName). How does the scope chain of function fullName relate to these variable objects? The steps are as follows:

  • When function fullName is created, save the scope chain to the built-in property [[scope]]

    FullName’s scope chain includes VO(global) and AO(getName)(getName performs VO(getName) to AO(getName)) according to static scope when parsing (getName is called).

      fullName.[[scope]] = [
        AO(getName),
        VO(global)
      ];
    Copy the code
  • When the function fullName is activated (call execution), the creation execution context is pushed to the top of the stack

      ECStack = [
        fullName EC,
        getName EC,
        globalContext
      ]
    Copy the code
  • The fullName function is not executed immediately, and preparation begins by copying the [[scope]] property to create the scope chain

    FullNameEC = {scopeChain: fullName.[[scope]],Copy the code
  • Use arguments to create active objects and add parameters, variable declarations, and function declarations

    fullNameExecutionContext = { scopeChain: [AO(getName), VO(global)], activeObject: { arguments: { length: 0 }, lastName: undefined, }, this: { ... }}Copy the code
  • Push the live object to the front of the fullName scope chain

    fullNameEC = { AO: { arguments: { length: 0 }, lastName: undefined, } scopeChain: [AO(fullName), AO(getName), VO(global)], // Scope chain}Copy the code
  • With all the preparation done, the function is executed to assign a value to the variable of the AO(fullName) object

    fullNameEC = { AO: { arguments: { length: 0 }, lastName: 'Jordan', } scopeChain: [AO(fullName), AO(getName), VO(global)], // Scope chain}Copy the code
  • When fullName completes execution, its context pops out of the execution stack

      ECStack = [
        getName EC,
        globalContext
      ]
    Copy the code

Here, we represent the scope chain as an array, with the first element of the chain being the active object of the current execution environment, and the last element of the array being the variable object of the global execution environment. The current execution environment in execution phase variable access will first from the forefront of the scope chain begins searching for the variable, if not find in the include environment, if contain environment without continued to find upwards, so, until the global environment variables in the object, the return is not established, that is to say, in the global scope and function of internal variables can be accessed.

Lengthen the scope chain

In JS, some statements can temporarily add a variable object to the front of the scope chain, which is removed when the code is finished executing. Specifically, the scope is lengthened when the execution stream enters two statements:

  • The catch block of a try-catch statement

    When a catch block is executed, a variable object containing the declaration of the object thrown is created and added to the front of the scope chain. Below, in the catch block, the error object E is added to the front of its scope chain, which makes the error object accessible within the catch block. After execution, the variable object inside the catch block is destroyed, so the error object E cannot be accessed outside the catch block (ie8 could, but IE9 fixed this).

    Var test = () => {try {throw Error(" Error "); } catch(e) { console.log(e); //Error: Error} console.log(e); //Uncaught ReferenceError: e is not defined } test();Copy the code
  • With (obj) statement

    Adds the OBJ object to the front of the scope chain. The with(persion) statement adds the object persion to the front of the function getName’s scope chain. The var myName = name statement looks for the variable name first in the front of its scope chain, the person object. Name attribute snow. Because the variable object of the with statement is read-only, variables defined in this layer are not stored in this layer, but in its upper scope. This accesses the variable myName in the scope of the function getName.

    var persion = { name: 'snow' };
    var name = 'summer';
    var getName = () => {
      with(persion) {
        var myName = name;
      }
      return myName;
    }
    console.log(getName())
    => snow
    Copy the code

conclusion

  • The global environment has no Arguments objects.
  • We don’t have access to the function’s variable object when we write code, but the interpreter can use it when it processes the data to make it an active object.
  • The search of the scope chain always starts at the front of the scope chain, and then works backwards up to the global environment. There is no reverse search.
  • The relationships between environments are linear and orderly.