scope


The concept of scope runs through the entire javascript body of knowledge, such as determining whether a variable can be accessed, the current value of this, and so on. To understand the relationship between performance and scope, you must first understand how scope works.

The scope chain


Each Function is an object (an instance of a Function object), so they all have properties that can be accessed, and a set of internal properties that can’t be accessed by code. One of the internal attributes is [[scope]]

Thanks to the power of Chrome, we can see the [[scope]] of the current function in the console.

Let’s give an example of the code and then use the console to watch its scope change


let color = 'blue'

debugger

function changeColor() {
    
    let anotherColor = 'red'

    debugger

    function swapColor() {
        
        let tempColor = anotherColor

        anotherColor = color

        color = tempColor

        debugger

    }

    debugger

    swapColor()

    debugger


}

changeColor()

Copy the code
  • Step1. Initialize the scope

Global is the outermost Global variable, the window, and the changeColor function defined in the code will also be inside.

Script is created when the JS is executed, and the color variable is stored here.

We can expand [[scope]] in Global to see. The discovery is exactly the same as the outside. This is because our changeColor function is defined on the outermost window.

  • Step2. Perform changeColor ()

Debugger after let anotherColor = ‘red’

At this point, changeColor has been executed

First let’s look at the Call Stack function

Call Stack Function Call Stack

CallStack is a stack structure, which is characterized by LIFO (last in first out), and the loading and unloading of the stack only occur at one end (the top).

The CallStack handles function calls and returns. Each time a function is called, Javascript runs to generate a new call structure that pushes into the CallStack. When the function call returns, the JavaScript runtime pops up the call structure at the top of the stack. Due to the LIFO nature of the stack, each pop-up must be the structure of the last function called.

You can see that changeColor has been put on the stack, which means that when a function is executed, it’s put on the stack, and when it’s finished, it’s popped

Then let’s look at scope

The script and Globle in the red box below are shown in [[scope]] at the time of definition. This part is defined when the function is created and will be pushed on the stack first when the function is executed.

Execution context

Executing this function creates an internal object called the execution context, which defines the environment in which a function is executed. It is important to note that calling the same function multiple times will result in the creation of different execution stacks, and therefore their execution contexts. When the function completes execution, the stack is pushed out and the execution context is destroyed.

In the figure above, the Local part is the live object created at runtime and pushed to the top of the scope stack when the function executes.

Because,swapColor() is created when changeColor() is executed (which we can see in the figure above). From this we can infer that swapColor() ‘s [[scope]] scope chain is the scope(Tips) of changeColor()

An interesting observation is that functions defined by function and const let var behave differently in scope. If a function defined directly by function is not executed (js scans to see if the function is called), it is not stored in scope. Const,let,var declared functions are stored in the scope whether or not they are called

You can see that the correspondence is very clear. Same scope as when changeColor() is executed

  • Step3. Perform swapColor

SwapColor () creates an active object and pushes it to the top of the scope chain, so its scope chain should be local(active object) + [[scope]] when it is created.

conclusion

A scope determines the value of a variable in the current environment, and a concatenation of multiple scopes makes up the scope chain.

The scope chain is local at call time + [[scope]] at definition time. The value of the variable is determined in the scope chain completion.

Function execution flow:

1. The js engine scans to determine the scope’s initialization content

2. Initialize, if a function has a [[scope]] property that contains the scope chain in which the function was created

3. Execute this function and create a process to push the Call Stack

4. Copy [[scope]] and push it to the top of the current scope

5. Call Stack displays the process

6. Recycling

Data storage access optimization


To clarify, due to the nature of JS, the value of a variable is not confirmed until the function is executed (such as the infamous this reference problem). So when the live object is created and pushed to the top of the scope, the JS engine looks it up along the scope chain, where performance optimization related to data storage is involved.

It is well known that all computer activities are costly. In most cases, the performance difference between accessing data from a literal and a local variable is trivial, while the overhead of accessing array elements and object members is much greater. How much bigger depends on the browser.

In modern high-performance browsers, arrays and objects are optimized for data access. The process by which a variable is retrieved has a technical name — Identifier Resolution Performance.

Identifier resolution

As mentioned earlier, any computer operation incurs a performance overhead. The deeper an identifier is in the scope chain of the execution environment, the slower it reads and writes. Therefore, it is always the fastest for functions to read and write local variables. Reading and writing global variables is generally the slowest (though some javaScript engines optimize it). The main reason is that the global variable is at the end of the scope chain, and therefore farthest.

However, high-performance browsers, such as chrome and safari4, have been optimized for accessing cross-scope identifiers, greatly reducing this performance penalty. In other browsers, however, the performance loss can be shockingly steep.

