LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

理解JavaScript闭包及其使用场景

admin
2024年11月5日 17:22 本文热度 366

闭包

闭包的官方定义是:一个表达式(通常是一个函数),它具有多个变量,并绑定到一个包含这些变量的环境。

在 JavaScript 中,闭包指的是函数即使在执行并离开其定义的词法作用域后,仍能够访问该作用域的能力。这是因为当函数被创建时,它会生成一个闭包,其中包含对当前函数定义环境的引用,从而使函数能够继续访问该环境中的变量。

以下是一个闭包的示例:

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变量,从而实现了计数器的功能。

闭包具有以下特点:

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

由于闭包的特殊性质,它们在 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函数的变量和作用域。

闭包的使用场景

实现私有变量、方法和模块化

// 模块化计数器
const counterModule = (function({
  let count = 0// 私有变量

  function increment(// 私有方法
    count++;
    console.log(`counter value: ${count}`);
  }

  function reset(// 私有方法
    count = 0;
    console.log('counter is reset');
  }

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

// 使用模块
counterModule.increment(); // counter value: 1
counterModule.increment(); // counter value: 2
counterModule.reset(); // counter is reset

在上述代码中,我们使用立即调用函数表达式(IIFE)返回一个包含两个公共方法incrementreset的对象,这些方法可以从外部访问。count变量以及incrementreset方法是私有的。这样,我们可以以模块化的方式组织代码,避免全局变量污染,同时保护私有变量和方法免受外部干扰。

实现函数记忆化

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(`calculating ${n} factorial`);
  if (n === 0) {
    return 1;
  }
  return n * factorial(n - 1);
}

const memoizedFactorial = memoize(factorial);

console.log(memoizedFactorial(5)); // calculating 5 factorial 120
console.log(memoizedFactorial(5)); // 120 
console.log(memoizedFactorial(3)); // calculating 3 factorial 6
console.log(memoizedFactorial(3)); // 6

在上述代码中,我们定义了一个memoize函数,它接受一个函数fn作为参数,并返回一个新函数,该新函数缓存fn的计算结果以避免重复计算。具体来说,我们使用一个名为cache的对象来存储计算结果,并返回一个在外部memoize函数中引用cache对象和fn函数的闭包。在闭包中,我们使用JSON.stringify将输入参数转换为字符串作为缓存键,然后检查结果是否已在缓存中。如果是,则直接返回;否则,计算结果并将其保存到缓存中。最后,我们可以使用memoize函数包装任何需要记忆化的函数以避免重复计算。在上述代码中,我们使用memoize函数包装了一个计算阶乘的函数factorial,并将其命名为memoizedFactorial。我们可以看到,第一次计算一个数的阶乘时,它会输出正在计算的消息。然而,当我们再次计算相同的数时,它不会输出消息,因为结果已经被缓存。

避免循环中的作用域问题

此技术还可用于避免循环中的作用域问题。当我们在循环中定义一个函数时,它可能会引用循环变量,但由于 JavaScript 的作用域规则,当函数被调用时,循环变量的值可能不是我们期望的。通过使用闭包为循环的每次迭代创建一个新的作用域,我们可以避免这个问题。

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函数,它返回一个包含五个函数的数组。这些函数的目的是返回它们在数组中的索引。我们使用闭包来避免循环中的作用域问题。具体来说,我们在循环中定义一个立即调用的匿名函数,它接受一个参数num并返回一个新函数,该新函数始终返回num。然后,我们立即调用这个匿名函数,将i作为参数传递,并将返回的函数保存到数组result的相应位置。由于匿名函数返回一个新函数,并且这个新函数引用外部函数createFunctions中的变量num,每个函数都会记录其在数组中的索引。因此,当我们调用这些函数时,它们将返回它们在数组中的索引,而不是循环变量i的值。最后,我们使用createFunctions函数创建一个包含五个返回自身索引的函数的数组,并分别调用这些函数,输出它们的返回值。

在异步编程中保存状态

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 秒后输出当前的计数值。请注意,在此过程中我们没有显式传递任何参数,而是使用闭包来维护计数状态,从而避免了在异步编程中手动传递状态的麻烦。

实现函数柯里化

实现以下代码的柯里化函数。

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

const curriedSum = curry(sum);

curriedSum(123); // 6
curriedSum(1)(23); // 6
curriedSum(12)(3); // 6
curriedSum(1)(2)(3); // 6

实现高阶函数

计算执行时间的高阶函数

function timingDecorator(fn{
  return function({
    console.time("timing");
    const result = fn.apply(thisarguments);
    console.timeEnd("timing");
    return result;
  };
}

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

const timingAdd = timingDecorator(add);
console.log(timingAdd(12));

缓存返回结果的高阶函数

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 < 2return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

const memoizeFibonacci = memoizeDecorator(fibonacci);
console.log(memoizeFibonacci(10));

实现延迟执行函数

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");

实现生成器

function makeGenerator(array{
  let index = 0;
  return function({
    if (index < array.length) {
      return { value: array[index++], donefalse };
    } else {
      return { donetrue };
    }
  };
}

const generator = makeGenerator([123]);

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

在这个示例中,我们定义了一个名为makeGenerator的函数,它接受一个数组作为参数并返回一个新函数。这个新函数使用闭包在内部保存数组索引,并根据索引依次返回数组中的每个元素,直到所有元素都被返回。我们将数组[1, 2, 3]传递给makeGenerator函数,并将返回的函数赋值给变量generator。然后,我们调用generator函数逐个检索数组中的元素并将它们输出到控制台。这样,我们可以轻松地使用闭包实现生成器,并以惰性方式逐个生成值,避免了一次性计算所有值所带来的性能和内存消耗问题。同时,使用闭包可以维护函数的状态和作用域,避免全局变量污染和变量冲突等问题。

实现事件监听器

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函数手动移除事件监听器,以停止响应按钮点击事件。这样,我们可以使用闭包轻松实现事件监听器,并灵活控制其生命周期,避免内存泄漏和性能问题。使用闭包还允许我们维护函数的状态和作用域,避免全局变量污染和变量冲突。


该文章在 2024/11/6 10:33:26 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved