After reading “JavaScript you don’t know (Volume 1)”, I felt inspired. After a few months, I felt almost forgotten. Today, I am going to make a note while brushing, and record some key ideas in the book for future review.
Today, I will review and summarize the first part: scope and closure.
1. What is scope
In programming languages, the ability to store and access variables brings state to the program
Rules about where variables are stored and how programs find them are called scopes
1.1 Compilation Principles
Any snippet of JavaScript code is compiled before execution (usually just before execution)
- Word segmentation/lexical analysis (
Tokenizing
/Lexing
)
Decompose a string of characters into blocks of code that make sense (to the programming language), called lexical units (tokens)
- Analysis/Grammar analysis (
Parsing
)
Converts a stream of lexical units (arrays) into a hierarchical nested tree of elements that represents the syntactic structure of the program.
This Tree is called the Abstract Syntax Tree (AST).
- Code generation
Transform the AST into executable code
1.2 JS compilation principle
- Engine – responsible for compiling and executing JS programs
- Compiler – responsible for parsing and code generation
- Scope – Is responsible for collecting and maintaining a series of queries made up of all variables (identifiers) and enforcing a set of rules that determine access to these identifiers by currently executing code
Take the assignment of a variable as an example
An assignment to a variable performs two actions:
- The compilerIn the currentscopeIn theThe statementA variable (if not previously declared)
- At runtime,engineWill be inscopeIn theTo find theThis variable is assigned if it can be found
How does the engine find variables
The engine uses LHS and RHS queries to find variables
An LHS query is performed when a variable appears on the left side of an assignment (find the location of the store and assign it) and an RHS query is performed when a variable appears on the right side (find its specific value)
LHS and RHS meaning “left or right side of assignment” does not necessarily mean “= left or right side of assignment operator”.
Assignment operations take several other forms, so conceptually they are best understood as “Who is the target of an assignment operation (LHS)” and “who is the source of the assignment operation (RHS)”
A simpler way to remember is: [left to find the position, right to find the value]
② Practice of LHS and RHS
function foo(a){ // 2. LHS finds the position of a and assigns 2 to a
var b = a; // 3. RHS find a value 4. LHS find b position, a value 2
return a + b; // 5. RHS looks for the value of a
}
var c = foo(2) // 1. RHS finds foo 7. LHS finds c and assigns it to foo(2) 4
Copy the code
(3) a few words of BB
LHS and RHS here remind me of getters and setters. LHS is the equivalent of a setter, finding its position, assigning a value to it. RHS is the equivalent of a getter, finding its value
1.3 Scope chain
When scope nesting occurs, when a variable cannot be found in the current scope, the engine continues to look in the outer nested scope until it finds the variable or reaches the outermost scope (that is, the global scope)
1.4 RHS and LHS cannot be found
If the RHS query does not find the desired variable in all nested scopes, the engine throws ReferenceError.
When the engine executes an LHS query, if the target variable cannot be found in the top-level (global scope), the global scope creates a variable with that name and returns it to the engine
Instead of creating and returning a global variable when an LHS query fails in strict mode, the engine throws a ReferenceError exception similar to that when an RHS query fails
1.5 summarize
-
Scope is a set of rules for determining where and how to look for variables (identifiers)
-
If the purpose of the lookup is to assign a value to a variable, an LHS query is used;
If the goal is to get the value of a variable, RHS queries are used
-
Both LHS and RHS queries start in the current execution scope and follow the scope to find the global scope
-
LHS can not find an error, RHS can not find an error
2. JS scope
2.1 Lexical scope
There are two main working models of scope: lexical and dynamic scope JS uses the == lexical scope ==
Lexical scope is the scope of the definition at the lexical stage.
The lexical scope is determined by where you write the variable and block scopes when you write the code, so the lexical analyzer leaves the scope unchanged when it processes the code.
Look at the scoped bubble below,
The scope created for foo contains three identifiers: A, bar and b. The scope created for bar contains only one identifier: C
The structure of scoped bubbles and their positional relationships give the engine enough positional information to use to find the location of identifiers. The scoped lookup stops when the first matching identifier is found.
No matter where or how a function is called, its lexical scope is determined only by the position in which the function is declared.
Lexical scope means that the scope is determined by the position of the function declaration when the code is written. The lexical analysis phase of compilation basically knows where and how all identifiers are declared, and thus can predict how they will be looked up during execution.
2.2 Function scope
The meaning of a function scope is that all variables belonging to the function can be used and reused throughout the scope of the function (in fact, nested scopes can also be used)
The advantage of function scope is that the internal implementation can be hidden, and the outer scope cannot access anything inside the wrapper function
On function declarations and function expressions
If function is the first word in the declaration, it is a function declaration; otherwise, it is a function expression
Execute the function expression immediately (IIFE
)
(function foo(){/ /... }) ()
(function(){/ /... } ())
(function IIFE(global){/ /... })(window)
Copy the code
2.3 Block-level scope
ES6 variables declared with lets and const have block-level scope
Block-level scope in try/catch
The catch clause of try/catch creates a block scope in which declared variables are only valid inside the catch.
2.4 summarize
Any variable declared in a scope will be attached to that scope
3. Variable promotion
Part of the compile phase is finding all the declarations and associating them with the appropriate scope.
All declarations, including variables and functions, are processed first before any code is executed
Functions are promoted before variables
The declaration itself is promoted, but assignment operations, including assignment of function expressions, are not
4. Scope closures
Closures are a natural consequence of writing code based on lexical scope
Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope
In other words,
The lookup of a variable is in the parent scope where the function is defined, not in the execution place
Look at an example [function as return value]
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); / / 2
Copy the code
This is really just calling the internal function bar() with a different identifier reference
Bar executes outside of its own lexical scope
Because bar declares its location in Foo, it has a closure that covers foo’s internal scope, allowing that scope to live forever and not be GC; Bar () holds a reference to this scope, which is called a closure
No matter how an inner function is passed outside its lexical scope, it holds a reference to the original definition scope and uses closures wherever the function is executed.
Let’s look at another example.
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait('Hello, closure! ')
Copy the code
Pass the function (timer) to setTimeout(..) .
Because a timer keeps a reference to the variable message, a timer has a closure that covers the wait scope.
So wait (..) Its internal scope does not disappear after 1000 milliseconds.
Inside the engine, the built-in utility function setTimeout(..) Holds a reference to a parameter (callback function). The engine calls this function (timer), and the lexical scope remains intact during this process.
That’s the closure.
Essentially, whenever and wherever you pass functions (accessing their respective lexical scopes) around as first-level value types, you’ll see closures applied to those functions.
Whenever you use callbacks in timers, event listeners, Ajax requests, cross-window communication, Web Workers, or any other asynchronous (or synchronous) task, you’re actually using closures!
practice
Function as [return value]
function create() {
let a = 100
return function() { / / define
console.log(a)
}
}
let fn = create()
let a = 200
fn() / / 100
Copy the code
② Function as [parameter]
function print(fn) {
let a = 200
fn() / / execution
}
let a = 100
function fn() { / / define
console.log(a)
}
print(fn) / / 100
Copy the code
The variable lookup is in the functiondefineLook up in the parent scope instead of inperformThe place where
Note: The value of this is determined during execution, not during definition
Application of closures in instance development
Extend for loops and closures
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
Copy the code
The execution outputs five sixes because the timer is executed after the end of the loop
Here’s why: We try to assume that each iteration in the loop “captures” itself a copy of I at run time. But according to how scopes work, the reality is that although the five functions in the loop are defined separately in each iteration, they are all enclosed in a shared global scope, so there is really only one I.
To do this, we need more closure scopes, especially one for each iteration of the loop.
【 improvement 1】 Consider using IIFE to create scope.
for (var i = 1; i <= 5; i++) {
(function() {
setTimeout(function timer() {
console.log(i);
}, i*1000); }) (); }Copy the code
However, this IIFE is just an empty scope with nothing, so it still doesn’t achieve the desired effect
[Refinement 2] Create a non-empty scope in IIFE
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j*1000); }) (); }Copy the code
You need to have your own variable in IIFE that stores the value of I in each iteration
[Improvement 3] Pass as a definition parameter in IIFE
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j*1000);
})(i);
}
Copy the code
Using IIFE within an iteration generates a new scope for each iteration, allowing the timer to enclose the new scope within each iteration, with a variable with the correct value for us to access within each iteration
[Improvement 4] Use the block-level scope of let
for (var i = 1; i <= 5; i++) {
let j = i;
setTimeout(function timer() {
console.log(j);
}, j*1000);
}
Copy the code
This is essentially what converts a block into one that can be closed
[Improvement 5] Final version
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
Copy the code
The let declaration in the header of the for loop also has a special behavior. This behavior indicates that the variable is declared not just once during the loop, but every iteration. Each subsequent iteration initializes this variable with the value at the end of the previous iteration.
conclusion
Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope
5. What closures do in real development: Hide data
example
Do closures hide data? They only provide apis
function createCache(){
const data = {} // The data in the closure is hidden from outside access
return {
set: function (key, val){
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
const c = createCache()
c.set('a'.100)
console.log(c.get('a'))
Copy the code
The module
This pattern is called a module in JavaScript. The most common way to implement a module pattern is often referred to as module exposure, a variation of which is shown below.
function CoolModule() {
let something = "cool";
let another = [1.2.3];
function doSomething() {
console.log(something);
}
function doAnther() {
console.log(another.join("!"));
}
return {
doSomething,
doAnther,
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnther(); / / 1! 2! 3
Copy the code
Neither the inner scope nor the closure can be created without executing an external function
There are two requirements for the module pattern:
- There must be an external enclosing function that must be called at least once (each call creates a new module instance).
- Enclosing functions must return at least one inner function so that the inner function can form a closure in the private scope and can access or modify the private state.
Module in singleton mode
Convert the module function to IIFE, call the function immediately and assign the return value directly to the singleton’s module instance identifier foo
var foo = (function CoolModule() {
let something = "cool";
let another = [1.2.3];
function doSomething() {
console.log(something);
}
function doAnther() {
console.log(another.join("!"));
}
return{ doSomething, doAnther, }; }) (); foo.doSomething();// cool
foo.doAnther(); / / 1! 2! 3
Copy the code
conclusion
Modules have two main features:
- A wrapper function is called to create the inner scope
- The return value of the wrapper function must include at least one reference to the inner function, so that a closure covering the entire inner scope of the wrapper function is created
6. Dynamic scope
Lexical scope is a set of rules about how and where the engine finds variables. Its most important feature is that its definition takes place at the writing stage of the code (assuming you don’t use eval() or with).
Dynamic scopes don’t care how or where functions and scopes are declared, just where they are called from in other words, scope chains are based on call stacks, not nested scopes in code, right
JavaScript doesn’t actually have dynamic scope. It only has lexical scope but the this mechanism is somewhat like dynamic scope
The main difference: lexical scope is determined at code or definition time, while dynamic scope is determined at run time. (This too!) Lexical scopes focus on where functions are declared, while dynamic scopes focus on where functions are called from.
Expand this
The arrow function behaves completely differently from normal functions when it comes to the this binding. It abandons all the rules of the normal this binding and instead overrides the original value of this with the current lexical scope.
This will be introduced in the following reading notes blog