Variable ascension
The so-called variable promotion refers to the “behavior” in which the JavaScript engine promotes the declaration part of variables and functions to the beginning of the code during the execution of JavaScript code. When a variable is promoted, it is given a default value, which is known as undefined.
“Variable promotion” means that declarations of variables and functions are moved to the front of the code at the physical level, as we simulated. But that’s not accurate. The actual location of variable and function declarations in the code does not change and is put into memory by the JavaScript engine at compile time.
Analog variable lifting
showName(); 1
console.log(myName); 2
var myName = 'Geek Time'; 3
function showName(){ 4
console.log('Function showName executed')}Copy the code
The steps are as follows:
Var myName = undefined;function showName(){
console.log('Function showName executed'} // Execute the code part showName(); console.log(myName); myName ='Geek Time'; // function showName is executed // undefinedCopy the code
process
As you can see from the figure above, when you enter a piece of code and compile it, you generate two parts: the Execution context and the executable code
** The execution context is the context in which JavaScript executes a piece of code. ** If a function is called, the execution context of the function will be entered and the functions, such as this, variables, objects, and functions, used during the execution of the function will be determined.
Compilation phase
showName(); 1
console.log(myName); 2
var myName = 'Geek Time'; 3
function showName(){ 4
console.log('Function showName executed')}Copy the code
- Lines 1 and 2, because they are not declarative operations, the JavaScript engine does nothing;
- In line 3, since this line is declared by var, the JavaScript engine will create a property named myName in the environment object and initialize it with undefined;
- In line 4, the JavaScript engine finds a function defined by function, so it stores the function definition in the HEAP, creates a showName attribute in the environment object, and points the value of that attribute to the location of the function in the HEAP.
Execution phase
-
The JavaScript engine starts executing executable code, line by line, in sequence. Let’s break down the execution line by line:
-
When the showName function is executed, the JavaScript engine looks for the function in the variable environment object. Since there is a reference to the function in the variable environment object, the JavaScript engine executes the function and prints “function showName was executed.”
-
The JavaScript engine then prints the “myname” message and continues to look for the object in the variable environment. Since the variable environment has the myName variable and its value is undefined, it prints undefined.
-
Next line 3 assigns “geek time” to the myname variable and changes the value of the myname attribute in the variable environment to “geek time”
showName()
var showName = function() {
console.log(2)
}
function showName() {
console.log(1)
}
//1
Copy the code
The call stack
The JavaScript engine pushes the execution context onto the stack, which is often referred to as the execution context stack, or call stack.
There are three ways to create an execution context
- When JavaScript executes global code, the global code is compiled and the global execution context is created, and there is only one global execution context for the lifetime of the page.
- When a function is called, the code inside the function is compiled and the function execution context is created. Generally, the function execution context is destroyed when the function is finished.
- When the eval function is used, the eval code is also compiled and the execution context is created.
var a = 2;
function add(b, c) {
return b + c;
}
function addAll(b, c) {
var d = 10;
var result = add(b, c);
return a + result + d;
}
addAll(3, 6);
Copy the code
- Create a global execution context and push it onto the stack
- Start executing global code, a = 2
- Call the addAll function. When the function is called, the JavaScript engine compiles the function, creates an execution context for it, and finally pushes the execution context of the function onto the stack, as shown below:
-
Start the addAll function, d = 10
-
Compiling the add function pushes the execution context onto the stack
- When add returns, the execution context of the function is popped off the top of the stack and the value of result is set to the return value of add, which is 9. As shown below:
- Immediately after addAll performs the last addition and returns, addAll’s execution context pops off the top of the stack, leaving only the global context in the call stack. The final picture is as follows:
- At this point, the JavaScript process is complete.
scope
Scope is the area in the program where variables are defined, and the location determines the lifetime of the variables. Commonly understood, scope is the accessible scope of variables and functions, that is, scope controls the visibility and life cycle of variables and functions.
Prior to ES6, ES had only two scopes: global scope and function scope.
-
Objects in a global scope can be accessed anywhere in the code, and their life cycle follows that of the page.
-
A function scope is a variable or function defined inside a function and can only be accessed inside the function. After the function is executed, the variables defined inside the function are destroyed.
Problems with variable promotion
- Variables can easily be overwritten without being noticed
var myname = "Geek Time"
function showName(){
console.log(myname);
if(0){
var myname = Geek Bang
}
console.log(myname);
}
showName()
Copy the code
- Variables that should have been destroyed were not destroyed
function foo() {for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
Copy the code
The variable I is promoted, so it is always overridden by assignment. Print 7
ES6 supports both let and const block-level scope
How does JS support both block-level scope and variable promotion
Problems with variable promotion
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
Copy the code
- Compile and create the execution context
All variables declared by var inside the function are stored in the variable environment at compile time. Variables declared by let are deposited in the Lexical Environment at compile time.
- Execute code, a is assigned 1, lexical environment B is assigned 2, and compile block-level scope puts the context of the scoped block into lexical environment
In fact, inside the lexical environment, a small stack structure is maintained. The bottom of the stack is the outermost variable of the function. After entering a scoped block, the variables inside the scoped block will be pushed to the top of the stack. When the scope execution is complete, the information for that scope is popped from the top of the stack, which is the structure of the lexical environment. Note that when I say variables, I mean variables declared by let or const.
In fact, inside the lexical environment, a small stack structure is maintained. The bottom of the stack is the outermost variable of the function. After entering a scoped block, the variables inside the scoped block will be pushed to the top of the stack. When the scope execution is complete, the information for that scope is popped from the top of the stack, which is the structure of the lexical environment. Note that when I say variables, I mean variables declared by let or const.
- Execute console.log(a) and console.log(b), and the variable lookup is shown
- After the lookup, push the lexical context off the stack
- Same thing with the rest
Note: Direct use of let and const promoted but not assigned in the context of a lexical environment causes a package syntax error, known as a temporary dead zone, and does not access Undefind as in a variable environment
As follows:
let a = 1;
{
console.log(a);
leta = 2; } // Error in syntaxCopy the code
The scope chain
function bar() {
console.log(myName)
}
function foo() {
var myName = Geek Bang
bar()
}
var myName = "Geek Time"
foo()
Copy the code
The call stack for the above code is as follows
The variable environment of each execution context contains an external reference to the external execution context. We call this external reference outer.
When a piece of code uses a variable, the JavaScript engine first looks for the variable in the current execution context. For example, if the myName variable is not found in the current context, The JavaScript engine then continues to look in the execution context that Outer points to. To get an idea, take a look at this:
As you can see from the figure, the outer of both bar and foo points to the global context, which means that if an external variable is used in bar or foo, the JavaScript engine will look in the global execution context. We call this chain of lookup the scope chain.
Now that you know that variables are looked up through the scope chain, one question remains: why is the external reference to the bar function called by foo the global execution context, and not foo’s execution context?
To answer this question, you also need to know what lexical scope is. This is because during JavaScript execution, its scope chain is determined by lexical scope.
Lexical scope
Lexical scope means that the scope is determined by the position of the function declaration in the code, so lexical scope is a static scope that predicts how the code will look up identifiers during execution.
This may not be easy to understand, but here’s a picture:
As you can see from the figure, the lexical scope is determined by the position of the code, where the main function contains the bar function, and the bar function contains the foo function. Because the JavaScript scope chain is determined by the lexical scope, the order of the entire lexical scope chain is: Foo function scope – >bar function scope – >main function scope – > global scope.
Now that we know about lexical scopes and scope chains in JavaScript, let’s go back to the question above: in the initial code, function foo called function bar, so why is the external reference to function bar the global execution context instead of function foo’s?
This is because the parent scopes of foo and bar are both global scopes according to lexical scope, so if foo or bar functions use a variable they don’t define, they will look it up in the global scope. That is, the lexical scope is determined at the code stage, regardless of how the function is called.
Variable lookup in block-level scope
function bar() {
var myName = "Geek World."
let test1 = 100,if (1) {
let myName = "Chrome"
console.log(test)}}function foo() {
var myName = Geek Bang
let test2 = {let test = 3
bar()
}
}
var myName = "Geek Time"
let myAge = 10
let test = 1
foo() //1
Copy the code
As in the call stack above, find the variable in line 4 and print 1 through the search order of 1, 2, 3, 4, 5
closure
function foo() {
var myName = "Geek Time"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName(Geek Bang)
bar.getName()
console.log(bar.getName())
Copy the code
The call stack for the above code is shown
** According to the rules of lexical scope, the inner functions getName and setName always have access to variables in their outer function foo, ** so when the innerBar object is returned to the global variable bar, even though foo has finished executing, But the getName and setName functions can still use the myName and test1 variables in foo. So when foo completes, its entire stack looks like this:
As you can see from the figure above, the execution context of foo is popped off the top of the stack, but the setName and getName methods returned use the myName and test1 variables inside foo, so they remain in memory. This is much like the setName and getName methods, which carry the foo function’s own backpack wherever they are called.
It is a specific backpack because it is inaccessible from anywhere except the setName and getName functions, so we can call the backpack the closure of the foo function.
Ok, now we can finally give closure a formal definition. In JavaScript, according to the rules of lexical scope, an inner function can always access the variables declared in its outer function. When an inner function is returned by calling an outer function, the variables referenced by the inner function remain in memory even after the outer function has finished executing. Let’s call this set of variables a closure. For example, if the external function is foo, then the set of variables is called the closure of the foo function.
So how do these closures work? When executing myName = “geek-bang” in the bar.setName method, the JavaScript engine looks for the myName variable in the order “current execution context – >foo function closure – > global execution context”. You can refer to the following call stack state diagram:
As you can see from the figure, there is no myName variable in the execution context of setName. Foo’s closure contains the variable myName, so calling setName changes the value of myName variable in foo’s closure.
Similarly, when bar.getName is called, the variable myName is called in the foo function closure.
You can also use developer Tools to see how closures work. Open Developer Tools in Chrome, hit a breakpoint anywhere in the bar function, and refresh the page to see something like this:
When bar.getName is called, the Scope item on the right represents the Scope chain: Local – >Closure(foo) – >Global – > Local – >Closure(foo) – >Global – > Local – >Closure(foo) – >Global
In the future, you can also use Scope to see the Scope chain of the actual code, which will be easier to debug the code.
How are closures recycled
Now that you understand what closures are, let’s talk a little bit about when closures are destroyed. Because closures can easily leak memory if used incorrectly, paying attention to how closures are recycled can help you use closures properly.
In general, if the function referencing a closure is a global variable, the closure will remain in place until the page closes; But if the closure is not used in the future, it can cause a memory leak.
If the function that references the closure is a local variable, then the JavaScript engine’s garbage collector will reclaim the memory if the closure is no longer in use the next time the JavaScript engine performs garbage collection.
When using closures, try to follow the same principle: if the closure is used forever, it can exist as a global variable. However, if it is used infrequently and takes up a lot of memory, try to make it a local variable.
this
We mentioned that the execution context includes a variable context, a lexical context, and an external context, but there is also a this that is not mentioned. You can refer to the following figure for details:
There are three main types of execution context: global execution context, function execution context, and eval execution context. Therefore, there are only three corresponding types of this: this in the global execution context, this in the function context, and this in eval.
This in the global execution context
This in the global execution context refers to the window object. This is also the only point where this intersects the scope chain, the bottom of which contains the Window object, and this in the global execution context also refers to the window object.
This in the context of function execution
-
Call a function in the global environment. This inside the function refers to the global variable Window.
-
Call a method within an object whose execution context this refers to the object itself.
-
Call,apply,bind change this to point
-
The constructor’s this points to the newly created instance object
Note: The arrow function has no execution context of its own, so the arrow function’s this is its outer function’s this.
The stack and the heap
Language type
- Static languages: Variable data types need to be confirmed before use
- Dynamic languages: Languages that need to check data types during runtime are called dynamic languages
- Weakly typed languages: languages that support implicit type conversions
- Strongly typed languages: languages that do not support implicit type conversions
So JS is a weakly typed dynamic language
Memory space
During the execution of JavaScript, there are three main types of memory space: code space, stack space, and heap space. Code space is mainly used to store executable code
function foo(){
var a = "Geek Time"
var b = a
var c = {name:"Geek Time"}
var d = c
}
foo()
Copy the code
Call stack diagram
When c = {name:” geek time “} is executed, the object is stored in the heap and the address is assigned to the variable C
Values of primitive types are stored directly in the stack, and values of reference types are stored in the heap
But why, you might wonder, should there be a “heap” and a “stack”? All data stored directly in the “stack” is not ok?
The answer is no. This is because the JavaScript engine needs to use the stack to maintain the state of the context during the execution of the program. If the stack space is too large, all the data will be stored in the stack space, which will affect the efficiency of the context switch, and thus affect the efficiency of the entire program execution. For example, after the foo function is executed, the JavaScript engine needs to leave the current execution context. All it needs to do is move the pointer down to the address of the previous execution context. The stack space of foo function execution context is completely reclaimed.
In general, the stack space is not set too large and is mainly used to store small data of primitive types.
An assignment of a primitive type copies the value of a variable, while an assignment of a reference type copies the reference address
D =c; d=c; d=c; d=c
The V8 compilation process
- The first stage is tokenize, also known as lexical analysis, which breaks down the lines of source code into tokens. Token refers to the smallest single character or string that is syntactically impossible to divide. You can refer to the following figure to better understand what tokens are.
-
The second stage is parse, also known as syntax analysis, which converts the token data generated in the previous step into the AST according to the syntax rules. If the source code is syntactically correct, this step is done smoothly. But if there is a syntax error in the source code, this step terminates and a “syntax error” is thrown.
-
Generate bytecode (bytecode is a type of code between AST and machine code. But regardless of the particular type of machine code, the bytecode needs to be converted to machine code by the interpreter to execute it.) So now that you have the AST and the execution context, the next step, the interpreter Ignition, comes in, and it generates bytecode based on the AST.
-
Once the bytecode is generated, it’s time for execution.