Closures are one of the most fundamental and important concepts in JavaScript, and many developers know them “inside out.” However, closures are by no means a single concept: they involve scope, scope chain, execution context, memory management, etc. Whether you’re a novice or an “experienced driver,” there’s a common “I think I’ve figured out the closure, but I’m still going to roll over in a few scenarios.” In this lesson we will sort through this topic and conclude with “should questions” to reinforce understanding closures.

Basic knowledge of

scope

A scope is simply a set of rules that determine how to look for variables in a particular scenario. Any language has the concept of scope, and the same language will improve its scope rules as it evolves. For example, in JavaScript, before ES6 came along, there was only a functional scope and a global scope.

Function scope and global scope

To summarize: When JavaScript executes a function and sees a variable reading its value, it first looks for the declaration or assignment of that variable within the function itself. This covers variable declaration and variable promotion, which we will cover later. If the variable cannot be found within the function, go out of the function scope and look for it in a higher scope. The “upper scope” here may also be a function scope, for example:

function bar() {
   var b = 'bar'
   function foo() {
       console.log(b)
   }
   foo()
}
bar()
Copy the code

When foo is executed, the declared or read value of variable b is retrieved in the scope of its upper function bar. At the same time, the “upper scope” can also spread out along the scope until it finds the global scope:

var b = 'bar'
function bar() {
   function foo() {
       console.log(b)
   }
   foo()
}
bar()
Copy the code

As we can see, the search of variable scope is a diffusion process, just like the chain of each link, step by step, which is the origin of the term scope chain.

Block-level scope and temporary dead zones

The concept of scope has evolved, with ES6 adding block-level scope for let and const declared variables, making the scope of JavaScript richer. Block-level scope, as the name implies, is limited to code blocks, a concept common in other languages as well. Of course, the addition of these new features also adds some complexity and introduces new concepts, such as temporary dead zones. It is necessary to expand a bit: when it comes to temporary dead zones, we also need to start with “variable promotion”, as shown in the following code:

function foo() {

   console.log(bar)

   var bar = 3

}

foo()

The output is undefined because the variable bar is promoted inside the function. Is equivalent to:

function foo() {

   var bar

   console.log(bar)

   bar = 3

}

foo()

But when using the LET declaration:

function foo() {

   console.log(bar)

   let bar = 3

}

foo()

Uncaught ReferenceError: bar is not defined.

We know that declaring a variable using let or const creates a closed block-level scope for that variable. In this block-level scope, if the variable is accessed before the declaration, referenceError is reported. If you access a variable after it has been declared, you can get the value of the variable normally:

function foo() {

   let bar = 3

   console.log(bar)

}

foo()

Normal output 3. Therefore, there is a “dead zone” in the scope formed by the corresponding curly braces, starting at the beginning of the function and ending at the line of the related variable declaration. Variables declared by lets or const cannot be accessed in this scope. The professional name of this “Dead Zone” is TDZ (Temporal Dead Zone). The introduction of related Language specifications can be referred to ECMAScript® 2015 Language Specification for readers who like to get to the bottom of the specifications.

Execution context and call stack

Many readers may not be able to accurately define execution context and call stack, but these two concepts have been around for as long as JavaScript has been around. Every line of code we write, every function we write is related to them, but they are invisible, hidden behind the code, in the JavaScript engine. In this section, we take a look at these two familiar but often overlooked concepts.

The execution context is the execution environment/scope of the current code, which is complementary to, but completely different from, the scope chain described above. Intuitively, execution contexts contain scope chains, but they are also like the upstream and downstream of a river: the scope chain is part of the execution context.

Two phases of code execution

To understand these two concepts, start with the execution of JavaScript code, which is not involved in normal development, but is very important for us to understand the JavaScript language and operation mechanism, please read carefully. JavaScript execution is divided into two phases:

  • Code precompilation phase
  • Code execution phase

The precompilation phase is the pre-compilation phase, when the compiler compiles JavaScript code into executable code. Note that this is not the same as traditional compilation, which is very complex and involves word segmentation, parsing, code generation, and so on. Precompilation here is a unique concept in JavaScript, even though JavaScript is an interpreted language that compiles one line and executes one line. But JavaScript engines do do some “prepping” before the code executes.

