Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

闭包 #4

Open
kangkang123269 opened this issue Feb 26, 2023 · 0 comments
Open

闭包 #4

kangkang123269 opened this issue Feb 26, 2023 · 0 comments
Labels
JavaScript JavaScript基础

Comments

@kangkang123269
Copy link
Owner

kangkang123269 commented Feb 26, 2023

闭包

在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深入之闭包(非常推荐)

image

  1. 学react的小伙伴可以看看 说说你对React Hook的闭包陷阱的理解,有哪些解决方案?
@kangkang123269 kangkang123269 added the JavaScript JavaScript基础 label Sep 7, 2023
@kangkang123269 kangkang123269 changed the title js4. 闭包 闭包 Sep 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
JavaScript JavaScript基础
Projects
None yet
Development

No branches or pull requests

1 participant