To sum up, in browsers without optimized javaScript engines, it is recommended to use local variables whenever possible. A good rule of thumb is to store a cross-scoped value in a local variable if it is referenced more than once in a function.

ChromeV8 engine how to optimize property access can see here

JavaScript engine basics: Shapes and Inline Caches

By the way, I recommend you to have a look at Teacher Li Bing’s illustrated Google V8.

Identifier resolution process:

  • Search for its own live object first, return if it exists, and continue to search for function A’s live object if it does not exist.

Search in turn until you find it.

  • If a function has a prototype object, it looks for its own prototype after looking for its own live object

Object, and continue to find. This is the variable lookup mechanism in Javascript.

  • Returns undefined if the entire scope chain cannot be found.

closure


The basics of closures

Closures, the most powerful feature of javaScript. It allows functions to access data that is locally external.

With an example, you can intuitively understand the power of closures.

function func(a, b, length) {
    let sum = length
    debugger
    return function func1() {
        let sum2 = a
        debugger
        return function func2() {
            let sum3 = b
            debugger
            return sum = a + b + length + sum2 + sum3
        }
    }
}


var step1 = func(1.2.5)
var step2 = step1()
var setp3 = step2()
Copy the code

Closures are just as powerful. When run inside func2, it still has access to the outer and even outermost variables. But there is a problem. When we look at the Call stack, it does follow the rule that func and Func1 are pushed off the stack after they are executed, so how does fun2 get the variables that were outside of it?

As mentioned earlier, an identifier resolution looks for the identifier up the scope chain, so the value of the identifier is determined at the moment of execution. Following this principle, it is easy to see why closures can get external variables — ** Even though external functions have been executed and removed from the function stack (call [[scope]]), but the function still retains references to external variables in its scope chain ([[scope]]). This is determined when the function is defined, regardless of whether the function is executed or not.

Func2 is defined when func1 runs to return, and we can see in its [[scope]] property that it holds the variables declared in func and func1, which will be pushed into the scope when the function executes, as shown in the figure:

This is the core principle of closures.

Memory leaks and garbage collection

Because of the closure, the existence of the scope chain keeps the variable referenced all the time, which makes the JS engine think the variable is always useful. So it can’t be freed from memory. The mechanism that controls memory release is called garbage collection.

The garbage collection

Learn about js garbage collection points here

A memory leak

Learn about memory leaks here

Actual interview questions

  • The original problem
var result = [];
var a = 3;
var total = 0;
function foo(a) {
    for (var i = 0; i < 3; i++) {
        result[i] = function () {
            total += i * a;
            console.log(total);
        }
    }
}
foo(1);
result[0] (); result[1] (); result[2] ();Copy the code

What is the final output?

The answer is:


result[0] ();/ / 3
result[1] ();/ / 6
result[2] ();/ / 9

Copy the code

If you are right, then you have some knowledge of scope, scope chain, and variable promotion. Let’s break down the process step by step.

First, we need to disentangle the for loop. Due to the nature of the var keyword, variable I will be promoted

function foo(a) {
	var i = 0;
    // let i = 0
    for (; i < 3; i++) {
        result[i] = function () {
            total += i * a;
            console.log(total); }}}Copy the code

So the I variable is in foo’s live object and pushed to the top of scope, rather than in the block-level scope of the for loop, as shown below:

So whenever you do a ++ operation on I in the for loop, you’re changing the I variable of the function foo’s active object. By the time the result array is initialized, the value of I is already 3.

Meanwhile, the function in the result array does not have an I variable itself, so it can only go up the scope chain until it finds the scope of function foo.

So the answer to 3,6,9 is obvious. The variable a is passed in as an argument to foo(1) and is in the scope of foo, so the value is 1, I is 3, and total increases to 3,6, and 9

  • Change a
var result = [];
var a = 3;
var total = 0;
function foo(a) {
	// var 改为 let
    for (let i = 0; i < 3; i++) {
        result[i] = function () {
            total += i * a;
            console.log(total);
        }
    }
}
foo(1);
result[0] (); result[1] (); result[2] ();Copy the code

The answer is:


result[0] ();/ / 0
result[1] ();/ / 1
result[2] ();/ / 3

Copy the code

If correct, you are familiar with the concepts of variable promotion and block-level scope.

Variables defined by the let keyword do not have variable promotion, which is valid only in the block-level scope in which it is defined. Each for loop has its own separate block-level scope, and variables defined in parallel block-level scopes do not affect each other.

Let’s look at what the scope looks like in practice

What the final result array looks like

You can see that [[scope]] holds the real-time block-level scope of variable I.

And then finally 0,1,3 makes a lot of sense.