While scoping knowledge is fundamental to JavaScript, a thorough understanding must begin with the fundamentals. From an interview point of view, lexical/dynamic scope, scope (chain), variable/function promotion, closure, garbage collection are a class of topics, get through these concepts and master, the interview basically need not worry about this part. This article is a study and summary of chapter 4 of JavaScript Advanced Programming (3rd edition), as well as part 1 of JavaScript You Don’t Know (Volume 1).
Compilation principle
For most programming languages, compilation takes roughly three steps.
-
Word Segmentation/Lexical Analysis (Tokenizing/Lexing)
Const firstName = ‘Yancey’ is decomposed into const, firstName, =, ‘Yancey’, whether Spaces are treated as lexical units, Depends on what the whitespace means to the language. Parser is recommended for parsing JavaScript source code. For this example, the participle structure is as follows.
[{type: 'Keyword'.value: 'const'}, {type: 'Identifier'.value: 'firstName'}, {type: 'Punctuator'.value: '='}, {type: 'String'.value: "'Yancey'",},];Copy the code
-
Parsing/Parsing
This process transforms the lexical unit stream into an Abstract Syntax Tree (AST). Uncaught SyntaxError: Unexpected Token new Uncaught SyntaxError: Unexpected Token new Uncaught SyntaxError: Unexpected Token new
For the above example, the generated AST is shown below, where Identifier stands for variable name and Literal stands for variable value.
-
Code generation
This stage is to convert the AST into executable code, like the V8 engine that compiles JavaScript strings into binary code (creating variables, allocating memory, storing a value in a variable…).
In addition to the above three phases, the JavaScript engine also performs some optimizations for parsing, code generation, and compilation, which is estimated to be based on the V8 source code. There is a library called Acorn that parses JavaScript code. Webpack, ESLint, etc.
Lexical scope and dynamic scope
There are two types of Scope models, one is Lexical Scope and the other is Dynamic Scope.
The lexical scope is defined at the lexical stage, in other words, where you write the variable and block scopes when you write code. JavaScript can change lexical scope with eval and with, but both of these will prevent the engine from optimizing scoped look-ups at compile time, so don’t use them.
Dynamic scopes are defined at run time, most typically this.
scope
The engine, the compiler, and the scope are all involved, both at compile time and runtime.
-
Engines are used to compile and execute JavaScript programs.
-
The compiler is responsible for parsing, code generation and so on.
-
A scope is used to collect and maintain all variable access rules.
Const firstName = ‘Yancey’ as an example, first the compiler encounters const firstName and asks the scope if it already has a variable of the same name in the current scope collection. If it does, the compiler ignores this declaration. Otherwise it will declare a new variable in the collection of the current scope and name it firstName.
The compiler then generates run-time code for the engine to handle the assignment firstName = ‘Yancey’. The engine will first ask the scope if there is a variable called firstName in the current scope collection. If so, the engine uses this variable, otherwise keep looking.
Engines find elements in scope in two ways: LHS and RHS. Generally speaking, LHS is an assignment phase lookup, while RHS is purely a variable lookup.
Look at the following example.
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
Copy the code
-
var c = foo(2); The engine looks for foo in the scope, which is an RHS lookup, and assigns it to c, which is an LHS lookup.
-
Function foo(a) {here we assign argument 2 to parameter a, so this is an LHS lookup.
-
var b = a; We’re going to find variable A first, so this is an RHS lookup. We then assign the variable A to b, which is an LHS lookup.
-
return a + b; A and B, so it’s two RHS lookups.
Global scope
Take the browser environment as an example:
-
Outermost functions and variables defined outside the outermost function have global scope
-
All variables that do not define a direct assignment are automatically declared to have a global scope
-
All properties of window objects have global scope
const a = 1; // Global variables
// Global function
function foo() {
b = 2; // An undefined initial value is considered a global variable
const name = 'yancey'; // Local variables
// local function
function bar() {
console.log(name); }}window.navigator; // The properties of the window object have global scope
Copy the code
The downside of global scopes is that they pollute the global namespace, so many library sources use (function(){…. In addition, the widespread use of modularity (ES6, CommonJS, etc.) also provides a better solution for preventing contamination of global namespaces.
Function scope
A function scope means that all variables belonging to the function can be used and reused throughout the function scope.
function foo() {
const name = 'Yancey';
function sayName() {
console.log(`Hello, ${name}`);
}
sayName();
}
foo(); // 'Hello, Yancey'
console.log(name); // The external cannot access the internal variable
sayName(); // The external cannot access the internal function
Copy the code
Note that the if, switch, while, and for conditional or loop statements do not create new scopes, although they also have a pair of {} wraps. Internal variables can be accessed depending on how they are declared (var or let/const)
if (true) {
var name = 'yancey';
const age = 18;
}
console.log(name); // 'yancey'
console.log(age); / / an error
Copy the code
Block-level scope
We know that the advent of lets and const changes the fact that JavaScript has no block-level scope (see elevation 3, page 76, before the concept of block-level scope). Let and const let and const let But the concept of temporary dead zones will be introduced later.
In addition, the catch clause of the try/catch clause also creates a block-level scope, as shown in the following example:
try {
noThisFunction(); // Create an exception
} catch (e) {
console.log(e); // The exception can be caught
}
console.log(e); // Error: external cannot get e
Copy the code
ascension
In the “wild West days” before ES6, variable promotion was often asked in interviews, and the advent of let and const solved the problem of variable promotion. But functional promotion has always existed, so let’s analyze promotion from the perspective of principle.
Variable ascension
As a reminder about compilers, the engine first compiles JavaScript code before it interprets it, and part of the compilation phase is finding all the declarations and concatenating them with the appropriate scope. In other words, all declarations, including variables and functions, are processed before the code executes.
So, for code var I = 2; JavaScript actually treats this code as var I; And I = 2, where the first is at compile time and the second assignment is left in place to wait for execution. In other words, the process of putting variable and function declarations at the top of their scope is called promotion.
You may be wondering, why don’t let and const have variable promotions? This is because at compile time, when a variable declaration is encountered, the compiler either pushes it to the top of the scope (var declaration) or puts it in a temporary dead zone (TDZ), which is a variable declared with let or const. Accessing a variable in the TDZ triggers a runtime error, and the variable is not accessible until it is removed from the TDZ after the variable declaration statement has been executed.
Can you answer all of the following examples correctly?
typeof null; // 'object'
typeof []; // 'object'
typeof someStr; // 'undefined'
typeof str; // Uncaught ReferenceError: str is not defined
const str = 'Yancey';
Copy the code
First, since null is basically a pointer, it returns ‘object’. In Javascript, the first three bits of a binary are all zeros, so the first three bits of a null binary are all zeros, so ‘Object’ is returned when typeof is executed.
The second thing I want to emphasize is that Typeof takes ‘object’ for all variables that refer to a type, so the operator cannot correctly identify a specific type, such as Array or RegExp.
Third, when typeof an undeclared variable, an error is not reported, but ‘undefined’ is returned.
Fourth, STR first exists in the TDZ, which states that accessing variables in the TDZ triggers a runtime error, so this code directly reports an error.
Function increase
Both function declarations and variable declarations are promoted, but it is worth noting that functions are promoted first, before variables.
test();
function test() {
foo();
bar();
var foo = function() {
console.log("this won't run!");
};
function bar() {
console.log('this will run! '); }}Copy the code
The above code will look like this: the internal bar function will be promoted to the top, so it can be executed; The variable foo is then promoted to the top, but the variable cannot be executed, so foo() is an error.
function test() {
var foo;
function bar() {
console.log('this will run! ');
}
foo();
bar();
foo = function() {
console.log("this won't run!");
};
}
test();
Copy the code
closure
Closures are functions that can access independent (free) variables that are used locally but defined in a closed scope. In other words, these functions can “remember” the environment in which they were created
A closure is a function that has access to the scope of another function.
Function objects can be related to each other via scope chains, and variables inside the body of a function can be stored within the scope of the function, a property known in the computer science literature as closures.
Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope.
The last explanation seems to be easier to understand, so let’s learn about closures from “remember and access”.
What is “remember”?
In JavaScript, if a function is already called and will not be used in the future, the garbage collection mechanism (described below) destroys the scope created by the function. We know that a variable that refers to a type is just a pointer and does not copy the actual value to the variable, but rather passes the object’s location to the variable. Thus, when a function is passed to a variable in an undestroyed scope, the function exists because the variable exists, and because the function exists depends on the lexical scope of the function, the lexical scope of the function also exists, thus “remembering” the lexical scope.
Look at the following example. When the Apple function is executed, a reference to output is passed as an argument to the ARG of the fruit function, so the ARG exists during the fruit function execution, so output exists, The local scope generated by the Apple function output depends on also exists. This is why output remembers the scope of apple.
function apple() {
var count = 0;
function output() {
console.log(count);
}
fruit(output);
}
function fruit(arg) {
console.log('fruit');
}
apple(); // fruit
Copy the code
Remember and visit
The above example is not a complete “closure “, because the scope is” remembered “, but not “accessed”. To modify the above example a little bit, executing the arg function in the fruit function is actually executing output and also accessing the count variable in the Apple function.
function apple() {
var count = 0;
function output() {
console.log(count);
}
fruit(output);
}
function fruit(arg) {
arg(); // This is the closure!
}
apple(); / / 0
Copy the code
Loops and closures
Here’s a classic interview question. We want the code to print 0 to 4, one per second, one at a time. But in reality, this code will print five 5’s at a rate of one per second at run time.
for (var i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
Copy the code
SetTimeout is executed asynchronously. After 1000 milliseconds, a task is added to the task queue. The task in the task queue will be executed only when all the tasks on the main thread are executed. So I is all 5. Because the I declared by var in the for loop is in the global scope, the printed I in the timer function is always 5.
We can generate a new scope for each iteration by using IIFE within the iteration, so that the callback of the delay function can enclose the new scope within each iteration, and each iteration will contain a variable with the correct value for us to access. The code is shown below.
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
Copy the code
If you look carefully at the API, you can also write it in the following form:
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, i * 1000, i);
}
Copy the code
Of course, the best way to declare I is to use let, where the variable I is applied to the loop block, and each iteration initializes the variable with the value at the end of the previous iteration.
for (let i = 0; i < 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
Copy the code
The garbage collection
As mentioned above, if a function has been called and will not be used in the future, the garbage collection mechanism destroys the scope created by the function. JavaScript has two types of garbage collection mechanisms, tag removal and reference counting, and tag removal is used by most modern browsers.
Mark clear
When the garbage collector runs, it marks all variables stored in memory, and then it unmarks variables in the environment and those referenced by variables in the environment. Variables tagged after this point are considered to be ready for deletion because they are no longer accessible to variables in the environment. Finally, the garbage collector completes the memory cleanup, destroying the tagged values and reclaiming the memory they occupy.
Reference counting
Reference counting keeps track of how many times each value is referenced. When a variable is declared and a reference type value is assigned to the variable, the number of references is 1; Conversely, if a variable containing a reference to this value acquires another value, the number of references is reduced by one; The next time you run the garbage collector, you can free up memory for values with zero references. Disadvantages: Circular references cause the number of references to never be zero.
conclusion
Q: What is scope?
A: Scope is A set of rules for finding variables by name.
Q: What is a scope chain?
A: Scope nesting occurs when A block or function is nested within another block or function. Therefore, when a variable is not found in the current scope, the search continues in the outer nested scope until the variable is found or the global scope is reached, and an error is reported if it is not found in the global scope. This pattern of ascending searches is the scope chain.
Q: What is a closure?
A: Closures occur when A function can remember and access its lexical scope, even if the function is executed outside the current lexical scope.
The last
The root cause of this long article is the damn var keyword of the interview! It’s a design error! Don’t use it!
End with a question: Write a function that returns 0 the first time it is called, and 1 more on each subsequent call. This problem is not difficult, mainly looking at closures and immediate execution of functions. My answers are as follows. If you have a better idea, please share in the comments section.
const add = (() = > {
let num = 0;
return (a)= >num++; }) ();Copy the code
reference
JavaScript Advanced Programming (3rd edition) by Nicholas C. Zakas
Deep Understanding of ES6 by Nicholas C. Zakas
JavaScript You Don’t Know (Volume 1) by Kyle Simpson
Javascript lexical scope
JavaScript Explorer: scope and closure
In-depth understanding of JavaScript scopes and scope chains
JavaScript compilation principles, compilers, engines, and scopes
Scope closures, do you really get it?
Welcome to pay attention to my public number: the front of the attack