Introduction to JavaScript Closure Application

Source: https://wintc.top/article/33

This article introduces an important concept in JS - closure. In fact, even the most junior front-end developers should have been exposed to it.

First, the concept and characteristics of closures

First look at an example of a closure:

function makeFab () {
  let last = 1, current = 1
  return function inner() {
    [current, last] = [current + last, current]
    return last
  }
}

let fab = makeFab()
console.log(fab()) // 1
console.log(fab()) // 2
console.log(fab()) // 3
console.log(fab()) // 5

Here is an example of generating the Fibonacci sequence. The return value of makeFab is a closure. makeFab is like a factory function. Each call will create a closure function, such as fab in the example. Each time fab is called, it does not need to pass parameters, and it will return different values, because when the closure is generated, it remembers the variables last and current, so that it can return different values ​​in subsequent calls. Can remember the variables in the scope of the function itself, which is the difference between closures and ordinary functions.

MDN The definition of a closure given in is that a function together with a reference to its state, the lexical environment, constitutes a closure. The "reference of the lexical environment" here can be simply understood as "referencing some variables outside the function". For example, in the above example, each time makeFab is called, the inner function will be created and returned, and the last and current variables are referenced.

 

2. Closures - the soul of functional programming

Both Javascript and python, two dynamic languages, emphasize one concept: everything is an object. Naturally, functions are also objects.

In Javascript, we can throw functions around in our code just like normal variables, and then call them at some point. This is called functional programming. Functional programming is flexible and concise, and the language's support for closures gives functional programming its soul.

Take implementing a reusable confirmation box as an example. For example, when the user performs some deletion or important operations, in order to prevent misoperation, we may ask the user to confirm the operation again through a pop-up window. Because the confirmation box is universal, the logic of the confirmation box component should be abstract enough. It is only responsible for pop-up windows, triggering confirmation, and triggering cancellation events. Triggering confirmation/cancellation events is an asynchronous operation. At this time, we need to use two callback functions. After completing the operation, the popup function confirm receives three parameters: a prompt statement, a confirmation callback function, and a cancellation callback function:

function confirm (confirmText, confirmCallback, cancelCallback) {
  // Insert prompt box DOM, including prompt statement, confirm button, cancel button
  // Add confirm button click event, do dom cleanup in event function and call confirmCallback
  // Add the cancel button click event, do dom cleanup in the event function and call cancelCallback
}

In this way, we can pass a callback function to confirm and complete different actions according to different results. For example, we can delete a piece of data according to the id and write:

function removeItem (id) {
  confirm('Confirm delete?', () => {
    // The user clicks OK to send a remote ajax request
    api.removeItem(id).then(xxx)
  }, () => {
    // User clicks Cancel,
    console.log('undelete')
  })
}

In this example, confirmCallback uses closures to create a function that references the id variable in the context. Such examples abound in callback functions, and most of the time the referenced variables are many. Think about it, what would happen to these variables if the language didn't support closures? All passed as parameters to the confirm function, and then passed to them as parameters when confirmCallback/cancelCallback is called? Obviously, closures provide great convenience here.

 

Three, some examples of closures

1. Anti-shake, throttling function

A very common requirement on the front end is remote search, which automatically sends an ajax request according to the content of the user input box, and then requests the search results back from the back end. In order to simplify the user's operation, sometimes we don't place a button to click to trigger the search event, but directly monitor the change of the content to search (such as the search bar of vue's official website). At this time, in order to avoid too frequent requests, we may use the "anti-shake" technique, that is, when the user stops inputting for a period of time (such as 500ms), the sending request is executed. You can write a simple anti-shake function to achieve this function:

function debounce (func, time) {
  let timer = 0
  return function (...args) {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      timer = 0
      func.apply(this, args)
    }, time)
  }
}

input.onkeypress = debounce(function () {
  console.log(input.value) // event handling logic
}, 500)

Each time the debounce function is called, a new closure function is created, which retains a reference to the event logic processing function func, the debounce interval time, and the timer flag timer. Similar to the throttling function:

function throttle(func, time) {
  let timer = 0 // The timer flag is equivalent to a lock flag
  return function (...args) {
    if (timer) return
    func.apply(this, args)
    timer = setTimeout(() => timer = 0, time)
  }
}

2. Elegantly solve the problem of multiple consecutive button clicks

When the user clicks a form submit button, the front end will send an asynchronous request to the background, but the request has not been returned, and the anxious user clicks the button a few more times, resulting in additional requests. Sometimes sending multiple requests only consumes some server resources at most, while in other cases, the form submission itself will modify the data in the background, and then multiple submissions will lead to unintended consequences. Whether it is to reduce server resource consumption or avoid multiple changes to background data, it is necessary to add click restrictions to form submit buttons.

