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.

  1. Global scope, with a single identifier: foo
  2. Scope created by foo with three identifiers: a, bar, and b
  3. 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:

  1. Scope look-ups start at the innermost scope of the runtime and work outward
  2. 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:

  1. Now I can’t find it in foo
  2. 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