javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript函数返回函数

JavaScript闭包实现函数返回函数详解

作者:友人.227

在JavaScript中,闭包是一个非常强大的特性,它允许一个函数访问并操作函数外部的变量,闭包通常通过返回一个内部函数来实现,这使得外部函数可以保持某些变量的状态,即使外部函数已经执行完毕,这种技术常用于创建私有变量、封装模块、模拟私有方法等场景

前言

在JavaScript的世界里,闭包是一个既神秘又强大的特性。它既是JavaScript的一大难点,也是JavaScript的特色之一。闭包的运用贯穿于许多高级应用中,可以说,掌握闭包,就能解锁JavaScript编程的更多可能性。

一、闭包与变量作用域

要理解闭包,我们首先得从JavaScript的变量作用域讲起。变量的作用域主要分为两种:全局变量和局部变量。全局变量可以在代码的任何地方被访问,而局部变量则只能在其所在的函数内部被访问。JavaScript语言的特别之处在于,函数内部可以直接读取全局变量,但函数外部却无法读取函数内部的局部变量。闭包就可以实现。

二、闭包的实现-函数返回函数

闭包通常是通过函数返回函数的方式来实现的。这种模式在JavaScript中非常常见,它不仅增强了代码的灵活性,还为闭包的实现提供了便利。以下是一个简单的例子,帮助你更好地理解闭包的实现方式:

    //  定义一个函数用来打招呼
    function greet(name) {
      return function () {
        console.log(`Hello, ${name}!`);
      };
    }
    // 定义一个变量 接收 返回值 
    // 这里返回的是一个函数 
    const greet1 = greet("小红");
    console.log(greet1)
    /**
     * ƒ () {
        console.log(`Hello, ${name}!`);
        }
      */
    //  执行 函数 
    greet1(); // 输出:Hello, 小红! 

在这个例子中,`greet` 函数接收一个参数 `name`,并返回一个匿名函数。这个匿名函数在执行时,会访问其创建时所在的作用域链中的变量 `name`。当我们调用 `greet("小红")` 时,返回的匿名函数就“捕获”了变量 `name` 的值 `"Alice"`,并将其保存在闭包中。因此,当我们调用 `greet()` 时,它就会输出 `"Hello, 小红!"`。这就是闭包的神奇之处,它让函数能够记住并访问其创建时的变量。

三、闭包的应用场景

闭包的应用场景非常广泛,从简单的问候函数到复杂的事件监听器、延迟任务和缓存功能,都可以看到闭包的身影。以下是一些常见的应用场景:

1.封装私有变量

闭包可以用来封装私有变量,实现数据的封装和隐藏。例如,我们可以创建一个用户对象,通过闭包来封装用户的姓名和年龄:

    function createUser(name, age) {
      return {
        getName: function () {
          return name; // 访问闭包中的变量
        },
        getAge: function () {
          return age; // 访问闭包中的变量
        },
        setAge: function (newAge) {
          age = newAge; // 修改闭包中的变量
        }
      };
    }
    const user = createUser("小明", 25);
    console.log(user.getName()); // 输出:小明
    console.log(user.getAge()); // 输出:25
    user.setAge(26);
    console.log(user.getAge()); // 输出:26

在这个例子中,`createUser` 函数通过闭包封装了用户的姓名和年龄。外部代码无法直接访问这些私有变量,只能通过 `getName`、`getAge` 和 `setAge` 方法来获取和修改它们的值。这种封装方式不仅保证了数据的安全性,还提供了灵活的接口供外部代码使用。

2.创建独立的计数器

闭包可以用来创建独立的计数器,每个计数器都有自己的状态,互不影响。例如:

 function createCounter() {
      let count = 0; // 闭包中的状态值
      return function () {
        count += 1; // 每次调用时递增
        console.log(`Count: ${count}`);
      };
    }
    const counter1 = createCounter();
    counter1(); // 输出:Count: 1
    counter1(); // 输出:Count: 2
    counter2(); // 输出:Count: 1
    counter2(); // 输出:Count: 2

在这个例子中,每次调用 `createCounter` 函数时,都会创建一个新的闭包,其中包含一个独立的计数器状态 `count`。因此,`counter1` 和 `counter2` 是两个独立的计数器,它们的计数互不影响。

3.实现延迟任务

闭包还可以用来实现延迟任务,例如:

  // task:这是一个函数,表示需要延迟执行的任务。
    // delay:这是一个数字,表示延迟的时间(单位为毫秒)。
    function createDelayedTask(task, delay) {
      // 在函数内部,定义了一个变量 timeoutId,用于存储 setTimeout 返回的定时器 ID。
      // 这个变量被闭包捕获,因此可以在返回的对象方法中访问和修改它。
      let timeoutId;
      return {
        run: function () {
          timeoutId = setTimeout(task, delay);
        },
        cancel: function () {
          clearTimeout(timeoutId);
          console.log("任务取消");
        }
      };
    }
    // createDelayedTask 返回一个对象,包含 run 和 cancel 方法,
    // 并将其赋值给变量 delayedTask。
    // task:一个匿名箭头函数 () => console.log("执行任务"),表示需要延迟执行的任务。
    // delay:延迟时间为 2000 毫秒(即 2 秒)。
    const delayedTask = createDelayedTask(() => console.log("执行任务"), 2000);
    // 在内部,setTimeout 被调用,将传入的任务函数 () => console.log("执行任务") 设置为在 2 秒后执行。
    delayedTask.run(); // 启动任务
    /**    这里又调用了一个 setTimeout,将 delayedTask.cancel 方法设置为在 1 秒后执行。
    当 delayedTask.cancel 被调用时:
    clearTimeout(timeoutId) 被执行,清除之前设置的定时器(即取消延迟任务)。
    打印消息 "任务取消"。*/
    setTimeout(delayedTask.cancel, 1000); // 在 1 秒后取消任务

