One of the most basic models of almost any language is to store values in variables and later retrieve or modify them. The ability to store and retrieve values in variables gives a program state. This raises two questions: where are these variables stored? How does the program find them when it needs them? Answering these questions requires a well-defined set of rules that define how variables are stored and how to find them. We call this set of rules: scope.

LHS and RHS query

Before talking about scopes in javascript, I thought I’d take a look at LHS and RHS queries to help you understand scopes.

Although javascript is thought of as an interpreted/dynamic language, it is actually a compiled language. In general, to run a piece of javascript code, there are two essential things: a JS engine and a compiler. The former is similar to the role of general manager, responsible for the scheduling of various resources required by the whole program. The latter is only part of the former and is responsible for compiling javascript source code into machine-readable instructions that the engine can then run.

compile

In javascript, a piece of source code typically goes through three steps before being executed, also known as compilation:

  1. Word segmentation/lexical analysisThe compiler first breaks a string of characters into meaningful (for the language) fragments called tokens, for examplevar a = 2;. This program is likely to be interrupted by the following token:var.a.=.2, and;.
  2. Parsing/parsing: the compiler will have onetokenIs converted to an “abstract syntax tree” (AST —— Abstract Syntax Tree), which represents the syntax structure of the program.
  3. Code generation: The compiler translates the abstract syntax tree generated in the previous step into machine instructions for the engine to execute.

perform

The compiler runs amok and generates a bunch of machine instructions, which the JS engine happily picks up and executes, and that’s where LHS and RHS come in.

LHS (left-hand Side) and RHS (Right-hand Side) are two ways for JS engine to operate variables in the code execution stage. The difference between them is whether the query purpose of variables is variable assignment or query.

LHS can be understood as a variable to the left of the assignment operator (=), such as a = 1. The purpose of the current engine search on variable A is to assign the variable. In this case, the engine doesn’t care what the original value of variable A is, it just assigns the value 1 to variable A.

RHS can be understood as a variable to the right of the assignment operator (=), such as console.log(a), where the engine’s search for variable A is a query. It needs to find the actual value of variable A before printing it out.

Take a look at this code:

var a = 2; / / LHS queries
Copy the code

When this code runs, the engine does an LHS query, finds A, and assigns it a new value of 2. Now look at the following paragraph:

function foo(a) { / / LHS queries
  console.log( a ); / / RHS queries
}

foo( 2 ); / / RHS queries
Copy the code

To execute it, the JS engine does both an LHS query and an RHS query, except that the LHS here is harder to find.

In short, the engine needs LHS and RHS to get/assign variables. However, these two operations are only means. Where to get variables is the key. The location where LHS and RHS fetch variables is the scope.

What is scope

Simply put, a scope is the area of a program where variables are defined, and it determines how much access the currently executing code has to the variables.

For the most part in javascript, there are only two scope types:

  • Global scope: A global scope is the outermost scope of a program and always exists.
  • Function scope: A function scope is created only when the function is defined, within the parent function scope/global scope.

Due to scope limitations, each independently executing code block can only access variables in its own scope and in the outer scope, but not in the inner scope.

/* Global scope starts */
var a = 1;

function func () { /* the func function scope starts */
  var a = 2;
  console.log(a);
}                  /* func function scope end */

func(); / / = > 2

console.log(a); / / = > 1

/* End of global scope */
Copy the code

The scope chain

In the example above, the executable code block can find variables in its own scope, so if the target variable is not found in its own scope, does the program run properly? Take a look at the following code:

function foo(a) {
  var b = a * 2;

  function bar(c) {
    console.log( a, b, c );
  }

  bar(b * 3);
}

foo(2); / / 2, 4 12
Copy the code

Based on the previous knowledge, we know that inside the bar function, three RHS queries will be made to obtain the values of three variables a, B and C respectively. The internal scope of bar can only fetch the value of variable C; both a and b are fetched from the scope of the external function foo.

When the executable code accesses a variable internally, it looks in the local scope first, returns if it finds the target variable, and continues in the parent scope otherwise… Go all the way to the global scope. We refer to this nesting mechanism of scopes as scope chains.