In the execution phase, the main task is to execute the code, and the execution context is created in this phase.

After the syntax has been parsed and verified, the JavaScript code allocates the memory space of the variables during the pre-compilation phase, where the familiar variable promotion process takes place. The following code:

After the precompilation process, we should note three things:

  • Variable declaration during precompilation;
  • The variable declaration is promoted in the precompilation phase, but the value is undefined.
  • All non-expression function declarations are promoted during precompilation.

Take a look at the following title:

foo(10)

function foo (num) {

   console.log(foo)

   foo = num;       

   console.log(foo)

   var foo

}

console.log(foo)

foo = 1

console.log(foo)

Output:

undefined

10

ƒ foo (num) {

   console.log(foo)

   foo = num     

   console.log(foo)

   var foo

}

1

When foo(10) is executed, the first line of the function prints undefined and the third line of the function prints foo after variable promotion. Then I run the code, and on line 8, console.log(foo) prints the contents of function foo (since foo = num in function foo, num is assigned to variable foo in function scope).

Conclusion The scope is determined during the precompilation phase, but the scope chain is fully generated during the creation phase of the execution context. This is because the function starts to create its execution context when it is called. The execution context includes a variable object, a scope chain, and a reference to this

As shown in the figure:

The entire process of code execution is said to be like an assembly line. The first step is to create a Variable Object in the precompile phase, where it is created without assigning a value. In the next process code execution stage, the variable Object is transformed into an Active Object, that is, VO → AO is completed. At this point, the scope chain is also determined, which consists of the variable objects of the current execution environment and all the outer activations that have been completed. This process ensures orderly access to variables and functions, meaning that if the variable is not found in the current scope, the search continues up to the global scope.

The pipelining of these processes is fundamental to JavaScript engine execution.

The call stack

With that in mind, the function call stack is easy to understand. When we execute a function, if that function calls another function, and that “other function” calls another function, we form a series of call stacks. The following code

function foo1() {

 foo2()

}

function foo2() {

 foo3()

}

function foo3() {

 foo4()

}

function foo4() {

 console.log('foo4')

}

foo1()

Call relationship: foo1 → foo2 → foo3 → foo4 This process starts with foo1 calling foo2, then foo2 calling foo2, and so on, foo3, foo4, until foo4 is finished — foo4 first, then foo3, then foo2, then foo1. This process is “first in, last out” (” last in, first out “) and is therefore called the call stack.

We deliberately miswrote the code in foo4:

function foo1() {

 foo2()

}

function foo2() {

 foo3()

}

function foo3() {

 foo4()

}

function foo4() {

 console.lg('foo4')

}

foo1()

Either way, with the help of the JavaScript engine, we can clearly see the error stack information, the function call stack relationship.

Note that normally, when a function completes and exits the stack, local variables in the function will be collected at the next garbage collection node, and the corresponding execution context of the function will be destroyed, which is why variables defined in the function cannot be accessed from the outside. That is, the variable is accessible to the function only when the function is executing, it is created during precompilation, activated during execution, and the context is destroyed after the function is executed.

closure

With all the precursors, we finally get to closure.

Definition: When a function nested a function, the inner function refers to a variable in the scope of the outer function, and the inner function is accessible in the global context, forming a closure.

Let’s look at a simple code example:

function numGenerator() {

   let num = 1

   num++

   return () => {

       console.log(num)

   }

}

var getNum = numGenerator()

getNum()

In this simple closure example, numGenerator creates a variable num and returns an anonymous function that prints the value of num. This function refers to the variable num and makes it accessible to outsiders by calling getNum. Therefore, after numGenerator has executed, That is, the variable num does not disappear after the relevant call is removed from the stack, and it still has a chance to be accessed by the outside world.

Execute the code and you can clearly see the analysis of the JavaScript engine

numThe value is marked as a Closure, or Closure variable.

By contrast, we know that the internal variables of a function are normally inaccessible and the context is destroyed after the function is executed. But in a function, if we return another function that uses a variable inside the function, the outside world can use the returned function to get the value of the variable inside the original function. That’s the rationale behind closures.

