In this article, we will talk about closures. Many articles, including some authoritative books, have different interpretations of closures, and everyone’s understanding is also different. There are also different implementations of closures in other languages, so let’s take a look at how and what features closures are implemented in Javascript.
To get straight to the point, here’s a short piece of code:
function outer(count) {
var temp = new Array(count)
function log() {
console.log(temp)
}
log(a)function inner() {
console.log('done')}return inner
}
var o = {}
for(var i = 0; i < 1000000; i++) {
o["f"+i] = outer(i)
}
Copy the code
If you don’t know the problems this code can cause, this article is worth reading.
Execute context & scope chain
Let’s put that aside and take a look at this simple code:
function outer() {
var b = 2
function inner() {
console.log(a, b)
}
return inner
}
var a = 1
var inner = outer()
inner()
Copy the code
In the JS engine, the code is managed and executed by executing the context stack. The pseudo execution process of the code is as follows (this section main reference Yuba great series of articles) :
0. Start the program
ECStack = []
Copy the code
1. Create the globalContext globalContext and push it onto the stack
ECStack = [
globalContext
]
Copy the code
2. Initialize the global context before execution
globalContext = {
VO: {
a: undefined,
inner: undefined,
outer: function outer() {... } }, Scope: [globalContext] }Copy the code
Initialize the Scope chain attribute to [globalContext]. This code has not yet been executed. The inner and a variables are undefined due to the promotion of the variables. Outer function’s scope [[scope]] internal property is determined (static scope) :
outer.[[scope]] = [
globalContext.VO
]
Copy the code
3. Execute globalContext
Inner = outer(); outer(); outer(); outer();
4. Create the outerContext execution context and push it to the stack
ECStack = [
outerContext,
globalContext
]
Copy the code
Initialize the outerContext execution context
outerContext = {
VO: {
b: undefined,
inner: function inner() {... } }, Scope: [VO, globalContext.VO] }Copy the code
Initialize the Scope chain attribute to [VO].concat(outer.[[Scope]]) that is, [VO, globalContext.vo]. At this point, the inner function’s [[scope]] property is determined:
inner.[[scope]] = [
outerContext.VO,
globalContext.VO
]
Copy the code
6. Run the outerContext context
Execute statement b = 2, set b in VO to 2, and return inner.
OuterContext completes execution, exits the stack, and continues back to globalContext to execute the rest of the code
ECStack = [
globalContext
]
Copy the code
Proceed with the assignment of inner = outer() and assign the result of outer to the inner variable.
Execute inner() to enter the execution context of the inner function.
Create the innerContext execution context and push it onto the stack
ECStack = [
innerContext,
globalContext
]
Copy the code
Initialize innerContext execution context
innerContext = {
VO: {},
Scope: [VO, outerContext.VO, globalContext.VO]
}
Copy the code
Initializing the Scope chain attribute Scope is [VO].concat(inner.[[Scope]]) i.e.[VO, outerContext.vo, GlobalContext.vo].
Execute the innerContext context
Log (a, b), VO, globalContext.VO, globalContext.VO, globalContext.VO, globalContext.VO, globalContext.VO, globalContext.VO, globalContext.VO, globalContext. log(a, b), VO, globalContext. log(a, b), VO, globalContext. log(a, b), VO, globalContext. log(a, b), VO, globalContext. log The console.log function is executed, which also involves the scope chain lookup of the variable console, and the execution context switch of the console.log function.
GlobalContext completes execution, exits the stack, and the program ends
ECStack = []
Copy the code
In step 7, after the outerContext completes execution, you can see that innerContext.Scope still has a reference to outerContext.vo, even though it is removed from the stack and then reclaimed by the garbage collection mechanism. When the outerContext is reclaimed, the outerContext.VO will not be reclaimed, as shown below:
And that’s what makes us performinner
Function can still be accessed through its chain of scopesouter
Variables in a function, that’s the closure.
We introduced the concept of closures by implementing context and scope chains, so let’s move on.
VO, globalContext.vo] is the inner function whose scope is [[scope]]. This is incorrect.
Let’s add two lines of code with a slight change:
function outer() {
var b = 2
var c = new Array(100000).join(The '*')
var d = 3
function inner() {
console.log(a, b)
}
return inner
}
var a = 1
var inner = outer()
inner()
Copy the code
You’ll be smart enough to notice that the variables c and D are not used in inner. If you set the inner function’s [[scope]] property to [outerContext.vo, globalContext.vo], If the inner function is not executed, the memory occupied by the new Array(100000).join(‘*’) will not be freed.
The inner function [[scope]] attribute is defined by the inner variable. In fact, the JS engine is as smart as you are and does just that, in the Chrome debugger:
As you can see, there is no reference to c. We can say that inner [[scope]] is:
inner.[[scope]] = [
Closure(outerContext.VO),
globalContext.VO
]
Copy the code
Here, we use Closure as a function to represent Closure within the inner function body (including the inner function within the inner function, and so on…). A collection of references to external function variables, that is, closures.
Sharing a closure
Let’s go ahead and change the above code slightly:
function outer() {
var b = 2
var c = new Array(100000).join(The '*')
var d = 3
function log() {
console.log(c)
}
function inner() {
console.log(a, b)
}
log(a)return inner
}
var a = 1
var inner = outer()
inner()
Copy the code
Here, we’re just adding a log function and printing out the variable c. For the inner function, nothing has changed, has it? Let’s take a look at the scope and closure information under Chrome debugger.
Outer function before executing:
Outer completes:
The inner closure contains the variable C! The inner function doesn’t use C, so you might notice something. Yes, we reference c in the log function, which affects the closure of the inner function.
In the previous section, we stated that when we define the inner [[scope]] property, we use the Closure function to get all the Closure variables referenced within the inner function. How many inner functions?
Closure(outerContext.vo); outer; outer; Closure(outerContext.vo); outer; Closure(outerContext.vo);
inner.[[scope]] = [
Closure(outerContext.VO),
globalContext.VO
]
log.[[scope]] = [
Closure(outerContext.VO),
globalContext.VO
]
Closure(outerContext.VO) = { b, c }
Copy the code
Let’s look at the closure information for the log function, which also has variable B:
So here, you might be wondering, where is variable a, which is in the globalContext.
At this point, if you are careful, you will find that this code is almost the same as the code given at the beginning of the article. What kind of problem will it bring? I think you should know: memory leak!
Let’s go back to the code at the beginning of this article. The returned inner function keeps referring to the temp variable, which can never be garbage collected without the inner function being executed.
Let’s change the code a bit:
function outer(count) {
var temp = new Array(count)
function log() {
console.log(temp)
}
log(a)function inner() {
var message = 'done'
return function innermost() {
console.log(message)
}
}
return inner()
}
var o = {}
for(var i = 0; i < 1000000; i++) {
o["f"+i] = outer(i)
}
Copy the code
Here, we wrap a layer inside the inner function. Is the innermost returned with a reference to the temp variable?
Following the previous logic about execution context, there is. Innermost’s [[scope]] properties are as follows:
innermost.[[scope]] = [
Closure(innerContext.VO): { message },
Closure(outerContext.VO): { temp },
globalContext
]
Copy the code
Of course, you might say that as soon as the inner function completes, the memory will be reclaimed. OK, let’s look at a more classic example:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join(The '*'),
someMethod: function() { console.log(someMessage); }}; };setInterval(replaceThing, 1000);
Copy the code
The unused function refers to originalThing. Due to the shared closure, the closure of theThing. SomeMethod also contains a reference to originalThing, which is the previous theThing. So the next theThing references the last theThing, forming a chain. And as setInterval executes, the chain gets longer and longer, resulting in a memory leak like this:
If you change the interval to smaller, minutes out of memory.
This example comes from here, and I suggest you all click on it to read it. (I remember a friend translated this article before, but I can’t find it. If you know the Link of Chinese translation, post it in the comments.)
Real Local Variable
vs Context Variable
Real Local Variable, literally translated, is a Real Local Variable. In this case, Variable d is Real Local Variable. At the level of C++, it can be directly allocated on the stack and immediately recycled after the completion of the stack operation of inner function. There is no need for a later garbage collection mechanism to intervene.
Context Variable, or closure Variable, in this case the Variable b is Context Variable, and at the C++ level, it must be allocated to the heap, even though it’s a primitive type.
C is a Real Local Variable, but it stores the memory address to this new Array() on the stack, and the actual contents of the new Array() are stored on the heap.
Memory distribution is as follows:
From the above analysis, the statement that primitive types are distributed on the stack and reference types are distributed on the heap is clearly not true. Variables referenced by closures, no matter what type, must be allocated on the heap.
eval
With the closure
As mentioned in the previous article, the JS engine analyzes which external function variables are referenced in all internal function bodies, but does not analyze direct calls to eval. Because it is impossible to predict which variables might be accessed in eval, all variables in external functions are included.
function outer() {
var b = 2
var c = new Array(100000).join(The '*')
var d = 3
function inner() {
eval("console.log(1)")}return inner
}
var a = 1
var inner = outer()
inner()
Copy the code
The JS engine’s internal OS is like this: eval will do anything, you (local variables) are not allowed to go!
If you run an eval underneath a nested function, all the variables in the parent function cannot be released.
What about indirect calls to eval?
function outer() {
var b = 2
var c = new Array(100000).join(The '*')
var d = 3
function inner() {(0,eval) ("console.log(a)"// Output 1}return inner
}
var a = 1
var inner = outer()
inner()
Copy the code
Eval is a local variable. You can go home and collect your clothes.
There are various postures for eval and function combinations, such as:
function outer() {
var b = 2
var c = new Array(100000).join(The '*')
var d = 3
return eval("(function() { console.log(a) })") / /return (0,eval) ("(function() { console.log(a) })") / /return (function() {return eval("(function(){ console.log(a) })")}) () / /... // More postures are left for you to explore and try, escape... } var a = 1 var inner = outer() inner()Copy the code
That’s it. I hope you have a new understanding and insight into closures.
Finally, we welcome all the bosses to slap each other in the face…