JavaScript闭包实现函数返回函数详解
作者:友人.227
前言
在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`)保存起来,方便在需要时进行操作。
运行过程总结
时间线:
- 0 秒:调用
delayedTask.run()
,设置一个定时器,计划在 2 秒后执行任务(打印"执行任务"
)。 - 1 秒:调用
delayedTask.cancel()
,清除定时器,取消任务。 - 2 秒:原计划的任务不会执行,因为定时器已被清除。
输出结果:
- 在 1 秒后,控制台会输出
"任务取消"
。 - 2 秒后,不会输出
"执行任务"
,因为任务已被取消。
这种模式在实际开发中非常有用,例如:
- 在用户操作频繁的场景下,避免重复触发某些操作(如搜索框的防抖功能)。
- 在需要延迟执行任务但可能需要取消任务的场景中(如用户取消操作或超时取消任务)。
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
执行之前调用了 cancelFn
,clearTimeout
会取消对应的定时器,fn
就不会被执行。
返回取消函数:
return cancelFn;
返回 cancelFn
是为了让调用者能够在需要的时候调用它。
如果调用者没有调用 cancelFn
,定时器会正常触发,fn
会在延迟时间 t
后执行。
如果调用者调用了 cancelFn
,clearTimeout
会取消定时器,fn
就不会被执行。
. setTimeout
和 clearTimeout
的工作机制
setTimeout
:设置一个定时器,延迟 t
毫秒后执行某个函数。它返回一个定时器的 ID(timeoutId
),这个 ID 用于后续的取消操作。
clearTimeout
:通过传入定时器的 ID 来取消对应的定时器。如果定时器已经被触发(即回调函数已经开始执行),clearTimeout
将不会有任何效果。
四、闭包的注意事项
虽然闭包非常强大,但在使用时也需要小心一些潜在的问题。例如,闭包可能会导致内存泄漏,因为闭包会一直保存其创建时的作用域链中的变量,即使这些变量不再被使用,也不会被垃圾回收器回收。因此,在使用闭包时,我们需要确保及时释放不再使用的变量,避免内存泄漏。
五、总结
闭包是JavaScript中一个非常重要的特性,它通过函数返回函数的方式,让函数能够记住并访问其创建时所在的作用域链中的变量。闭包不仅可以封装私有变量,实现数据的封装和隐藏,还可以创建独立的计数器、实现延迟任务和缓存功能等。在实际开发中,闭包的应用场景非常广泛,掌握闭包的使用方法,可以让你的代码更加灵活和强大。当然,在使用闭包时,我们也要注意避免内存泄漏等问题,合理地使用闭包,才能充分发挥其优势。
以上就是JavaScript闭包实现函数返回函数详解的详细内容,更多关于JavaScript函数返回函数的资料请关注脚本之家其它相关文章!