Intuitively, therefore, the concept of closures provides access and convenience to variables within functions in JavaScript. There are many benefits to doing this, such as “modularity” with closures; For example, if you look at the middleware implementation mechanism in the Redux source code, you’ll see that closures are heavily used. And we’re going to talk a lot more about that later in the course. Closures are an essential foundation for advancing the front end. We’ll also help you understand closures better by doing problems later.

Memory management

Memory management is a concept in computer science. Regardless of the programming language, memory management refers to the management of the memory life cycle, and the memory life cycle is nothing more than:

  • Allocating memory space
  • Read/write memory
  • Free up memory

Let’s use code as an example:

Var foo = 'bar' // Allocates space to variables in heap memory

Alert (foo) // Use memory

Foo = null // Free memory space

Basic concepts of memory management

We know that memory space can be divided into stack space and heap space, where

  • Stack space: automatically allocated by the operating system to store function parameter values, local variable values, etc., operates in a manner similar to stacks in data structures.
  • Heap space: Usually allocated by the developer, this space should be considered for garbage collection.

In JavaScript, data types include (without the ES Next new data type) :

  • Basic data types, such as Undefined, Null, Number, Boolean, String, etc
  • Reference types, such as Object, Array, Function, etc

In general, base data types are held in stack memory and reference types are held in heap memory. The following code:

var a = 11

var b = 10

var c = [1, 2, 3]

var d = { e: 20 }

The behavior of allocating memory and reading and writing memory is relatively consistent in all languages, but freeing memory space varies from language to language.For example, JavaScript relies on the host browser’s garbage collection mechanism, which is generally not a programmer’s concern. But that doesn’t mean everything is fine, and memory leaks can still occur in some cases.

A memory leak is a memory space that is no longer in use, but for some reason is not released. This is a very “metaphysical” concept, because whether or not memory space is still in use is somewhat undecidable, or costly to determine. The danger of a memory leak is straightforward: it can cause a program to slow down or even crash.

Example of memory leakage scenario

Let’s look at a few typical examples of memory leaks:

var element = document.getElementById("element")

element.mark = "marked"



// Remove the element node

function remove() {

   element.parentNode.removeChild(element)

}

In the above code, we simply removed the element, but the variable Element still exists and the memory occupied by the element cannot be freed.

Here’s another example:

var element = document.getElementById('element')

Element. innerHTML = 'Click'



var button = document.getElementById('button')

button.addEventListener('click', function() {

   // ...

})



element.innerHTML = ''

After this code is executed, the button element has been removed from the DOM because element.innerhtml = “, but its event handler is still there, so it cannot be garbage collected. We also need to add removeEventListener to prevent memory leaks.

function foo() {

 var name  = 'lucas'

 window.setInterval(function() {

   console.log(name)

}, 1000).

}



foo()

The name memory cannot be freed due to the presence of window.setInterval. If it is not required by the business, remember to use clearInterval to clear the name memory when appropriate.

Browser garbage collection

Of course, most scene browsers rely on:

  • Mark clear
  • Reference counting

Two algorithms for active garbage collection. There are a lot of good articles on this topic in the content community, but I have a few of my favorites to share with you. These articles are more about browser engine implementation, and I don’t want to cover them too much here, but you can refer to the following:

  • Understand JavaScript memory management through the garbage collection mechanism
  • How do I handle JavaScript memory leaks
  • The garbage collection
  • Write memory-friendly code
  • Four common memory leak traps in JavaScript
  • Write down a web page memory overflow analysis and resolution practice

Memory leaks and garbage collection considerations

Memory leaks and garbage collection should be analyzed in practice rather than in theory, as browsers are constantly evolving and changing. As you can see from the example above, binding data variables with closures protects blocks of memory for these data variables from being collected by the garbage collection mechanism while the closure is alive. Therefore, improper use of closures can cause memory leaks and requires special attention. The following code:

function foo() {

   let value = 123

   function bar() { alert(value) }

   return bar

}

let bar = foo()

In this case, the variable value will be stored in memory if:

