preface

A variable in JavaScript is loosely typed; there are no rules that define what data type it must contain, and its value and data type can change during execution.

Such design rules are powerful, but they also raise a number of issues, such as scopes and closures, which we discuss in this article. Interested developers are welcome to read this article.

The principle of analytic

Before we understand scopes and closures, we need to take a closer look at variables.

The original and reference values of a variable

Variables can store two different types of data: original and reference values

  • The value created with the underlying wrapper type is the original value
  • A value created with a reference type is a reference value

Let’s look at what the base wrapper type and reference type have:

  • Basic wrapper types: Number, String, Boolean, Undefined, Null, Symbol, BigInt
  • Reference types: Array, Function, Date, RegExp, etc

When assigning a value to a variable, the JavaScript engine must determine whether the value is a primitive or a reference value:

  • The variable that holds the original value is accessed by value and is stored in stack memory.
  • The variable that holds the reference value is accessed by reference and is stored in heap memory.

A reference value is an object stored in memory. JavaScript does not allow direct access to memory locations, so you cannot directly manipulate the memory space in which the object resides.

When you manipulate an object, you are actually manipulating a reference to that object, so the variable that holds the reference value is accessed by reference.

Operations on properties

Raw and reference values are defined in a similar way, creating a variable and assigning it a value.

However, what can be done with the value after the variable holds it makes a big difference.

  • Reference values can add, modify, and delete their properties and methods
  • The original value cannot have attributes or methods, only the value itself can be modified

Next, let’s take an example to verify:

let person = {};
person.name = "The Amazing Programmer.";
console.log(person.name); // Amazing programmer

let person1 = "";
person1.name = "The Amazing Programmer.";
console.log(person1.name); // undefined
Copy the code

In the above code:

  • We created a file calledpersonIs the empty object of the reference value
  • Then, givepersonAdd the name attribute and assign
  • Then, printperson.nameAs expected, you will get the right result
  • Next, we create a file namedperson1The empty string, which is the original value
  • And then we giveperson1Add the name attribute and assign
  • Finally, printperson1.nameThe value is undefined

The result is as follows:

Note ⚠️ : When we create a variable using the underlying wrapper type, the resulting value is an object, which is a reference value that you can add properties and methods to. Such as:

let person2 = new String("");
person2.name = "The Amazing Programmer.";
console.log(person2.name);
Copy the code

The value of the copy

When we copy the value of a variable to another variable, the JS engine is not the same in handling the original value and the reference value. Next, we will analyze it in detail.

  • When you copy the original value, its value is copied to the location of the new variable.
  • When a reference value is copied, its pointer is copied to the location of the new variable.

Let’s use an example to illustrate:

let age = 20;
let tomAge = age;

let obj = {};
let tomObj = obj;
obj.name = "tom";
console.log(tomObj.name); // tom
Copy the code

In the above code:

  • We created a variable calledage, is assigned 20, which is a primitive value
  • Subsequently, we created a file calledtomAge, and assign it to age.
  • Next, we create an empty object namedobj.
  • We then created a name calledtomObjObject, and assign it to obj.
  • And then we giveobjaddednameProperty, assigned totom.
  • Finally, we printtomObj.nameIs found to betom.

TomAge = age is a copy of the original value. Since the original value is stored in the stack memory, it will open a new area in the stack and copy the value of age to the new area, as shown in the figure below:

Finally, let’s analyze obj and tomObj in the above example:

  • tomObj = objBelongs to reference value replication.
  • The reference value is stored in heap memory, so it is copied over as a pointer.

In the example code above, both obj and tomObj point to the same location in the heap, and tomObj’s pointer points to OBj. As we learned in the deeper understanding of stereotype chains and inheritance article, objects have stereotype chains, so when we add the name attribute to obj, tomObj will also include this attribute.

Next, let’s draw a picture to illustrate the above words:

Passing of parameters

Now that we have laid the groundwork for the previous two chapters, let’s examine how arguments to functions are passed.