As shown in the figure, the above code has three levels of scope nesting, respectively:

  1. Global scope
  2. fooscope
  3. barscope

Note that function parameters are also in function scope.

Lexical scope

With the concept of scope and scope chain understood, let’s look at lexical scope.

Lexical Scopes are the type of scope used in javascript, and Lexical Scopes can also be called static Scopes, as opposed to dynamic Scopes. So what’s the difference between lexical and dynamic scoping that javascript uses? Take a look at this code:

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar();

// The result is...?
Copy the code

In the above code, there are three scopes:

  • Global scope
  • fooThe function scope of
  • barThe function scope of

It makes sense all the way over here, but foo calls a variable value that’s not in local scope. In order to retrieve this variable, the engine has to go to foo’s upper scope. What is foo’s upper scope? Is it the bar scope from which it is called? Or is it the global scope in which it is defined?

The key issue is the type of scope in javascript — lexical scope.

Lexical scope means that when a function is defined, its scope is already defined, regardless of where it is executed. Therefore, lexical scope is also called “static scope”.

If the type is dynamically scoped, the code above should run as a result of 2 in the bar scope. You might be wondering what languages are dynamically scoped? Bash is dynamic scope, if you are interested.

Block-level scope

What is block-level scope? Simply put, the curly braces {… } is the block-level scoped region.

Many languages support block-level scopes themselves. As mentioned above, most of the time in javascript, there are only two types of scope: global scope and function scope. Is there a block-level scope in javascript? Take a look at the following code:

if (true) {
  var a = 1;
}

console.log(a); / / the result???????
Copy the code

When you run it, you’ll see that the result is still 1, and that the variable a defined and assigned in curly braces goes global. This is enough to say that javascript does not natively support block scope, or at least did not take block scope into account when the language was created… (Come out and take the blame!!)

But the ES6 standard proposes to “create block-level scopes” by using let and const instead of the var keyword. That is, the block-level scope works if the above code is changed to the following:

if (true) {
  let a = 1;
}

console.log(a); // ReferenceError
Copy the code

For more details on lets and const, enter the portal

Create scope

In javascript, we have several ways to create/change scopes:

  1. Define functions and create functions (recommended) :

    function foo () {
      // creates a function scope for foo
    }
    Copy the code
  2. Creating block-level scopes with lets and const (recommended) :

    for (let i = 0; i < 5; i++) {
      console.log(i);
    }
    
    console.log(i); // ReferenceError
    Copy the code
  3. Try catch creation scope (not recommended),err only exists in the catch clause:

    try {
     undefined(a);// Force an exception
    }
    catch (err) {
     console.log( err ); // TypeError: undefined is not a function
    }
    
    console.log( err ); // ReferenceError: `err` not found
    Copy the code
  4. Use eval to “cheat” lexical scopes (not recommended) :

    function foo(str, a) {
     eval( str );
     console.log( a, b );
    }
    
    var b = 2;
    
    foo( "var b = 3;".1 ); / / 1 3
    Copy the code
  5. Use with to cheat lexical scope (not recommended) :

    function foo(obj) {
     with (obj) {
       a = 2; }}var o1 = {
     a: 3
    };
    
    var o2 = {
     b: 3
    };
    
    foo( o1 );
    console.log( o1.a ); / / 2
    
    foo( o2 );
    console.log( o2.a ); // undefined
    console.log( a ); // 2 -- Global scope is leaked!
    Copy the code

In summary, there are only two ways to create scopes: define function creation and let const creation.

Application scenarios of the scope

One common use of scope is modularity.

The fact that javascript does not natively support modularity leads to a number of unpleasant problems, such as global scope contamination and variable name conflicts, bloated code structures and poor reuse. Prior to the formal modularity approach, developers came up with the idea of using function scopes to create modules in order to solve these problems.

function module1 () {
  var a = 1;
  console.log(a);
}

function module2 () {
  var a = 2;
  console.log(a);
}

module1(); / / = > 1
module2(); / / = > 2
Copy the code