bar = null

In this case, as bar is no longer referenced, value is also cleared.

With the optimization of the browser engine, we changed the above code:

function foo() {

   let value = Math.random()

   function bar() {

       debugger

   }

   return bar

}

let bar = foo()

bar()

function foo() {

   let value = Math.random()

   function bar() {

       console.log(value)

       debugger

   }

   return bar

}

let bar = foo()

bar()

Let’s look at a real situation, with the help of Chrome Devtool, check and find memory leaks.

Code:

var array = []

function createNodes() {

   let div

   let i = 100

   let frag = document.createDocumentFragment()

   for (; i > 0; i--) {

       div = document.createElement("div")

       div.appendChild(document.createTextNode(i))

       frag.appendChild(div)

   }

   document.body.appendChild(frag)

}

function badCode() {

   array.push([...Array(100000).keys()])

   createNodes()

   setTimeout(badCode, 1000)

}



badCode()

We recursively call badCode. This function writes a new array of 100,000 items from 0 to 1 to the array each time. After the badCode function uses the full local variable array, it does not manually free memory. Memory leakage occurs. At the same time, the badCode function calls the createNodes function to create 100 div nodes every 1s.

In this case, open the Chrome DevTool, select the Performance TAB, and take a snapshot to get:

This shows that the JS Heap (blue line) and Nodes (green line) lines, which are rising over time, are not garbage collected. Therefore, a large risk of memory leakage can be determined. If we don’t know where the offending code is and how to pinpoint the risk points, we need to investigate every item in the JS heap, especially the first few with the largest size, in the Chrome Memory TAB. As shown in figure

It’s clearly our array that’s not right.

In this section we examine the basic concepts involved in closure knowledge and introduce mechanisms for memory management and garbage collection. In the next section, we’ll focus on code examples to strengthen our understanding

Column problem analysis

Practical questions a

const foo = (function() {
   var v = 0
   return () = > {
       return v++
   }
}())

for (let i = 0; i < 10; i++) {
   foo()
}

console.log(foo())
Copy the code

Analysis of the

Foo is an immediate function, and we try to print foo:

const foo = (function() {
   var v = 0
   return () = > {
       return v++
   }
}())

console.log(foo)
Copy the code

Output:

() => {    return v++ }

When the loop executes, foo() is referenced 10 times, v incremented 10 times, and finally foo is executed to get 10. Free variables are variables that are not declared in the scope of the related function, but are used.

Practical questions 2

const foo = () = > {
   var arr = []
   var i

   for (i = 0; i < 10; i++) {
       arr[i] = function () {
           console.log(i)
       }
   }

   return arr[0]
}

foo()()
Copy the code

Example 1: foo() returns arr[0], arr[0] is the function:

function () {    console.log(i) }

The variable I has a value of 10.

Practical topic three

var fn = null
const foo = () = > {
   var a = 2
   function innerFoo() {
       console.log(a)
   }
   fn = innerFoo    
}

const bar = () = > {
   fn()
}

foo()
bar()
Copy the code

Analysis: Normally, based on the knowledge of the call stack, after the foo function completes, its execution environment life cycle ends, the occupied memory is freed by the garbage collector, and the context disappears. But the innerFoo function assigns a value to fn, which is a global variable, resulting in foo’s variable object A being preserved as well. So when the function fn executes inside the function bar, it can still access the retained variable object, and the output is 2.

Actual combat is four

var fn = null
const foo = () = > {
   var a = 2
   function innerFoo() {
       console.log(c)            
       console.log(a)
   }
   fn = innerFoo
}

const bar = () = > {
   var c = 100
   fn()    
}

foo()
bar()
Copy the code

Execution result: An error is reported.

Analysis: When fn() is executed in bar, fn() is already copied as innerFoo, and the variable C is not in its scope chain; c is just an internal variable of the bar function. ReferenceError: C is not defined.

conclusion

What a qualified senior front-end engineer needs to do is not recite “closures and GC principles”, but based on the scenario, with a solid foundation, can improve application performance, analyze memory accidents and break bottlenecks by consulting data.

Reference: www.zhihu.com/market/paid…