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 called
person
Is the empty object of the reference value - Then, give
person
Add the name attribute and assign - Then, print
person.name
As expected, you will get the right result - Next, we create a file named
person1
The empty string, which is the original value - And then we give
person1
Add the name attribute and assign - Finally, print
person1.name
The 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 called
age
, is assigned 20, which is a primitive value - Subsequently, we created a file called
tomAge
, and assign it to age. - Next, we create an empty object named
obj
. - We then created a name called
tomObj
Object, and assign it to obj. - And then we give
obj
addedname
Property, assigned totom
. - Finally, we print
tomObj.name
Is 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 = obj
Belongs 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 called
add
Function of, which takes one argumentnum
- Inside the function, the argument is incremented and then returned.
- Next, we declare a name called
count
And assign a value of 10. - call
add
Function, declarationresult
Variable 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 called
setAge
Function, which takes an object - Inside the function, a new one is added for the parameter object
age
Property, 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 named
tom
The empty object - The Tom object is then passed as a parameter
setAge
Method and call, declareresult1
Variable to receive its return value - Finally, we print
tom
Object and theresult1
Object 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:
- perform
fun1()
Function, a context is created and pushed onto the execution context stack fun1
The function is called inside againfun2
Function, therefore createdfun2
Function, pushed onto the context stackfun2
The function is called inside againfun3
Function, therefore createdfun3
Function, pushed onto the context stackfun3
The function completes and exits the stackfun2
The function completes and exits the stackfun1
The 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:
- function
changeName
The scope chain contains two context objects: its own function context object and a global context object arguments
In its own variable object,name
In a variable object in the global context- We can access it inside the function
arguments
withname
Properties, 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:
swapName
Function context object,changeName
Function context object, global context object - in
swapName
Inside the function, we have access to all variables defined in the three context objects. - in
changeName
Inside 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 called
getResult
The function takes two arguments - Internal use of functions
var
Declare a name namedtotal
And assign a value to the sum of the two arguments. - Inside the function, we also directly initialize a function named
globalResult
And assign the value tototal
The 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 it
var
The keyword is declared and the printed value isundefined
- Subsequently, we announced a statement called
getName
In 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 let
a
- We repeatedly declare two variables with the same name using var
b
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 const
name
,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 reported
TypeError: 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:
- variable
a
In a global context, its lifetime is permanent - variable
name
In the context of functions, whengetName
After 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 called
selfAdd
The function of - A variable is defined inside the function
a
- A reference to an anonymous function is then returned inside the function
- Inside the anonymous function, it can be accessed
selfAdd
Variables in a function context - We’re calling
selfAdd()
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,
selfAdd
The 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 are
Zero, one, two, three, four
It 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 parenthesesfor (let i = 0; i < divs.length; I++) {loop body}
Before each loop executes the body of the loop, the JS engine puts thei
Redeclare 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 function
Foo ()
Declared twice, because the default behavior function of the JS engine is promoted, the function declared by the latter is executed foo = 1
Is a direct initialization behavior that is automatically added to the global context.- Because in the block scope,
foo
It’s a function that’s executingfoo = 1
Will start looking for the scope chain, which is found in the block scopefoo
, so assign it to 1. - In the same way,
foo = 2
It 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 insidef
Function, 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:
- perform
changeName()
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 stackf()
The function completes and exits the stackchangeName()
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:
- perform
changeName()
Function to create an execution context and push it onto the context stack changeName()
Function completes, exits the stack, returnsf()
Function reference- perform
f()
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 name
unknownSum()
The function of - The function is declared internally
arr
Array to hold each parameter passed in - One is implemented inside the function
add
Function to concatenate the array of arguments passed in toarr
An array of - The inside of the function is rewritten
add
Function of thetoString()
Methods,arr
The array is summed and the result is returned - Finally, return inside the function
add
Function, 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 💌