Module1 and module2 are two functions representing modules, and a variable with the same name is defined in the two functions. Due to the isolation nature of function scopes, the two variables are stored in different scopes (not nested). When executing these two functions, JS engine will read from different scopes. And the external scope does not have access to the a variable inside the function. In this way, the problems of global scope pollution and variable name conflict are solved ingeniously. And, because of the wrapping of the function, it looks much more encapsulated.

However, the function declarations above still look redundant and, more importantly, the function names for module1 and Module2 themselves pollute the global scope. Let’s continue rewriting:

// module1.js
(function () {
  var a = 1;
  console.log(a); }) ();// module2.js
(function () {
  var a = 2;
  console.log(a); }) ();Copy the code

The Function declaration is changed into the Immediately Invoked Function Expression (IIFE). The code is more concise and encapsulation is better. The problem of module name polluting the global scope is solved.

The easiest way to distinguish a function declaration from a function expression is if it begins with the function keyword: function is a function declaration; otherwise, function is an expression.

The above code uses the IIFE code, which has evolved a lot. We can strengthen it, strengthen it into a later version, give it the power to judge the external environment — the power to choose.

(function (global) {
  if (global...) {
    // is browser
  } else if (global...) {
    // is nodejs
  }
})(window);
Copy the code

Let the waves continue to surge, and our imagination is not strong enough to imagine UMD modular code:

// UMD modularization
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    // Node, CommonJS-like
    module.exports = factory(require('jquery'));
  } else {
    // Browser globals (root is window)
    root.returnExports = factory(root.jQuery);
  }
}(this.function ($) {
  // methods
  function myFunc(){};

  // exposed public method
  return myFunc;
}));
Copy the code

I looked at the modular application scenario of scopes with real envy. If you are as envious as I am and want to learn more about modularity, enter the portal.

closure

With scopes out of the way, let’s talk about closures.

Functions that can access variables inside other functions are called closures.

A closure is a function defined inside a function that is returned and called externally. We can express this in code:

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // This forms a closure
Copy the code

We can briefly analyze the flow of the above code:

  1. At compile time, variables and functions are declared and scopes are determined.
  2. Run the functionfoo(), one will be createdfooThe execution context of the function is stored internallyfooAll variable function information declared in.
  3. functionfooAfter running, the internal function will bebarAssigns a reference to an external variablebazAt this time,bazThe pointer is pointing to orbarSo even though it’s locatedfooOutside of scope, it can still get itfooInternal variables of.
  4. bazBeing executed externally,bazInternal executable codeconsole.logRequest fetch from the scopeaVariable, local scope not found, continue request parent scope, foundfooIn theaVariable, return toconsole.logTo print out2.

The implementation of closures seems like a little bit of a “cheat” used by developers to bypass the scope’s regulatory mechanisms and get information about the internal scope from outside. This feature of closures greatly enriches the way developers code and provides many useful scenarios.

Application scenarios for closures

Closures are mostly used in situations where internal variables need to be maintained.

The singleton pattern

The singleton pattern is a common reference pattern that guarantees only one instance of a class. The implementation method is generally to check whether the instance exists, if it exists, directly return, otherwise create and return. The benefit of the singleton pattern is to avoid the memory overhead associated with repeated instantiations:

// Singleton mode
function Singleton(){
  this.data = 'singleton';
}

Singleton.getInstance = (function () {
  var instance;
    
  return function(){
    if (instance) {
      return instance;
    } else {
      instance = new Singleton();
      returninstance; }}}) ();var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'
Copy the code

Simulate private properties

Javascript does not have the same access control as Java does in public private. Methods and attributes used in objects can be accessed, which causes security risks. Any developer can modify internal attributes at will. Although private attributes are not supported at the language level, we can use closures to simulate private attributes:

// Simulate private attributes
function getGeneratorFunc () {
  var _name = 'John';
  var _age = 22;
    
  return function () {
    return {
      getName: function () {return_name; },getAge: function() {return _age;}
    };
  };
}

var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); / / 22
obj._age; // undefined
Copy the code

Currie,