In JavaScript all function arguments are passed by value, meaning that values outside the function are copied to arguments inside the function, as we explained in the previous section.

  • When a parameter is passed by value, the value is copied to a local variable that is modified inside the function.
  • When a parameter is passed by reference, the value’s location in memory is stored in a local variable.

Let’s first verify the rule for passing parameters by value with an example like this:

function add(num) {
  num++;
  return num;
}

let count = 10;
const result = add(count);
console.log(result); / / 11
console.log(count); / / 10
Copy the code

In the above code:

  • First, we created a file calledaddFunction of, which takes one argumentnum
  • Inside the function, the argument is incremented and then returned.
  • Next, we declare a name calledcountAnd assign a value of 10.
  • calladdFunction, declarationresultVariable to receive the return value of the function.
  • Finally, print result and count, respectively:11,10

When we call add, we pass in the count argument. When we process it internally, it copies the value of count to the local variable. When we modify it internally, it changes the value of the local variable, so our internal increment of num does not affect the count variable outside the function.

The running results are as follows:

Next, we verify the rule for passing parameters by reference with an example like this:

function setAge(obj) {
  obj.age = 10;
  obj = {};
  obj.name = "The Amazing Programmer.";
  return obj;
}
let tom = {};
const result1 = setAge(tom);
console.log("tom.age", tom.age); / / 10
console.log("tom.name", tom.name); // undefined
console.log("result1.age", result1.age); // undefined
console.log("result1.name", result1.name); // Amazing programmer

Copy the code

In the above code:

  • We created a file calledsetAgeFunction, which takes an object
  • Inside the function, a new one is added for the parameter objectageProperty, and assign it a value of 10
  • We then assign the parameter object to an empty object, add a name attribute and assign.
  • Finally, the parameter object is returned.
  • Next, we create a file namedtomThe empty object
  • The Tom object is then passed as a parametersetAgeMethod and call, declareresult1Variable to receive its return value
  • Finally, we printtomObject and theresult1Object whose execution results comply with the rule of passing parameters by reference

When we call setAge, we copy the reference of the parameter object inside the function to the local variable. In this case, the reference of the parameter object refers to the Tom object outside the function. We add the age attribute to the parameter object, and the Tom object outside the function will also be added with the age attribute.

When we assign obj to an empty object inside the function, the object reference of the local variable refers to the empty object, which is disconnected from the Tom object outside the function, so we add the name attribute, which only adds to the new object.

Finally, the parameter object that we return inside the function, which points to a new address, naturally has only the name attribute.

So, Tom only has the age property, result1 only has the name property.

The running results are as follows:

Execution context and scope

With variables out of the way, let’s look at the execution context.

Execution context is an important concept in JavaScript, which uses stack as data structure. For convenience, this paper simply calls it context, and its rules are as follows:

  • The context of variables or functions determines what data they can access
  • Each context is associated with a variable object
  • All variables and functions defined in this context exist on variable objects and cannot be accessed by code
  • The context is destroyed after all of its code has been executed

Global context

The global context refers to the outermost context, which is determined by the host environment as follows:

  • The global context is destroyed when the web page is closed or the browser exits
  • The global context varies depending on the host environment, and in the browser this is the Window object
  • Global variables and functions defined using var appear on the Window object
  • Global variables and functions declared with lets and const do not appear on window objects

Function context

Each function has its own context, so let’s look at the execution context rules for functions:

  • When a function starts execution, its context is pushed into a context stack.
  • After the function completes execution, the context stack pops the function context.
  • Returns control to the previous execution context
  • The execution flow of the JS program is controlled by this context stack

Let’s use an example to illustrate the context stack:

function fun3() {
    console.log('fun3')}function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1(); 
Copy the code

When JavaScript starts parsing code, the first thing it encounters is global code, so a global execution context is pushed onto the stack at initialization, and the stack is emptied at the end of the application.

When a function is executed, an execution context is created and pushed onto the execution context stack. When the function is finished, the execution context of the function is ejected from the stack.

