Scope and closure
Scope and closure are very important concepts in JS, and they are inseparable. To understand closure, we must start from understanding scope
What is the scope
A scope is a set of rules that say, where are variables stored? How can the program find them when it needs them?
There are two main working models of scope. The first, the most common, is the lexical scope adopted by most programming languages, the lexical scope formally adopted by JS. The other, called dynamic scoping, is still used by some programming languages, such as Bash scripts
Understanding lexical scope requires an understanding of compilation. A piece of source code goes through three steps before execution, known collectively as compilation
- Word Segmentation/Lexical Analysis (Tokenizing/Lexing)
This process breaks down a string of characters into meaningful code blocks called lexical units, for example: var a = 2; This line of code is typically decomposed into the following lexical units: var, a, =, 2,; .
- Parsing/Parsing
This process is to convert lexical units into a hierarchical nested Tree of elements that represents the Syntax structure of the program. This Tree is called an Abstract Syntax Tree (AST).
- Code generation
The process of turning an AST into executable code is called code generation. This process is related to information about language, target platform, etc.
Simply put, lexical scope is the scope of a definition at the lexical stage. In other words, the lexical scope is determined by where you write the variable and block scopes when you write the code, so the lexical analysis will keep the scope constant when processing the code, right
A profound
Consider the following code:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3)
}
foo(2); / / 2, 4, 12
Copy the code
There are three hierarchically nested scopes in this example.
- Global scope, with a single identifier: foo
- Scope created by foo with three identifiers: a, bar, and b
- The scope created by bar contains an identifier: c
When console.log is executed and references to variables a, B, and C are searched, the engine starts at the innermost scope, the bar function scope. The engine cannot find a in this scope, so it will continue at the next level up to foo.
Two principles for finding scopes:
- Scope look-ups start at the innermost scope of the runtime and work outward
- The scope stops when the first matching identifier is found
Understand lexical scope again
Lexical scope means that the scope is determined by the position of the function declaration when the code is written, and the lexical phase of compilation basically knows where and how all identifiers are declared, so that it can predict how it will be looked up during execution
Example code to illustrate:
function foo() {
console.log(a); / / 2
}
funtion bar() {
var a = 3;
foo();
}
var a = 2;
bar();
Copy the code
The search process of A is as follows:
- Now I can’t find it in foo
- Go to the upper scope of Foo, and notice that the upper scope of Foo is global, not bar, because foo is defined globally (and the lookup rules are determined at this point) and bar only calls foo
The lexical scope makes a in foo() refer to a in the global scope, so 2 is printed
It takes a long time to come out
One of the most important yet elusive, almost mythical concepts in this language is closure
Let’s look at a piece of code that clearly shows closures
function foo() {
var a = 2;
funcion bar() {
console.log(a)
}
return bar;
}
var baz = foo()
baz(); // 2, that's what closure does
Copy the code
The lexical scope of the function bar() has access to the inner scope of foo(), but we pass the function bar itself as a value type. After foo() is executed, its return value (that is, the inner bar() function) is assigned to the variable baz and baz() is called. Bar () can obviously be executed. But in this case, it executes outside of its own lexical scope
After foo() is executed, it is usually expected that foo() ‘s entire internal scope will be destroyed, so we know that the engine has a garbage collection mechanism to free up unused memory, and since it looks like foo()’ s contents will no longer be used, it is a natural consideration to recycle them.
The magic thing about closures is that they prevent this from happening. In fact, the inner scope is still there, so it’s not reclaimed. Who uses this inner scope? Bar () is in use
Bar () still keeps a reference to the changed scope, and that reference is called a closure
Still holding lute half cover face
Closures are more than just fun to use. What about the fact that the previous code was artificially structurally modified to explain how to use closures
var a = 2;
(function IIFE() {
console.log(a) / / 2}) ()Copy the code
Although this code works, it is not technically a closure. Why? Because the function (IIFE()) does not execute outside its own lexical scope
Loops and closures
for(var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)}Copy the code
Normally, we would expect this code to print the numbers 1-5, one at a time, one at a time, but in reality, this code will run at one time, five sixes per second
Why is that?
So, first of all, where does 6 come from? The termination condition of the loop is that the value of I is 6, so the output is the final value of I at the end of the loop, right
If you think about it, the callback to the delay function is executed at the end of the loop. In fact, when the timer runs, all the callback functions are executed at the end of the loop, so it prints a 6 each time
This leads to a further question: what are the flaws in the code that cause it to behave differently than the semantics suggest?
Defects are we trying to assume that cycle of every iteration at run time to capture a copy of the I, but according to the principle of scope, five of the actual situation is that although circulation function is defined, respectively, in each iteration, but they are closed in the global scope of a Shared, so there will be an I really only
In this case, of course, all functions share a reference to I
How to resolve defects?
We need more closure scopes, especially one for each iteration of the loop
IIFE creates a scope by declaring and immediately executing a function
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 callback of the delay function to enclose the new scope within each iteration, and each iteration will contain a variable with the correct value for us to access
The same thing as the same thing
Closures in React hooks
Closure in useEffect
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function() {
setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000); } []);return (
<div>
{count}
<button onClick={()= > setCount(count + 1) }>
Increase
</button>
</div>
);
}
Copy the code
After clicking the button multiple times, the console prints Count is 0, which has actually been incremented multiple times
Why is that?
Count is initialized to 0 on the first rendering. After the component is mounted, useEffect calls setInterval(log, 2000) to print every 2 seconds. The count variable captured by the closure log is 0. Even if count is incremented multiple times, the log closure still uses the initial rendered value of count=0.
Fix the problem above
useEffect(function() {
const id = setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
return function() {
clearInterval(id);
}
}, [count]);
Copy the code
Properly set dependencies, useEffect updates the closure as count changes
Closures in useState
function DelayedCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count + 1);
}, 1000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>Increase async</button>
</div>
);
}
Copy the code
Click the button twice quickly and count is still 1, not 2
Delay 1 second after each click to call delay function, delay function capture count is always 0, both delay functions are closures, update the same value: SetCount (count + 1) = setCount(0 + 1) = setCount(1
To fix this, we update count with the function setCount(count => count + 1). The callback returns a new state based on the previous state.
Vue source closure
function defineReactive(obj, key, value) {
return Object.defineProperty(obj, key, {
get() {
return value;
},
set(newVal){ value = newVal; }})}Copy the code
Value is a parameter in a function and is a private variable, but why use value externally, or assign a value to vulue?
According to the properties of the closure, the inner function can refer to the outer function’s variables, when there is this reference relationship variables not recycling garbage collection mechanism, when you get this variable is actually call the inner layer of the get function, when you set the variable is actually call set function, is to the operation of the value parameter
Redux source closure
function applyMiddleware(. middlewares) {
return (createStore) = > (reducer) = > {
const store = createStore(reducer)
let dispatch = store.dispatch
const midApi = {
getState: store.getState,
dispatch: (action) = > dispatch(action) // The closure's use captures the modified dispatch
}
const chain = middlewares.map(middleware= > middleware(midApi))
// Modified dispatchdispatch = compose(... chain)(store.dispatch)return { ...store, dispatch }
}
}
Copy the code
Why isn’t the dispatch property in midApi written as follows?
const midApi = {
getState: store.getState,
dispatch: dispatch
}
Copy the code
It’s important to understand that Redux is using middleware to rewrite the dispatch method. Make sure that dispatches sent to middleware functions are modified dispaths, otherwise some middleware may not work. To ensure that each dispatch passed to the middleware function is a modified dispatch, this should be written as a closure rather than as a store.dispatch.
reference
[1] the Javascript You Didn’t Know Series