likes
comments
collection
share

js闭包的理解

作者站长头像
站长
· 阅读数 58

闭包

官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

在JavaScript中,闭包(Closure)是指函数能够访问其定义时所在的词法作用域的能力,即使在函数在定义后,仍然可以访问该词法作用域中的变量。这种能力是由于函数在创建时会生成一个闭包,其中包含了当前函数的定义环境的一份引用,因此函数内部可以继续访问这些变量。

以下是一个闭包的例子:

function makeCounter() {
  var count = 0;
  return function() {
    return ++count;
  };
}

var counter = makeCounter();

console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3

在这个例子中,makeCounter函数返回了一个匿名函数,并在makeCounter函数的作用域中定义了一个count变量。当执行makeCounter函数时,将返回该匿名函数,并将其赋值给变量counter。在每次调用counter函数时,都会访问makeCounter函数作用域中的count变量,并将其递增。由于该匿名函数在创建时会生成一个闭包,因此即使makeCounter函数已经执行完毕,counter函数仍然可以访问makeCounter函数作用域中的count变量,从而实现了计数器的功能。

闭包具有以下特性:

  1. 闭包可以访问外部函数作用域中的变量,即使外部函数已经返回。
  2. 闭包会持有外部函数作用域的引用,因此可能导致内存泄漏问题。
  3. 闭包可以在多个函数间共享状态,但需要注意避免意外修改该状态。

由于闭包的特殊性质,它在JavaScript中被广泛应用,例如实现模块化、封装私有变量等。但是,由于闭包可能导致内存泄漏等问题,因此在使用时需要谨慎处理。

闭包的实现原理

闭包的实现原理是基于 JavaScript 的函数作用域和作用域链机制。当一个函数被定义时,它会创建一个新的作用域,并将当前的变量环境保存在该作用域中。当函数执行时,它会创建一个新的执行环境,并将当前的作用域链保存在该执行环境中。当函数执行完成后,它会将执行环境和作用域链一同销毁,但是作用域中的变量仍然被保存在内存中。

当一个函数返回一个内部函数时,这个内部函数仍然可以访问外部函数的作用域和变量,因为它的作用域链中包含了外部函数的作用域链。这样就形成了一个闭包,内部函数可以访问外部函数的变量,并且这些变量不会被销毁,直到内部函数被销毁。

下面是一个简单的闭包例子,可以帮助理解闭包的实现原理:

function outer() {
  let x = 10;
  return function inner() {
    console.log(x);
  };
}

const innerFn = outer();
innerFn(); // 输出 10

在这个例子中,outer 函数返回了一个内部函数 inner,该函数可以访问 outer 函数中的变量 x。当 outer 函数执行完毕后,变量 x 仍然被保存在内存中,因为 inner 函数形成了一个闭包,可以访问 outer 函数中的变量和作用域。

闭包的应用

  1. 实现私有变量和方法
  2. 实现模块化
// 模块化代码,可以实现一个计数器
const counterModule = (function() {
  let count = 0; // 私有变量

  function increment() { // 私有方法
    count++;
    console.log(`计数器值为: ${count}`);
  }

  function reset() { // 私有方法
    count = 0;
    console.log('计数器已重置');
  }

  return { // 暴露公共方法
    increment,
    reset
  }
})();

// 使用模块
counterModule.increment(); // 计数器值为: 1
counterModule.increment(); // 计数器值为: 2
counterModule.reset(); // 计数器已重置

在上面的代码中,我们使用了一个立即执行函数(IIFE),它返回一个对象,其中包含了两个公共方法 increment() 和 reset(),这些公共方法可以在外部访问,而 count 变量和 increment()、reset() 方法则是私有的。这样,我们就可以通过模块的方式组织我们的代码,避免了全局变量的污染,同时保护了私有变量和方法,使其不受外部干扰。

  1. 实现函数记忆
function memoize(fn) {
  const cache = {}; // 缓存计算结果的对象

  return function(...args) {
    const key = JSON.stringify(args); // 将参数转化为字符串作为缓存的键

    if (cache[key] === undefined) { // 如果缓存中没有此结果,则计算并保存
      cache[key] = fn.apply(this, args);
    }

    return cache[key]; // 返回缓存的计算结果
  };
}