With these concepts in mind, let’s go back to the code above:

  • performfun1()Function, a context is created and pushed onto the execution context stack
  • fun1The function is called inside againfun2Function, therefore createdfun2Function, pushed onto the context stack
  • fun2The function is called inside againfun3Function, therefore createdfun3Function, pushed onto the context stack
  • fun3The function completes and exits the stack
  • fun2The function completes and exits the stack
  • fun1The function completes and exits the stack

Let’s draw a picture to understand this process:

Scope and scope chain

Now that we know the context, it’s easy to understand the scope.

When context code is executed, the set of variables accessible to the current context is the scope.

Context code, when executed, creates a scope chain of variable objects, which determines the order in which the code of the various contexts accesses variables and functions.

The variable object of the context in which the code is executing is always at the front of the scope chain, and if the context is a function, its active object is used as the variable object.

Active objects initially have only one default variable: arguments (global context does not exist), and the next variable object in the scope chain comes from the include context, and the next object comes from the include context. And so on down to the global context.

The variable object of the global context is always the last variable object in the scope chain.

Identifier resolution at code execution is accomplished by searching identifier names down the scope chain, always starting at the front of the chain and working down the chain until the identifier is found. (No identifier found, an error is reported)

Next, let’s use an example to illustrate the above words:

var name = "The Amazing Programmer.";

function changeName() {
  console.log(arguments);
  name = "White";
}

changeName();
console.log(name); / / to light

Copy the code

In the above code:

  • functionchangeNameThe scope chain contains two context objects: its own function context object and a global context object
  • argumentsIn its own variable object,nameIn a variable object in the global context
  • We can access it inside the functionargumentswithnameProperties, because they can be found through the scope chain.

The result is as follows:

Next, let’s take an example to explain the search process of scope chain:

var name = "The Amazing Programmer.";

function changeName() {
  let insideName = "White";

  function swapName() {
    let tempName = insideName;
    insideName = name;
    name = tempName;

    // Access tempName, insideName, name
  }
  // Access insideName and name
  swapName();
}
// Access name
changeName();
console.log(name);

Copy the code

Above code:

  • The scope chain contains three context objects:swapNameFunction context object,changeNameFunction context object, global context object
  • inswapNameInside the function, we have access to all variables defined in the three context objects.
  • inchangeNameInside a function, we can access its own context object and variables defined in the global context object
  • In the global context, we can only access variables that exist in the global context.

Through the analysis of the above example, we know that the search of scope chain is from inside to outside, inside can access external variables, external can not access internal variables.

Next, let’s draw a diagram to illustrate the scope chain of the above example, as follows:

Note ⚠️ : The function parameter is considered a variable in the current context, so it follows the same access rules as the other variables below.

Variable scope

In JavaScript, variables are declared by keywords such as var, let, and const. The scope of variables declared by different keywords is quite different. Let’s analyze their scope step by step.

Function scope

When a variable is declared using var, it is automatically added to the nearest context. In a function, the closest context is the local context of the function.

If a variable is not declared directly initialized, it is automatically added to the global context.

Let’s take an example to verify the above statement:

function getResult(readingVolume, likes) {
  var total = readingVolume + likes;
  globalResult = total;
  return total;
}

let result = getResult(200.2);
console.log("globalResult = ", globalResult); / / 202
console.log(total); // ReferenceError: total is not defined

Copy the code

In the above code:

  • We declared a name calledgetResultThe function takes two arguments
  • Internal use of functionsvarDeclare a name namedtotalAnd assign a value to the sum of the two arguments.
  • Inside the function, we also directly initialize a function namedglobalResultAnd assign the value tototalThe value of the variable
  • Finally, return the value of total.

We call getResult, passing 200 and 2, and print globalResult and total. We find that globalResult is printed correctly, and Total is not defined.

The result is as follows:

Var declarations are raised to the top of a function or global scope, before all code in the scope. This phenomenon is called variable promotion.

Variable promotion causes code in the same scope to be used before the declaration, as shown in the following example:

