An introduction to JavaScript scopes and closures.

scope

Scope is a mechanism that binds a name to an entity and allows access to the entity through the name. Following the specified syntax, we can create scopes on which we define names and their corresponding entities (such as variables, functions, etc.). The code in this scope can access the corresponding entities with the defined names.

The scope js uses is lexical scope, also known as static scope. When a variable is searched, it is searched in the current scope. If it is not found, it is searched one level up until the highest global scope is found or reached.

var a = 'A'

function printABC() {
  var b = 'B'
  function print(c) {
    console.log(a, b, c)
  }
  print('C')
}

printABC()
// A B C
Copy the code

Inside print, you can access the parameter C of print’s scope, the variable B defined in printABC’s scope, and a of the global scope. A nested scope of printC scope -> printABC function scope -> global scope is formed. The code in printC, if it wants to use a variable, looks for that variable from the inside out.

Lexical scope is determined at declaration time

Lexical scope is determined at declaration time, not at execution time. Let’s look at the following js code:

var a = 'outer'
function printA() {
  var a = 'inner'
  realPrintA()
}

function realPrintA() {
  console.log(a)
}

printA()
Copy the code

If you don’t know anything about the binding behavior of your scope, you might intuitively assume that the output is inner. But in fact, the correct answer is outer. This result verifies that the scope of JS is syntactic, that is, the scope is determined at the time of function definition and is independent of execution. RealPrintA functions are defined in a global scope, so code in a printA function can access only two scopes: the realPrintA function scope and the global scope. The a identifier in the scope of printA cannot be accessed by it. So we end up with the string value outer bound to the global scope for the A identifier.

The opposite of lexical scope is static scope, which has only one scope and changes as the program executes. The Bash scripting language we commonly use uses static scope.

#! /bin/bash

a="outer"
function printA() {
  a="inner" # or local a = "inner"
  realPrintA
}

function realPrintA() {
  echo $a
}

printA
Copy the code

Copy this code to a file, then type the bash file name and press Enter on a terminal on a Unix-like system to get the result. Unlike js, the output is inner. Bash maintains only one scope, and executing a=”inner” actually modifies global A, not creating a new scope. It’s worth noting that Bash can declare local variables in functions with local, but it still doesn’t create a new scope, so you can just temporarily replace global variables and restore them to their original values when the function is finished executing.

ascension

Promotion is a cliche, so here’s a rundown.

When executing code, variable declarations and function declarations in the code take precedence. Because the effect is equivalent to lifting the declaration to the top of the scope, it is called lifting.

Executing a piece of JS code is divided into two steps: compiler compilation and JS engine interpretation. One job at compile time is to scan the code to find all declarations and add them to the current scope, which is the root cause of promotion.

console.log(b)
var b = 2
printA()

function printA() {
  console.log('a')}// The output is:
// undefined
// a
Copy the code

For promotion reasons, this code is equivalent to:

function printA() {
  console.log('a')}var b // The value is undefined

console.log(b)
b = 2
printA()
Copy the code

Why variable promotion?

Function promotion is understandable, but why does variable promotion also occur? Js author Brendan Eich tweeted frankly about the reasons for the variable increase:

  • The function is going to be promoted
  • The block scoped feature is not supported
  • Js development time is very short (10 days)

The variable boost feature was not what the authors wanted, but a compromise that had to be made due to tight development time. Fortunately, ES6 introduced the let/const declaration keyword, which addresses some of the strange behavior caused by var declarations of variable promotion.

The problem of var

Some unconventional behavior regarding VAR.

  • Even if a variable name already exists in the current scope, you can use the var to declare it again without an error (let/const can solve this problem).
var a = 1
var a = 2 // Write this without error
console.log(a) // Output: 2
Copy the code
  • If the assigned variable is not found in any nested scope, a global scope variable is declared (this is not a VAR problem, but a JS problem). Cannot be resolved for now, only use variables carefully)
function setA() {
  a = 1
}

setA()
console.log(a) // Output: 1
Copy the code
  • Unable to form block scope (causes scope contamination)
for (var i = 0; i < 3; i++) {
  console.log(i)
}

console.log('i:', i)

// Output result:
/ / 0
/ / 1
/ / 2
// i: 3
Copy the code

Var does not support block scope, causing I to contaminate the external scope. The solution is to use let/const, which can be block scoped. You can also use immediate function expressions.

Did let improve?

Let’s declaration has been improved, but unlike VAR, it has been added to TDZ (Temporal Dead Zone). If a variable is in TDZ, a ReferenceError is thrown if an attempt is made to read its value before it is initialized (that is, assigned for the first time). Some articles suggest that let is not improving because at first glance it does look like this, but this is wrong. Let’s look at the following code:

let x = 'outer';
(function() {
    console.log(x);
    let x = 'inner'; } ());Copy the code