function factorial(n) {
  console.log(`正在计算 ${n} 的阶乘`);
  if (n === 0) {
    return 1;
  }
  return n * factorial(n - 1);
}

// 使用记忆函数
const memoizedFactorial = memoize(factorial);

console.log(memoizedFactorial(5)); // 正在计算 5 的阶乘 120
console.log(memoizedFactorial(5)); // 120 (缓存中已经有此结果,不需要重复计算)
console.log(memoizedFactorial(3)); // 正在计算 3 的阶乘 6
console.log(memoizedFactorial(3)); // 6 (缓存中已经有此结果,不需要重复计算)

在上面的代码中,我们定义了一个 memoize() 函数,该函数接收一个函数 fn 作为参数,返回一个新的函数,该新函数会将 fn 的计算结果缓存起来,以避免重复计算。具体来说,我们使用了一个对象 cache 来缓存计算结果,并返回了一个闭包,这个闭包中引用了外层函数 memoize() 中的 cache 对象和 fn 函数。在闭包中,我们使用 JSON.stringify() 将传入的参数转化为字符串作为缓存的键,然后检查缓存中是否已经有这个结果,如果有就直接返回,否则计算结果并保存到缓存中。最后,我们可以使用 memoize() 函数包装任何需要记忆的函数,从而避免重复计算。在上面的代码中,我们使用 memoize() 函数包装了一个计算阶乘的函数 factorial(),并称其为 memoizedFactorial。我们可以看到,第一次计算某个数的阶乘时,会输出一条正在计算的消息,而之后再次计算时,就不会输出此消息了,因为结果已经被缓存了。

  1. 避免循环中的作用域问题
function createFunctions() {
  const result = [];

  for (var i = 0; i < 5; i++) {
    result[i] = function(num) {
      return function() {
        return num;
      };
    }(i);
  }

  return result;
}

const funcs = createFunctions();
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
console.log(funcs[3]()); // 3
console.log(funcs[4]()); // 4

在上面的代码中,我们定义了一个函数 createFunctions(),该函数返回一个数组,其中包含了 5 个函数。这些函数的作用是返回它们在数组中的索引。我们使用了一个闭包来避免循环中的作用域问题。具体来说,我们在循环中定义了一个立即执行的匿名函数,该函数接收一个参数 num,返回一个新的函数,该新函数总是返回 num。然后,我们立即调用这个匿名函数,并传入 i 作为参数,将返回的函数保存到数组 result 中的对应位置。由于匿名函数返回的是一个新的函数,而这个新函数中引用了外层函数 createFunctions() 中的变量 num,因此每个函数都会记录它们在数组中的索引。这样,当我们调用这些函数时,它们会返回它们在数组中的索引,而不是循环变量 i 的值。最终,我们使用 createFunctions() 函数创建了一个包含 5 个返回自身索引的函数的数组,并分别调用这些函数,输出了它们的返回值。

  1. 在异步编程中保存状态
function createIncrementer() {
  let count = 0;

  function increment() {
    count++;
    console.log(`Count: ${count}`);
  }

  return {
    incrementAsync() {
      setTimeout(() => {
        increment();
      }, 1000);
    }
  };
}

const incrementer = createIncrementer();

incrementer.incrementAsync(); // Count: 1
incrementer.incrementAsync(); // Count: 2
incrementer.incrementAsync(); // Count: 3

在上面的代码中,我们定义了一个函数 createIncrementer(),该函数返回一个包含一个方法 incrementAsync() 的对象。这个方法会在 1 秒钟后调用一个内部的 increment() 函数。这个 increment() 函数通过闭包访问了外层函数 createIncrementer() 中定义的变量 count,因此它可以在多次调用 incrementAsync() 方法之间持续地记录计数。我们创建了一个 incrementer 对象,并多次调用它的 incrementAsync() 方法,每次调用后它会在 1 秒钟后输出当前的计数值。注意,在这个过程中,我们并没有显式地传递任何参数,而是通过闭包来保持计数状态,从而避免了在异步编程中需要手动传递状态的麻烦。

  1. 实现函数柯里化