在这个例子中,`createDelayedTask` 函数通过闭包封装了延迟任务的逻辑和状态。`run` 方法用于启动任务,`cancel` 方法用于取消任务。通过闭包,我们可以将任务的状态(如 `timeoutId`)保存起来,方便在需要时进行操作。

运行过程总结

时间线:

输出结果:

这种模式在实际开发中非常有用,例如:

4.实现缓存功能

闭包还可以用来实现缓存功能,例如:

    function createCache() {
      const cache = {}; // 闭包中的缓存对象
      return {
        get: function (key) {
          return cache[key];
        },
        set: function (key, value) {
          cache[key] = value;
        }
      };
    }
    const myCache = createCache();
    myCache.set("name", "小军");
    console.log(myCache.get("name")); // 输出:小军

在这个例子中,`createCache` 函数通过闭包封装了一个缓存对象 `cache`。通过 `set` 方法可以将数据存储到缓存中,通过 `get` 方法可以从缓存中获取数据。这种缓存功能在实际开发中非常有用,可以提高程序的性能。

5.leetcode.2715执行可取消函数的解法

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ArrayWrapper Example</title>
</head>
<body>
  <script>
    /**
     * 创建一个可取消的函数执行器
     * @param {Function} fn - 需要延迟执行的函数
     * @param {Array} args - 传递给 fn 的参数数组
     * @param {number} t - 延迟时间(毫秒)
     * @return {Function} - 返回一个取消函数,用于取消延迟执行
     */
    var cancellable = function (fn, args, t) {
      let timeOutId; // 用于存储 setTimeout 返回的定时器 ID
      // 取消函数,用于清除定时器
      function cancelFn() {
        clearTimeout(timeOutId); // 清除定时器,阻止 fn 执行
      }
      // 设置定时器,延迟 t 毫秒后执行 fn
      timeOutId = setTimeout(() => {
        fn(...args); // 使用展开运算符将 args 作为参数传递给 fn
      }, t);
      // 返回取消函数,供外部调用
      return cancelFn;
    };
    // 用于存储执行结果的数组
    const result = [];
    // 示例函数,将输入参数乘以 5
    const fn = (x) => x * 5;
    // 示例函数的参数和延迟时间
    const args = [2], t = 20, cancelTimeMs = 50;
    // 记录开始时间,用于计算延迟执行的时间差
    const start = performance.now();
    // 日志函数,记录函数执行的时间和返回值
    const log = (...argsArr) => {
      const diff = Math.floor(performance.now() - start); // 计算从开始到现在的毫秒数
      result.push({ "time": diff, "returned": fn(...argsArr) }); // 将执行时间和返回值存入 result
    };
    // 创建一个可取消的延迟任务
    const cancel = cancellable(log, args, t);
    // 在 cancelTimeMs 毫秒后调用取消函数,取消延迟任务
    const maxT = Math.max(t, cancelTimeMs); // 计算延迟时间和取消时间的最大值
    setTimeout(cancel, cancelTimeMs);
    // 在延迟任务和取消任务之后,打印结果
    setTimeout(() => {
      console.log(result); // [{"time":20,"returned":10}]
    }, maxT + 15); // 确保在所有任务完成后打印结果
  </script>
</body>
</html>

详细解释:

设置定时器:

timeoutId = setTimeout(() => {
    fn(...args); // 使用 args 作为参数执行 fn
}, t);

这里使用 setTimeout 设置了一个定时器,延迟 t 毫秒后执行 fn(...args)

setTimeout 返回一个唯一的 timeoutId,这个 ID 用于后续的取消操作。

2.定义取消函数:

function cancelFn() {
    clearTimeout(timeoutId); // 清除定时器,取消 fn 的执行
}

cancelFn 是一个函数,它的作用是调用 clearTimeout(timeoutId)

如果在 fn 执行之前调用了 cancelFnclearTimeout 会取消对应的定时器,fn 就不会被执行。

返回取消函数:

return cancelFn;

返回 cancelFn 是为了让调用者能够在需要的时候调用它。

如果调用者没有调用 cancelFn,定时器会正常触发,fn 会在延迟时间 t 后执行。

如果调用者调用了 cancelFnclearTimeout 会取消定时器,fn 就不会被执行。

. setTimeoutclearTimeout 的工作机制

setTimeout:设置一个定时器,延迟 t 毫秒后执行某个函数。它返回一个定时器的 ID(timeoutId),这个 ID 用于后续的取消操作。

clearTimeout:通过传入定时器的 ID 来取消对应的定时器。如果定时器已经被触发(即回调函数已经开始执行),clearTimeout 将不会有任何效果。

四、闭包的注意事项

虽然闭包非常强大,但在使用时也需要小心一些潜在的问题。例如,闭包可能会导致内存泄漏,因为闭包会一直保存其创建时的作用域链中的变量,即使这些变量不再被使用,也不会被垃圾回收器回收。因此,在使用闭包时,我们需要确保及时释放不再使用的变量,避免内存泄漏。

五、总结

闭包是JavaScript中一个非常重要的特性,它通过函数返回函数的方式,让函数能够记住并访问其创建时所在的作用域链中的变量。闭包不仅可以封装私有变量,实现数据的封装和隐藏,还可以创建独立的计数器、实现延迟任务和缓存功能等。在实际开发中,闭包的应用场景非常广泛,掌握闭包的使用方法,可以让你的代码更加灵活和强大。当然,在使用闭包时,我们也要注意避免内存泄漏等问题,合理地使用闭包,才能充分发挥其优势。

以上就是JavaScript闭包实现函数返回函数详解的详细内容,更多关于JavaScript函数返回函数的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文