If we assume that no variable promotion has occurred in the let, the x should be global before the local x variable is declared, and the final output of the program should be outer. But actually running this code throws a ReferenceError. The variable declared by the let is promoted to the top of the function scope when trying to get the value because the TDZ mechanism threw an error.

The same goes for const.

closure

Closures, a combination of functions and their associated environment bindings, are common in programming languages where functions are first citizens. Associated environments can be not only function scoped, but also global and block scoped, although function scoped is more common. You can say that when you declare a function, closures happen.

Js supports closures. In JS, the most common use is to get a function bound to the scope of an external function by returning a function in an external function. Even if the function is called outside of the current scope, you can retrieve the outer function scope bound to the closure.

function createCounter() {
  let count = 0
  function addAndPrint() {
    count++
    console.log(count)
  }
  return addAndPrint
}

const counter = createCounter()
counter() / / 1
counter() / / 2

const otherCounter = createCounter()
otherCounter() / / 1
Copy the code

As you can see, each time createCounter is executed, it gets a function bound to a scope containing the count variable, similar to creating an instance from a class in an object-oriented language.

Closures are often used in conjunction with immediate function expressions (IIFE) to create a unique function that contains a scope that only you can access. Closure to prevent multiple button clicks from sending repeated requests:

const fetchData = (function () {
  let loading = false
  return function() {

    if (loading == true) return
    loading = true
    
    api_method()
      .then(res= > {
        // The request was successfully processed
      })
      .catch(err= > {
        // The request failed to be processed
      })
      .finally(() = > {
        loading = false})}}) ()Copy the code

This prevents loading from being modified by other code errors by putting it in the fetchData function’s association scope.

The role of closures

The power of closures is that they bind functions to a private scope, and we can do a lot with this feature.

1. Create objects with private attributes (module mode)

The modular pattern is a pattern that encapsulates variables and functions in a scope. With closures, we can create objects with private variables, similar to the way classes in object-oriented languages write private attributes:

function createPoint() {
  let x = 0
  let y = 0
  return {
    printPos: function() {
      console.log({ x, y })
    },
    moveTo(tx, ty) {
      x = tx
      y = ty
    }
  }
}

const point = createPoint()
point.printPos() // { x: 0, y: 0 }
point.moveTo(3.4)
point.printPos() // { x: 3, y: 4 }
Copy the code

2. Implement the function Coriolization

An implementation of currization:

const curriedAdd = (function() {
  let addedNums = []
  function f() {
    if (arguments.length == 0) {
      const sum = addedNums.reduce((acc, cur) = > acc + cur, 0)
      console.log(sum)
      returnsum } addedNums.push(... arguments)return f
  }
  return f
})()

curriedAdd(1.5) (7) ()// output: 13
Copy the code

Some interview questions

1. The following code output is?

var result = [];
var a = 3;
var total = 0;
function foo(a) {
  var i = 0
  for (; i < 3; i++) {
    result[i] = function() {
      total += i * a;
      console.log(total);
    }
  }
}

foo(1);
result[0] (); result[1] (); result[2] ();Copy the code

Although a closure in its normal form returns a function inside a function, it can do so as long as the function inside is passed around. In this code, we create three functions that we pass into an array when executing foo(1), and note that all three refer to the same lexical environment. The local variables of this lexical environment are a=1, I =3. Execute three functions in sequence, total 3, 6, and 9. So the resulting output is:

3
6
9
Copy the code

1. What does the following code say?

This is a classic interview question.

for (var i = 0; i < 3; i++) {
  setTimeout(() = > {
    console.log(i)
  }, i * 1000)}Copy the code

This code prints a 3 every one second.

Var declarations do not support block scope. After executing the play loop, a global I is generated with the value I because the block scope does not exist. The callback passed in by the setTimeout method is declared to bind to a global scope. When the timer executes the callback at the specified time, it takes the value 3 of global scope I and prints it.

If we want to print 0,1,2. So how do you do that? One of the simplest ways is to use a block-scoped let:

for (let i = 0; i < 3; i++) {
  setTimeout(() = > {
    console.log(i)
  }, i * 1000)}Copy the code

Another approach is to use function scopes created by IIFE instead of block scopes that fail to function. We just want to create a scope that holds the value of I for each loop. Since block scopes cannot be reused, why not use function scopes?

for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() = > {
      console.log(j)
    }, j * 1000)
  })(i)
}
Copy the code

reference

  • JavaScript you Don’t Know (Volume 1)
  • Wikipedia – Closure
  • MDN – closure
  • medium-Module Pattern in JavaScript
  • Zhihu – What is the difference between dynamic scope and lexical scope
  • stackoverflow – Why does JavaScript hoist variables?
  • TEMPORAL DEAD ZONE (TDZ) DEMYSTIFIED
  • Curry and Function Composition