console.log(name);// undefined
var name = "The Amazing Programmer.";
function getName() {
  console.log(name); // undefined
  var name = "White";
  return name;
}
getName();

Copy the code

Above code:

  • We print the name variable before we use itvarThe keyword is declared and the printed value isundefined
  • Subsequently, we announced a statement calledgetNameIn the function, the name variable is allowed first, then declared, the allowed value isgetName
  • Finally, the getName method is called.

In both the global context and the function context, we call a variable with the value undefined before the declaration, which proves without error that the var declaration causes the variable to be promoted.

Block-level scope

Variables declared with the let keyword have their own block scope, which is block-level, defined by the nearest pair of curly braces {}. In other words, the scope of variables declared with let in blocks of if, while, for and function is defined inside {}, and even the scope of variables declared with let in individual blocks is also defined inside {}.

Let’s take an example to verify:

let result = true;
if (result) {
  let a;
}
console.log(a); // ReferenceError: a is not defined

while (result) {
  let b;
  result = false;
}
console.log(b); // ReferenceError: b is not defined

function foo() {
  let c;
}
console.log(c); // ReferenceError: c is not defined

{
  let d;
}
console.log(d); // ReferenceError: a is not defined

Copy the code

In the above code, we declare variables in if, while, function, and a separate {}, and call variables outside the block with ReferenceError: Xx is not defined, in addition to function, if we use the var keyword inside the block to declare, then outside the block can normally access the variables inside the block.

The running results are as follows:

When declaring a variable using let, it cannot be declared twice in the same scope, and SyntaxError is thrown if it is repeated.

Let’s take an example to verify:

let a = 10;
let a = 11;
console.log(a); // SyntaxError: Identifier 'a' has already been declared

var b = 10;
var b = 11;
console.log(b); / / 11
Copy the code

In the above code:

  • We repeatedly declared two variables with the same name using leta
  • We repeatedly declare two variables with the same name using varb

SyntaxError: Identifier ‘a’ has already been declared

When we print b, the repeated var declaration is ignored and the result is the same, so the value is 11

Note ⚠️ : Strictly speaking, let declared variables are also promoted at run time, but because of “temporary dead zones” you can’t actually use let variables before declaration. Therefore, from a JavaScript code perspective, let’s boost is not the same as VAR.

Constant statement

A variable declared with the const keyword must have an initial value. Once declared, it cannot have a new value at any time in its life.

Let’s take an example to verify:

const name = "The Amazing Programmer.";
const obj = {};
obj.name = "The Amazing Programmer.";
name = "White";
obj = { name: "White" };
Copy the code

In the above code:

  • We declare two variables using constname,obj
  • To add the name attribute to obj, we did not reassign to obj, so it can be added as usual
  • Next, we assign a new value to name, and an error is reportedTypeError: Assignment to constant variable.
  • Finally, we assign a new value to obj, which also returns an error.

The running results are as follows:

The const obj declared in the above example can modify its properties. To make the entire Object immutable, you can use Object.freeze(), as shown below:

const obj1 = Object.freeze({ name: "White" });
obj1.name = "The Amazing Programmer.";
obj1.age = 20;
console.log(obj1.name);
console.log(obj1.age);

Copy the code

The running results are as follows:

Note ⚠️ : Since the const declaration implies that the value of a variable is of a single type and not modifiable, the JavaScript runtime compiler can replace all instances of it with the actual value without variable look-ups through lookup tables (this optimization is performed by the V8 engine).

The lifetime of a variable

Next, let’s look at the life cycle of a variable.

  • If a variable is in a global context, its lifetime is permanent if we do not actively destroy it.
  • A variable in the context of a function is destroyed at the end of the function call.

Let’s take an example:

var a = 10;
function getName() {
  var name = "The Amazing Programmer.";
}
Copy the code

In the above code:

  • variableaIn a global context, its lifetime is permanent
  • variablenameIn the context of functions, whengetNameAfter execution, the name variable is destroyed.

Understand the closure