题目:写一个curry化函数

function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);

curriedSum(1, 2, 3); // 6
curriedSum(1)(2, 3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2)(3); // 6

答案:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);

console.log(curriedSum(1, 2, 3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2)(3)); // 6
  1. 实现高阶函数
  • 计算函数执行时间的高阶函数
function timingDecorator(fn) {
  return function() {
    console.time("timing");
    const result = fn.apply(this, arguments);
    console.timeEnd("timing");
    return result;
  };
}

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

const timingAdd = timingDecorator(add);
console.log(timingAdd(1, 2)); // 输出结果为3,并在控制台打印执行时间
  • 缓存函数返回结果的高阶函数
function memoizeDecorator(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const fibonacci = function(n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

const memoizeFibonacci = memoizeDecorator(fibonacci);
console.log(memoizeFibonacci(10)); // 输出结果为55
  • 将函数柯里化的高阶函数

上述科里化例子

  1. 实现延迟执行函数
function delayDecorator(fn, delay) {
  return function() {
    const args = arguments;
    setTimeout(function() {
      fn.apply(this, args);
    }, delay);
  };
}

const sayHello = function(name) {
  console.log(`Hello, ${name}!`);
};

const delayedHello = delayDecorator(sayHello, 1000);
delayedHello("John"); // 1秒后输出 "Hello, John!"
  1. 实现生成器
function makeGenerator(array) {
  let index = 0;
  return function() {
    if (index < array.length) {
      return { value: array[index++], done: false };
    } else {
      return { done: true };
    }
  };
}

const generator = makeGenerator([1, 2, 3]);

let result = generator();
while (!result.done) {
  console.log(result.value);
  result = generator();
}

在这个例子中,我们定义了一个 makeGenerator 函数,该函数接受一个数组作为参数,并返回一个新的函数。这个新函数利用了闭包,将数组索引保存在内部,并根据索引依次返回数组中的元素,直到返回所有元素为止。

我们将数组 [1, 2, 3] 传给 makeGenerator 函数,并将返回的函数赋值给 generator。然后,我们通过调用 generator 函数来逐个获取数组中的元素,并将它们输出到控制台。

通过这种方式,我们可以方便地利用闭包实现生成器,并以惰性计算的方式逐个生成值,从而避免一次性计算所有值带来的性能问题和内存占用问题。同时,利用闭包可以保持函数的状态和作用域,避免全局变量污染和变量冲突等问题。

  1. 实现事件监听器
function createEventListener(element, eventName, handler) {
  element.addEventListener(eventName, handler);
  return function() {
    element.removeEventListener(eventName, handler);
  };
}

const button = document.getElementById("myButton");
const onClick = function() {
  console.log("Button clicked!");
};

const removeEventListener = createEventListener(button, "click", onClick);

// 在一段时间后,手动移除事件监听器
setTimeout(function() {
  removeEventListener();
}, 5000);

在这个例子中,我们定义了一个 createEventListener 函数,该函数接受一个 DOM 元素、一个事件名和一个事件处理函数作为参数,并返回一个新的函数。这个新函数利用了闭包,将 DOM 元素、事件名和事件处理函数保存在内部,并在执行时将事件监听器添加到 DOM 元素上。

我们将一个按钮元素、一个点击事件处理函数和事件名 click 传给 createEventListener 函数,并将返回的函数赋值给 removeEventListener。然后,我们通过调用 removeEventListener 函数来手动移除事件监听器,从而在一段时间后停止响应按钮点击事件。

通过这种方式,我们可以方便地利用闭包实现事件监听器,并以灵活的方式控制事件监听器的生命周期,从而避免内存泄漏和性能问题。同时,利用闭包可以保持函数的状态和作用域,避免全局变量污染和变量冲突等问题。

推荐文章

  1. 要深入闭包原理看冴羽大佬的JavaScript深入之闭包(非常推荐)

  2. 学react的小伙伴可以看看 说说你对React Hook的闭包陷阱的理解,有哪些解决方案?

转载自:https://juejin.cn/post/7206696994063908925
评论
请登录