How to solve it? A common method is to mark, that is, declare a boolean variable lock in the scope where the response function is located. When the response function is called, the value of the lock is first judged. If it is true, it means that the last request has not been returned, and this click is invalid; If it is false, set the lock to true, then send the request, and then change the lock to false after the request ends.

Obviously, this lock will pollute the scope where the function is located. For example, in the Vue component, we may record this mark on the component attribute; and when there are multiple such buttons, we need different attributes to mark ( Think naming these properties is a headache!). Generating closures is accompanied by the creation of new function scopes, which can be used to solve this problem. Here is a simple example:  

let clickButton = (function () {
  let lock = false
  return function (postParams) {
    if (lock) return
    lock = true
    // send request using axios
    axios.post('urlxxx', postParams).then(
      // Form submitted successfully
    ).catch(error => {
      // form submission error
      console.log(error)
    }).finally(() => {
      // Unlock regardless of success or failure
      lock = false
    })
  }
})()

button.addEventListener('click', clickButton)

In this way, the lock variable will be in a separate scope. After a click request is issued, the next request must wait for the request to come back.

Of course, in order to avoid declaring locks and modifying locks in various places, we can abstract the above logic and implement a decorator, just like the throttling/anti-shake function. The following is a generic decorator function:

function singleClick(func, manuDone = false) {
  let lock = false
  return function (...args) {
    if (lock) return
    lock = true
    let done = () => lock = false
    if (manuDone) return func.call(this, ...args, done)
    let promise = func.call(this, ...args)
    promise ? promise.finally(done) : done()
    return promise
  }
}

By default, the original function needs to return a promise to reset the lock to false after reaching the promise resolution, and if there is no return value, the lock will be reset immediately (for example, if the form validation fails, the response function returns directly). Example of calling :

let clickButton = singleClick(function (postParams) {
  if (!checkForm()) return
  return axios.post('urlxxx', postParams).then(
    // Form submitted successfully
  ).catch(error => {
    // form submission error
    console.log(error)
  })
})
button.addEventListener('click', clickButton)

In some places where it is inconvenient to return a promise or perform other actions at the end of the request before resetting the lock, singleClick provides the second parameter manuDone, which allows you to manually call a done function to reset the lock. This done function will be placed in the original The end of the function parameter list. Example of use:

let print = singleClick(function (i, done) {
  console.log('print is called', i)
  setTimeout(done, 2000)
}, true)

function test () {
  for (let i = 0; i < 10; i++) {
    setTimeout(() => {
      print(i)
    }, i * 1000)
  }
}

The print function is decorated with singleClick, and the lock variable is reset after each call for 2 seconds. The test calls the print function every second. The output of the execution code is as follows:

As you can see, some of these calls do not print results, which is exactly what we want! The singleClick decorator is much more convenient than setting the lock variable each time. Here, the return value of the singleClick function, as well as the done function in it, is a closure.

3. Closures simulate private methods or variables

"Encapsulation" is one of the characteristics of object-oriented. The so-called "encapsulation" means that an object hides the implementation details of some of its internal properties or methods, and the outside world can only operate the object through the exposed interface. JS is a relatively "free" language, so it does not provide the definition of private variables or member functions like C++ language, but using closures can simulate this feature well.

For example, in game development, the player object usually has an experience attribute, assuming it is exp, "killing monsters", "doing tasks", "using experience books", etc. will increase the value of exp, and it will be reduced when upgrading. The value of exp, it is obviously bad to directly expose exp to various businesses to operate. In JS, we can hide it with closures, and the simple simulation is as follows:

function makePlayer () {
  let exp = 0 // Experience
  return {
    getExp () {
      return exp
    },
    changeExp (delta, sReason = '') {
      // log(xxx), record the change log
      exp += delta
    }
  }
}

let p = makePlayer()
console.log(p.getExp()) // 0
p.changeExp(2000)
console.log(p.getExp()) // 2000

In this way, when we call makePlayer(), a player object p will be generated. The variable exp is manipulated by methods in p, but it cannot be accessed through p.exp, which is obviously more in line with the characteristics of "encapsulation".

4. Summary

Closures are one of the powerful features in JS, but as for how to use closures, I don’t think it’s a problem, and we don’t even need to study how to use them. My point is that closures should appear naturally in your code because they are the most straightforward solution to the problem at hand; when you use them deliberately, you may have taken a detour.

Posted by rgermain on Mon, 02 May 2022 08:22:02 +0300