The article backup address is here

Closures are one of the most difficult things to understand in javascript. There are a lot of articles about closures on the Internet, but there are few articles that make people understand them completely. The reason for this, I think, is that closures involve a chain of knowledge. Only this series of knowledge points are understood thoroughly, to achieve a concept of closed loop, can really understand it. Today I’m going to take a different look at closures, from the perspective of memory allocation and reclamation. Hopefully this will help you really digest what you’ve seen about closures, and hopefully this will be the last article you read about closures.

When you look at the pictures in this article, keep in mind the direction of the arrow. Since this is the principle by which the root object window traverses memory garbage, anything that can be found starting with window and following the arrow is not memory garbage and will not be reclaimed. Only those objects that cannot be found are memory garbage and will be collected by GC at the appropriate time.

Introduction of closure

When a function is nested, a closure is formed when the inner function refers to a variable in the scope of the outer function, and the inner function is referred to by a variable in the global environment.

Closures are essentially a byproduct of function scope.

One important point about closures is that all functions defined within a function share the same closure object. What does that mean? Look at this code:

var a
function b() {
  var c = new String('1')
  var d = new String('2')
  function e() {
    console.log(c)
  }
  function f() {
    console.log(d)
  }
  return f
}
a = b()Copy the code

In the above code, f refers to the variable d, and f is referred to by the external variable a, so it forms a closure, leaving the variable d in memory. Let’s think about it. What about the variable C? I don’t think we’re using C, so it’s not going to stay in memory. And then the fact that C is also stuck in memory. The closure formed by the code above contains two members, C and D. This phenomenon is known as closure sharing within a function.

Why is this feature so important? Because of this feature, it is easy to write code that causes memory leaks if we are not careful.

So much for the conceptual aspects of closures, but there are a few things you need to know to really understand them

  • Function scope chain
  • Execution context
  • Variable object, live object

You can Google it and get a sense of it. I’m going to talk about how to understand closures from the browser’s point of view, so I won’t go into too much detail.

How to identify memory garbage

The modern browser garbage collection process is more complex, you can Google the detailed process. Here I only talk about how to determine memory garbage. Basically, anything that starts at the root object and can be found by following a reference cannot be reclaimed. Objects that cannot be found following the reference are considered garbage and are collected at the next garbage collection node. Looking for garbage can be understood as a process of following the trail.

The memory representation of a closure

Starting with the simplest code, let’s look at global variable definitions.

var a = new String('little song')Copy the code

Such a piece of code is represented in memory as follows

In the global context, a variable a is defined and a string is assigned to it, with the arrow representing the reference.

Let’s define another function:

var a = new String('little song')
function teach() {
  var b = new String('dell')}Copy the code

The memory structure is as follows:

It all makes sense, but if you pay attention you’ll notice that the teach function object has a property called [[scopes]]. What is that? Why does this property exist after the function is created? I’m glad you asked, and it’s a key point in understanding closures.

Keep in mind that once a function is created, the javascript engine appends a property called a scope chain to the function object. This property points to an array object that contains the scope of the function and its parent scope, all the way to the global scope

Therefore, the above figure can be simply understood as: Teach function is created in the global environment, so the scope chain of teach has only one layer, that is the global scope global

To be clear, global points to the window object in the browser, and global points to the Global object in the nodeJS environment

Again, remember: Function at the time of execution, will apply for the space to create the execution context and the execution context contains the function definition of the scope chain, the second contains the function of the definition of internal variables, parameters, etc., when the function in the current scope executes, will first find under the current scope of variables, if you cannot find, to the function definition of the scope chain in search, until the global scope, If the variable is also not found in the global scope, an error is thrown.

As we all know, when a function is executed, it creates an execution context. In fact, it is applying for a stack of memory space. The local variables in the function are allocated in this space, and after the function is executed, the local variables are collected at the next garbage collection node. OK, let’s update the code again and take a look at the structure of the function’s runtime memory.

var a = new String('little song')
function teach() {
  var b = new String('dell')
}
teach()Copy the code

Memory is shown as follows:

Obviously, we can see that the function performs only a local variable assignment, not a global variable assignment, so we can’t find the variable b in the execution context by following the reference (arrow in the figure) from the window object. So after the function executes, the variable b will be reclaimed.

Let’s update the code again:

var a = new String('little song')
function teach() {
  var b = new String('dell')
  var say = function() {
    console.log(b)
  }
  a =  say
}
teach()Copy the code

Memory is shown as follows:

Note: Gray represents objects that cannot be traced from the root object.

Function execution order:

  1. Before the teach function starts executing, apply for stack space, as shown in the blue square above.
  2. Create the context scope (stack structure) and press the [[scopes]] from the teach function definition into the scope.
  3. Initialize variable b (variable promotion), create the function say, and initialize the say scopes properties by first pressing teach’s scopes into the [[scopes]] function say. Since say refers to the variable b, it forms a closure. So we’ll also push the closure object into the [[scopes]] function say.
  4. Create the variable object local, pointing to the local variables b and say, and press local into the scope in Step 2.
  5. Start function execution
    1. Assign the string object ‘gogu’ to the variable b.
    2. Point the global variable a to the function say.

When the function finishes executing, the variable b should normally be released. However, we find that we can find B by looking down the window. According to the principle of determining memory garbage we mentioned before, B is not memory garbage, so B cannot be freed. This is why the closure keeps the variables inside the function in memory.

Updating the code again, let’s look at the shared memory representation of closures:

var a = new String('0')
function b() {
  var c = new String('1')
  var d = new String('2')
  function e() {
    console.log(c)
  }
  function f() {
    console.log(d)
  }
  return f
}
a = b()Copy the code

The gray graph is memory garbage that will be collected by the garbage collector.

C is referenced by function e, so c is present in the closure. If you start looking for c in the window object, you can find c, so c cannot be released.

How, you may ask, can this feature cause a memory leak? Well, consider the following code that compares the classic Meteor memory leak problem.

        var t = null;
        var replaceThing = function() {
            var o = t
            var unused = function() {
                if (o)
                    console.log("hi")
            }
            t = {
                    longStr: new Array(1000000).join(The '*'),
                    someMethod: function() {
                      console.log(1)
                    }
                }
        }
        setInterval(replaceThing, 1000)Copy the code

This code has a memory leak. When you execute this code in the browser, you will notice that the memory is rising. Although the GC frees some memory, there is still some memory that cannot be freed, and it is gradient rising. The following figure

This curve indicates that there is a memory leak, and we can use developer tools to analyze which objects are not being reclaimed. In fact, I can tell you that the memory that’s not freed is just the big object T that we create every time. Let’s draw a picture of it:

The figure above assumes that the replaceThing function is executed three times. You will notice that each time we assign a large object to the variable t, the previous large object is still traceable from the window object due to shared closures, so these large objects cannot be reclaimed. What really works for us is that the last time we gave t a large object, the previous object caused a memory leak.

You can imagine how quickly the browser would crash if we didn’t realize this and let the program run.

The solution to this problem is very simple, each time after executing the code, the variable o is set to null, you can try ha ~

conclusion

At the end of this article, I suggest you take a look at some closure examples that you have encountered, and draw them in a way that I think you can easily understand. If not, you are welcome to communicate with me in private.