Currying is a technique that converts a function that takes multiple arguments into a function that takes a single argument (the first argument of the original function), and returns a new function that takes the remaining arguments and returns a result.

The concept is a bit abstract, but currization is actually a use of higher-order functions. Common javascript bind methods can be implemented using currization:

Function.prototype.myBind = function (context = window) {
    if (typeof this! = ='function') throw new Error('Error');
    let selfFunc = this;
    let args = [...arguments].slice(1);
    
    return function F () {
        // New F() is returned
        if (this instanceof F) {
            return newselfFunc(... args,arguments);
        } else  {
            // Bind can implement code like f.bind(obj, 1)(2), so you need to concatenate the arguments on both sides
            return selfFunc.apply(context, args.concat(arguments)); }}}Copy the code

One of the advantages of currization is the reuse of parameters. It can generate a completely new function based on the parameters passed in.

function typeOf (value) {
    return function (obj) {
        const toString = Object.prototype.toString;
        const map = {
            '[object Boolean]'	 : 'boolean'.'[object Number]' 	 : 'number'.'[object String]' 	 : 'string'.'[object Function]'  : 'function'.'[object Array]'     : 'array'.'[object Date]'      : 'date'.'[object RegExp]'    : 'regExp'.'[object Undefined]' : 'undefined'.'[object Null]'      : 'null'.'[object Object]' 	 : 'object'
        };
        returnmap[toString.call(obj)] === value; }}var isNumber = typeOf('number');
var isFunction = typeOf('function');
var isRegExp = typeOf('regExp');

isNumber(0); // => true
isFunction(function () {}); // true
isRegExp({}); // => false
Copy the code

By passing different type string arguments into typeOf, you can generate the corresponding type judgment function, which can be reused as syntactic sugar in business code.

Closure problems

As we know from the introduction above, closures can be used in a wide variety of scenarios. Can we use closures in large numbers? No, because overuse of closures can cause performance problems.

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // This forms a closure
Copy the code

At first glance, this may not seem like a problem, but it can cause memory leaks.

As we know, javascript’s internal garbage collection mechanism uses reference count collection: when a variable in memory is referenced once, the count is incremented by one. The garbage collection mechanism polls these variables at a fixed time, marking variables with a count of 0 as invalid and cleaning them up to free up memory.

In the above code, foo is theoretically scoped out from the outside, and all variable references are done inside the function. After Foo is run, the internal variables should be destroyed and memory reclaimed. However, closures cause the global scope to always have a baz variable referring to the bar function inside Foo, which means that the number of references to the bar function defined inside Foo is always 1, and the spamming mechanism cannot destroy it. To make matters worse, bar may also use variable information in the parent scope foo, which cannot be destroyed… The JS engine can’t tell when you’re going to call closure functions again, so it just keeps letting the data eat up memory.

This inability to release memory due to overuse of closures is called a memory leak.

Memory leaks

A memory leak is a condition in which a block of memory is not returned to the operating system or memory pool for some reason when it is no longer being used by an application. A memory leak can cause an application to stall or crash.

In addition to closures, memory leaks can be caused by unintentional creation of global variables. The developer intended to use the variable as a local variable, but forgetting to write var causes the variable to be leaked globally:

function foo() {
    b = 2;
    console.log(b);
}

foo(); / / 2

console.log(b); / / 2
Copy the code

If you forget to unregister the event method before removing the DOM element, you will also cause a memory leak:

const wrapDOM = document.getElementById('wrap');
wrapDOM.onclick = function (e) {console.log(e); };// some codes ...

// remove wrapDOM
wrapDOM.parentNode.removeChild(wrapDOM);
Copy the code

Check for memory leaks

We may all have heard of the infamous “memory leak”, but in the face of the vast amount of ancestral code, how to find the location of the memory leak, but it is difficult to start. Here we still use Google developer tools, Chrome browser, F12 open developer tools, I found Teacher Ruan Yifeng ES6 website demo.

Performance

Click this button to start recording, and then switch to the web page for operation. After recording, click the Stop button, and the developer tool will record the data of the current application from the recording moment.