From the analysis of the above section, we know that variables in the context of a function are destroyed when the function completes execution. If we have a way to prevent variables in a function from being destroyed when the function completes execution, this method is called a closure.

Let’s use an example to illustrate:

var selfAdd = function() {
  var a = 1;
  return function() {
    a++;
    console.log(a);
  };
};

const addFn = selfAdd();
addFn(); / / print 2
addFn(); / / print 3
addFn(); / / print 4
addFn(); / / print 5

Copy the code

In the above code:

  • We declared a name calledselfAddThe function of
  • A variable is defined inside the functiona
  • A reference to an anonymous function is then returned inside the function
  • Inside the anonymous function, it can be accessedselfAddVariables in a function context
  • We’re callingselfAdd()Function, which returns a reference to an anonymous function
  • Because an anonymous function continues to be referenced in the global context, it has a reason not to be destroyed.
  • So you have a closure here,selfAddThe life of a variable in the context of a function is continued

Here’s an example of what closures can do:

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Learning the closure</title>
  <script type="text/javascript" src="js/index.js"></script>
</head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</body>
</html>
Copy the code
window.onload = function() {
  const divs = document.getElementsByTagName("div");
  for (var i = 0; i < divs.length; i++) {
    divs[i].onclick = function() { alert(i); }; }};Copy the code

The code above, we obtain all div tags of the page, click event loop for each label binding, due to the click event is asynchronous trigger, when the event is triggered, the for loop is already over, as the value of the variable I is 6, so in the click event functions of div down the scope chain from inside to outside to find the variable I, find the value of the always 6.

This is not what we expected. Here we can use closures to close the I value for each loop, as follows:

window.onload = function() {
  const divs = document.getElementsByTagName("div");
  for (var i = 0; i < divs.length; i++) {
    (function(i) {
      divs[i].onclick = function() { alert(i); }; })(i); }};Copy the code

In the above code:

  • Inside the for loop, we use a self-executing function that encloses the I value for each loop
  • When I is searched down the scope chain in an event function, I is first found enclosed in the closure environment
  • There are five divs in the code, so the I’s here areZero, one, two, three, fourIt is in line with our expectations

Use block-level scopes wisely

In the for loop expression above, variable I is defined using var. As we explained in the function scope section, when a variable is declared using var, the variable is automatically added to the nearest context. Here variable I is promoted to the context of window.onload, so every time we execute the for loop, The value of I will be overwritten, and when the synchronous code executes, the asynchronous code executes, it will get the overwritten value.

In addition to using closures to solve the above problem, we can also solve the problem with let, as shown in the following code:

window.onload = function() {
  const divs = document.getElementsByTagName("div");
  for (let i = 0; i < divs.length; i++) {
    // let's hide the scope
    // {let i = 0}
        // {let i = 1}
    // {let i = 2}
    // {let i = 3}
    // {let i = 4}
    divs[i].onclick = function() { alert(i); }; }};Copy the code

In the for loop expression above, we declare variable I using let. As we explained in the block-level scope section, variables declared using let have their own scope block, so using let in the for loop expression is equivalent to using let in the code block, so:

  • for (let i = 0; i < divs.length; i++) This code has a hidden scope between the parentheses
  • for (let i = 0; i < divs.length; I++) {loop body}Before each loop executes the body of the loop, the JS engine puts theiRedeclare and initialize once in the context of the body of the loop

Because let has its own scope in the code block, each value of let in an expression in a for loop is stored in a separate scope and cannot be overridden.

Surface application

Now, let’s use a few examples to reinforce what we’ve already said.

Scope lifting

In the code shown below, we declare a function foo() within a block, initialize a variable foo, and assign it to 1. Declare the foo() function again, and change the value of the variable foo again.

{
  function foo() {
    console.log(1111);
  }
  foo(); / / 2222
  foo = 1;
  // Error: foo is already 1, not a function
  // console.log(foo());
  function foo() {
    console.log(2222);
  }
  foo = 2;
  console.log(foo); / / 2
}
console.log(foo); / / 1
Copy the code

