What is a closure and how to use it properly

A closure is a combination of a function and its surrounding state (lexical environment). This environment contains all the variables and parameters that are accessible at the time of function declaration and always exists during function execution.

In JavaScript, every time a function is created, a new lexical environment is created. This lexical environment contains all variables and parameters used in the function. If this function returns a function, the returned function will hold this lexical environment, that is, it will have access to all variables and parameters defined in the parent function. Such functions are called closures.

1. The characteristics of closure

Closures are the property of a function having access to the lexical scope in which it was defined. Specifically, closures have the following characteristics:

  1. Function-nested functions: Closures must have a structure of function-nested functions.
  2. Inner functions can access variables of outer functions: Inner functions can access variables in outer functions, even after the outer function finishes executing, these variables can still be accessed.
  3. Outer function returns inner function: An outer function must return an inner function in order to form a closure.

2. The use field of closure

Encapsulation variables: Using closures can realize the privatization of variables, thereby avoiding the pollution of global variables.

Variable encapsulation is achieved by taking the function and the variables defined inside the function as a whole to form a private scope. Specifically, closures can limit the scope of variables within the function, thereby avoiding the pollution of global variables and direct access to variables from outside.

The following is a simple example that demonstrates how to use closures to implement variable encapsulation

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

const counter = createCounter();
counter(); // 输出 1
counter(); // 输出 2
counter(); // 输出 3

In the code above, createCounter the function returns an inner function that has access to  the variables createCounter defined in  the function count . Since  count the variable is defined  createCounter inside the function, the variable cannot be accessed directly from the outside, thus realizing the encapsulation of the variable.

通过调用 createCounter 函数返回的内部函数,可以实现计数器的功能,每次调用时计数器加 1,最后输出计数器的值。由于 count 变量被定义在 createCounter 函数内部,并且该函数返回的是一个闭包,因此 count 变量的值可以被保存在内存中,并且在多次调用时能够保持不变。

实现模块化:通过闭包可以将代码模块化,从而避免变量名冲突等问题。

使用闭包,可以将模块内部的状态和方法封装起来,从而实现模块化的设计。下面是一个简单的示例,演示了如何使用闭包实现模块化编程:

const module = (function () {
  let count = 0;

  function increment() {
    count++;
    console.log(count);
  }

  function decrement() {
    count--;
    console.log(count);
  }

  return {
    increment,
    decrement,
  };
})();

module.increment(); // 输出 1
module.increment(); // 输出 2
module.decrement(); // 输出 1

在上面的代码中,定义了一个自执行函数,该函数返回了一个对象,该对象包含两个方法 increment 和 decrement。这两个方法都可以访问函数内部的变量 count,从而实现了模块内部的状态封装。

在执行该函数时,会创建一个闭包,该闭包可以访问自执行函数内部的变量和方法。由于闭包的特性,外部无法访问自执行函数内部的变量和方法,从而实现了模块化的设计。

实现柯里化:使用闭包可以实现柯里化,从而方便地进行函数组合。

柯里化(Currying)是一种函数式编程技术,它允许我们把接受多个参数的函数转化为一系列只接受一个参数的函数,且这些函数都返回一个新函数,新函数可以接受下一个参数,直到所有参数被接受完毕。

举个例子,假设我们有一个加法函数 add,它接受两个参数并返回它们的和:

function add(x, y) {
  return x + y;
}

add(2, 3); // 输出 5

通过柯里化,我们可以把这个函数转化为一个只接受一个参数的函数:

function add(x) {
  return function (y) {
    return x + y;
  };
}

add(2)(3); // 输出 5

在这个例子中,我们将 add 函数转化为一个返回一个函数的函数。返回的函数可以接受第二个参数 y,并返回它们的和。

使用闭包实现柯里化非常方便。我们可以使用一个函数返回另一个函数,并在返回的函数中使用闭包来保留外部函数的参数。下面是一个简单的示例:

function add(x) {
  return function (y) {
    return x + y;
  };
}

const add2 = add(2);
console.log(add2(3)); // 输出 5

在上面的示例中,add 函数返回一个新函数,该函数接受一个参数 y 并返回它们的和。我们可以将 add 函数应用于第一个参数 2,从而创建一个新的函数 add2add2 函数只需要一个参数 y,它会将 2 和 y 相加并返回结果。

使用闭包实现柯里化的关键在于,返回的函数可以访问外部函数的变量 x,从而将其保留下来。每次调用返回的函数时,它都会使用之前的参数 x,并接受一个新的参数 y,从而实现柯里化的效果。

实现异步编程:使用闭包可以实现异步编程,从而避免回调地狱的问题。

因为它们可以帮助我们处理回调函数的嵌套和异步代码的执行顺序。

下面是一个使用闭包实现异步编程的示例。假设我们有一个 getData 函数,它会从远程服务器获取数据。由于网络请求是异步的,我们需要使用回调函数来处理获取到数据的情况。我们可以使用闭包来避免回调函数的嵌套,从而提高代码的可读性。

function getData(url, onSuccess, onError) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() {
    if (xhr.status === 200) {
      onSuccess(xhr.response);
    } else {
      onError(xhr.statusText);
    }
  };
  xhr.onerror = function() {
    onError(xhr.statusText);
  };
  xhr.send();
}

function processData(data) {
  // 处理数据
}

function handleGet() {
  getData('/api/data', function(response) {
    processData(response);
  }, function(error) {
    console.error('获取数据失败:', error);
  });
}

在上面的示例中,getData 函数接受三个参数:一个 URL,一个成功回调函数 onSuccess 和一个失败回调函数 onError。它会使用 XMLHttpRequest 对象来获取数据,并在数据加载完成后调用适当的回调函数。

handleGet 函数是一个事件处理程序,它会调用 getData 函数并提供两个回调函数。由于异步编程的特性,getData 函数会立即返回,而不会等待数据加载完成。因此,我们需要使用闭包来保留对回调函数的引用,并在数据加载完成后调用它们。这样,我们就可以在代码中使用普通的函数调用,而不必担心回调函数的嵌套。

闭包可以将回调函数与其相关的数据进行捆绑,并在适当的时候调用回调函数。这使得异步编程变得更加直观和易于管理,从而使我们的代码更加清晰和可读。

三、闭包的运用方式

闭包可以使用以下方式来运用:

  1. 将内部函数作为返回值:将内部函数作为返回值,即可创建闭包。
  2. 将内部函数作为参数传递给其他函数:将内部函数作为参数传递给其他函数,即可在其他函数中创建闭包。
  3. 使用 IIFE(立即执行函数表达式):使用 IIFE 可以立即执行函数,并且将内部函数作为返回值,从而创建闭包。

四、闭包的注意事项

闭包虽然有很多优点,但是也需要注意以下几点:

  1. 内存泄漏:闭包会一直持有外部作用域中的变量和参数,如果不及时释放,就会导致内存泄漏。
  2. 性能问题:由于闭包会持有外部作用域中的变量和参数,因此会占用更多的内存和 CPU 资源。
  3. 作用域链问题:由于闭包可以访问外部作用域中的变量和参数,因此会导致作用域链的变长,从而影响代码的执行效率。

Guess you like

Origin juejin.im/post/7216650471747321893