Select JS Heap, and the blue line below represents the change in the JS Heap memory information during this recording process.

If the blue line keeps going up, it’s basically a memory leak. In fact, I think this is biased. The increase in THE JS heap memory usage does not necessarily mean a memory leak. It only means that there is a lot of memory that has not been released.

memory

You can pinpoint Memory usage more precisely with the Memory option of the developer tools.

When the first snapshot is generated, the developer tools window already shows a detailed memory footprint.

Field Description:

  • Constructor– Type of the resource that occupies memory
  • distance– The reference-level distance from the current object to the root
  • Shallow Size– Memory occupied by objects (excluding memory occupied by other objects referenced internally) (in bytes)
  • Retained Size– Total memory occupied by objects (including internal references to other objects) (in bytes)

Expand each item to see more detailed data information.

We cut back to the page again, continue a few more times, and then create a snapshot again.

Pay special attention to this #Delta. If it’s positive, it means more memory is being created and less memory is being freed. Closure items, if positive, indicate a memory leak.

Let’s look for a memory leak in the code:

Memory leak solution

  1. Use strict mode to avoid inadvertent disclosure of global variables:

    "use strict";
    
    function foo () {
    	b = 2;
    }
    
    foo(); // ReferenceError: b is not defined
    Copy the code
  2. Pay attention to the DOM life cycle and remember to unbind related events during the destruction phase:

    const wrapDOM = document.getElementById('wrap');
    wrapDOM.onclick = function (e) {console.log(e); };// some codes ...
    
    // remove wrapDOM
    wrapDOM.onclick = null;
    wrapDOM.parentNode.removeChild(wrapDOM);
    Copy the code

    Alternatively, event delegate can be used to process events uniformly and reduce the extra memory overhead caused by event binding:

    document.body.onclick = function (e) {
        if (isWrapDOM) {
            // ...
        } else {
            // ...}}Copy the code
  3. Avoid overusing closures.

Most memory leaks are also due to code irregularities. Code ten million, the first specification, code is not standard, the development of two lines of tears.

conclusion

  1. javascriptOnly two scope types are natively supported at the language level:Global scopeFunction scope. A global scope exists when a program runs, a function scope exists only when a function is defined, and there is an inclusive relationship between them.
  2. Scopes can be nested, and we call this nesting a scope chain.
  3. When executable code queries variables in a scope, it can only query local and upper-level scopes, not internal function scopes. When searching for a variable, the JS engine will query the local scope first. Layer up until you reach the global scope.
  4. javascriptIs used in“Lexical scope”So the scope of a function is defined at function definition, regardless of where the function is executed.
  5. A function that has access to the internal variables of another function is called a closure. The essence of a closure is to use the scope mechanism to allow the outer scope to access the inner scope.
  6. Closures can be used in a wide range of scenarios. However, overuse of closures can cause the memory space occupied by variables in closures to be unable to be released, resulting in the problem of memory leaks.
  7. We can usechromeDeveloper tools look for code that is causing memory leaks.
  8. Several ways to avoid memory leaks: Avoid using global variables, and carefullyDOMBind events and avoid overusing closures. The most important thing is code specification. 😃

This article has been included in the front-end interview Guide column

Relevant reference

  • JavaScript You Don’t Know
  • “You Don’t Know JS:” My Understanding of Scopes and Closures
  • Closures – JavaScript | MDN
  • JavaScript deep lexical scope and dynamic scope
  • You don’t understand JS — scopes and closures
  • Js variable declaration and assignment from LHS and RHS Angle
  • The way of the front-end interview
  • Record a front-end memory leak detection experience

Previous content recommended

  1. Thoroughly understand throttling and anti-shaking
  2. [Basic] Principles and applications of HTTP and TCP/IP protocols
  3. 【 practical 】 WebPack4 + EJS + Express takes you through a multi-page application project architecture
  4. Event Loop in browser
  5. Interviewer: Tell me about the execution context
  6. Interviewer: Talk about prototype chains and inheritance
  7. Interviewer: Talk about modularity in JS
  8. Interviewer: Talk about let and const