In the above code:

  • Inside the block, the delta functionFoo ()Declared twice, because the default behavior function of the JS engine is promoted, the function declared by the latter is executed
  • foo = 1Is a direct initialization behavior that is automatically added to the global context.
  • Because in the block scope,fooIt’s a function that’s executingfoo = 1Will start looking for the scope chain, which is found in the block scopefoo, so assign it to 1.
  • In the same way,foo = 2It also starts looking for scope chains, and it finds them in the block scopefoo, so assign it a value of 2.

To sum up, when assigning a value to foo inside the block, it finds the variable object in the block scope first and does not change foo in the global context, so console.log(foo) outside the block is still the value of the variable promoted during the first initialization inside the block.

Execution context stack

Let’s take an example to reinforce our knowledge of executing a context stack. The code looks like this:

var name = "The Amazing Programmer.";
function changeName() {
  var name = "White";
  function f() {
    return name;
  }
  return f();
}
const result = changeName();
console.log(result);/ / to light

Copy the code
var name = "The Amazing Programmer.";
function changeName() {
  var name = "White";
  function f() {
    return name;
  }
  return f;
}
const result = changeName()();
console.log(result); / / to light

Copy the code

In both cases, the final result is the same, except that:

  • The first piece of code,changeName()The function is called internallyf()Function and returns the result of its execution
  • The second piece of code,changeName()The function returns directly from the insidefFunction, which forms a closure structure.

They are stored in a very different order in the execution context stack, so let’s start with the first code:

  • performchangeName()Function to create an execution context and push it onto the context stack
  • changeName()The function is called internallyf()Function to create an execution context and push it onto the context stack
  • f()The function completes and exits the stack
  • changeName()The function completes and exits the stack

Let’s draw a diagram to illustrate the process, as follows:

Finally, let’s examine the second code:

  • performchangeName()Function to create an execution context and push it onto the context stack
  • changeName()Function completes, exits the stack, returnsf()Function reference
  • performf()Function to create an execution context and push it onto the context stack
  • f()The function completes and exits the stack

Let’s draw a diagram to illustrate the process, as follows:

The function is currified

Function currification is an idea that caches the results of a function, and is an application of closures.

Let’s use an example of the sum of unknown parameters to illustrate currization, as follows:

function unknownSum() {
  // Store the arguments for each function call
  let arr = [];
  const add = (. params) = > {
    // Concatenate new parameters
    arr = arr.concat(params);
    return add;
  };

  // Sum the parameters
  add.toString = function() {
    let result = 0;
    // Sum the elements in arR
    for (let i = 0; i < arr.length; i++) {
      result += arr[i];
    }
    return result + "";
  };

  return add;
}
const result1 = unknownSum()(1.6.7.8) (2) (3) (4);
console.log("result1 =", result1.toString());
Copy the code

Unknown parameter summation: a function can be called an infinite number of times, each time with variable parameters.

In the above code:

  • We declared the nameunknownSum()The function of
  • The function is declared internallyarrArray to hold each parameter passed in
  • One is implemented inside the functionaddFunction to concatenate the array of arguments passed in toarrAn array of
  • The inside of the function is rewrittenaddFunction of thetoString()Methods,arrThe array is summed and the result is returned
  • Finally, return inside the functionaddFunction, forming a closure structure

When we called unknownSum, the first call to () returned a reference to Add, and subsequent calls to () called add. After passing parameters to Add, the arR variables inside the function were not destroyed due to closure, so Add cached parameters in the ARR variable.

Finally, the toString method of add is called to sum the cached parameters in the ARR.

The result is as follows:

The code address

This article is “JS principle learning” series of the third article, this series of complete route please move: JS principle learning (1) “learning route planning

For all the sample code in this series, go to jS-learning

Write in the last

At this point, the article is shared.

I’m an amazing programmer, a front-end developer.

If you are interested in me, please visit my personal website for further information.

  • If there are any errors in this article, please correct them in the comments section. If this article helped you, please like it and follow 😊
  • This article was first published in nuggets. Reprint is prohibited without permission 💌