1 闭包的概念
闭包
是由一个函数以及与其相关的引用环境组合而成的实体。闭包
可以在函数内部访问外部函数的变量,并且这些变量可以在外部函数执行结束后仍然保持其状态。
听起来可能有点抽象,咱们来段代码:
function outerFunction(x) { return function innerFunction(y) { return x + y; }; } // 创建闭包函数 var closure = outerFunction(5); // 调用闭包函数 var result = closure(3); console.log(result); // 输出:8
可以看到 innerFunction
可以获取到传入 outerFunction
的参数,可见原本在外部函数执行结束后本该销毁的地址得到了保留。
在这里我们总结一下闭包的特点:
闭包
可以访问和修改其创建时捕获的外部环境中的变量值,从而实现状态的保存和共享。闭包
可以私有化变量,从而避免命名冲突
和污染全局作用域
。闭包
可以延长变量的生命周期
,从而实现回调、事件处理等高级操作。
有的同学可能会联想到高阶函数
,或许对高阶函数
、闭包
存在不清晰的认知,那么我们就再来讲一下高阶函数
。
1.2 高阶函数
高阶函数
指的是能够接受函数作为参数或返回函数作为结果的函数,如 Array.prototype.map
,Array.prototype.filter
和 Array.prototype.reduce
就是高阶函数的实现。 老样子,我们用代码说明一下:
1.2.1 接收函数
// 定义接收函数的高阶函数 function times(n, f) { for (var i = 0; i < n; i++) { f(i); } } // 使用接收函数的高阶函数 times(5, function(x) { console.log("Hello, " + x + "!"); }); // 输出: // Hello, 0! // Hello, 1! // Hello, 2! // Hello, 3! // Hello, 4!
1.2.2 返回函数
// 定义返回函数的高阶函数 function add(x) { return function(y) { return x + y; }; } // 使用返回函数的高阶函数 var addFive = add(5); console.log(addFive(3)); // 输出:8 console.log(addFive(7)); // 输出:12
明显可以看出闭包
就属于返回函数的一类,由此得知,闭包
是高阶函数
的一种特殊形式。
二、常见应用
2.1 防抖
防抖
的原理是在一段连续触发的时间内,只执行最后一次操作。
function debounce(func, delay) { let timer; return function () { const context = this; const args = arguments; // 再次调用时,清除time,重新计时 clearTimeout(timer); timer = setTimeout(() => { // 通过apply执行传入的函数 func.apply(context, args); }, delay); }; }
2.2 节流
节流
的原理是在一段时间内,固定执行操作的频率。
function throttle(func, interval) { let timer; return function () { const context = this; const args = arguments; if (!timer) { timer = setTimeout(function () { func.apply(context, args); // 触发完成后清除timer,进入下一周期 timer = null; }, interval); } }; }
2.3 curry函数
柯里化(Currying)
是一种将多个参数的函数转换为接受单个参数的函数序列的技术。
// 实现一个add函数 // return 1 + 2 + 3 + 2 传参为undefined时返回sum
function add() { // 创建空数组来维护所有要 add 的值 const args = [] // curry 函数,存入每次调用传入的参数 function curried(...nums) { if (nums.length === 0) { // 长度为0,说明调用结束,返回 args 的 sum return args.reduce((pre, cur) => pre + cur, 0); } else { // 长度不为0,将传入的参数存入 args,返回 curried函数给下一次调用 args.push(...nums); return curried; } } // 一开始给 curried 传递 add 接收到的参数 arguments return curried(...Array.from(arguments)); } console.log(add(1, 2)(1)()); // 输出:4 console.log(add(1)(2)(3)(4)()); // 输出:10 console.log(add(5)()); // 输出:5
2.4 迭代器
在 JavaScript
中,可以使用生成器函数(generator function)
来实现迭代器。我们按照这个思路,来实现一个迭代器,满足遍历给定范围内的数字的功能
function rangeIterator(start, end) { // current 用于维护当前遍历到的值 let current = start; return { next: function() { if (current <= end) { return { value: current++, done: false }; } else { return { done: true }; } } }; } // 使用示例 const iter = rangeIterator(1, 5); console.log(iter.next()); // 输出:{ value: 1, done: false } console.log(iter.next()); // 输出:{ value: 2, done: false } console.log(iter.next()); // 输出:{ value: 3, done: false } console.log(iter.next()); // 输出:{ value: 4, done: false } console.log(iter.next()); // 输出:{ value: 5, done: false } console.log(iter.next()); // 输出:{ done: true }
2.5 链式调用
链式调用
是一种编程风格,通过在对象上连续调用多个方法,使得代码看起来像是一条链条。每个方法都会返回对象本身,使得可以在同一个表达式中连续调用多个方法。如 Promise
、lodash
库都有体现这样的风格。
这里给出一个链式调用函数,包含了 二次方、三次方、取反、随机数、取值等操作。
function Chainable(value) { // result 维护当前的运算值 let result = value; this.square = function() { result = Math.pow(result, 2); return this; }; this.cube = function() { result = Math.pow(result, 3); return this; }; this.negate = function() { result = -result; return this; }; this.random = function() { result = Math.random() * result; return this; }; this.value = function() { return result; }; } // 创建可链式调用对象 const chain = new Chainable(5); const value = chain.square().cube().negate().random().value(); console.log(value); // 示例输出:-239.40167432539412
当然也可以换一种写法:
function chainable(val) { // result 维护当前的运算值 let result = val; const square = function () { result = Math.pow(result, 2); return this; }; const cube = function () { result = Math.pow(result, 3); return this; }; const negate = function () { result = -result; return this; }; const random = function () { result = Math.random() * result; return this; }; const value = function () { return result; }; return { square, cube, negate, random, value } } const value = chainable(5).square().cube().negate().random().value(); console.log(value); // 示例输出:-239.40167432539412
2.6 发布订阅模式
发布订阅模式
是一种常见的设计模式,它用于在不同的对象之间建立松散耦合的联系。该模式包含两个核心概念:发布者(Publisher)
和订阅者(Subscriber)
。发布者负责发布事件和通知订阅者,而订阅者则负责订阅事件并接收通知。
下面我通过闭包实现如上功能,因为结构比较复杂,所以注释写的非常详细,如果大家还看不懂的话,可以移步其他大佬的文章~
function eventEmitter() { // events 维护各事件以及对应的订阅者 const events = {}; // on 函数绑定订阅者(callback)至相应的事件(eventName) function on(eventName, callback) { // 如果 events 不存在该事件则创建该事件并赋值一个空数组存放订阅者 events[eventName] = events[eventName] || []; // 存入订阅者 events[eventName].push(callback); } // emit 函数发布事件(eventName),并传递相关参数(args)给订阅者 function emit(eventName, ...args) { // 赋值订阅者数组 const callbacks = events[eventName]; if (callbacks) { callbacks.forEach(callback => callback(...args)); } } // off 函数解除订阅关系 function off(eventName, callback) { if (!callback) { // 订阅者为空,直接删除事件 delete events[eventName]; } else { // 订阅者不为空,筛选订阅者 events[eventName] = events[eventName].filter(cb => cb !== callback); } } return { on, emit, off }; } // 使用示例 const emitter = eventEmitter(); function handler1(name) { console.log(`${name} says hello from handler1`); } function handler2(name) { console.log(`${name} says hello from handler2`); } emitter.on('hello', handler1); emitter.on('hello', handler2); emitter.emit('hello', 'Alice'); // 输出 "Alice says hello from handler1" 和 "Alice says hello from handler2" emitter.off('hello', handler1); emitter.emit('hello', 'Bob'); // 只输出 "Bob says hello from handler2"
2.7 缓存
闭包
还可以实现缓存
的效果,下面的例子我用 timeGap
来体现一下缓存的作用
function memoize(fn) { const cache = {}; // 缓存对象,用于存储函数执行结果 return function (...args) { const startTime = performance.now() const key = JSON.stringify(args); // 将参数序列化为字符串作为缓存的键 if (cache[key] === undefined) { // 如果缓存中没有该键对应的结果,则执行函数并将结果缓存起来 cache[key] = fn.apply(this, args); } // 获取时间差 判断是否有缓存 console.log('timeGap', performance.now() - startTime) return cache[key]; // 返回缓存的结果 }; } // 没啥意义的函数,纯属跑循环延长函数运行时间 const getTime = memoize((times) => { let a = 0 for (let i = 0; i < times; i++) { a++ } return times }) getTime(10000000) // timeGap 4.4000000059604645 getTime(20000000) // timeGap 8 getTime(20000000) // timeGap 0
三、闭包的缺点
先上结论:
闭包
对外部函数有引用时,若闭包被调用且未及时解绑,则会造成外部函数的变量无法被释放,导致内存泄露
闭包
涉及作用域链查找
,性能相较直接访问局部、全局变量要低一些,在一些频繁调用或要求高性能的场景不适用闭包
可以访问外部函数中的私有变量,这可能导致信息泄露和安全问题
。如果闭包被滥用或不当使用,可能会导致数据被意外泄露给未授权的代码。
就这三个问题,我们依次还原案例并给出解决方案
3.1 内存泄露
下面我们仅针对于闭包造成的内存泄露给出案例:
function outer() { let count = 0; function inner() { count++; console.log(count); } return inner; } // 创建闭包 const fn = outer(); // 调用多次,但没有及时解绑闭包 fn(); fn(); fn(); fn(); // ... // 结果:count 变量无法被释放,造成内存泄漏
解决方案:
- 将闭包函数设置为 null:
fn = null;
- 将闭包函数重新赋值:
fn = outer();
3.2 作用域链查找
作用域和作用域链
也是常问的点,这边的例子延用上例的 outer
函数
// 创建闭包 const fn = outer(); // 调用多次,性能受到影响 for (let i = 0; i < 10000; i++) { fn(); }
解决方案:
- 考虑使用其他的编程模式或技术替代闭包
- 将其应用范围限制在必要的情况下,并在可能的情况下将访问局部、全局变量的次数降到最低
- 将闭包函数的执行结果缓存(这里可以用闭包实现缓存)起来,以便减少性能开销。
3.3 信息泄露与安全
体现的案例与4.1相同,如果 count
为隐私数据,则可能触发问题。
解决方案:
- 将敏感信息存储在非闭包变量中,并仅向必要的代码公开(例如使用
访问控制
和权限管理
技术) - 避免其他代码访问到闭包中的变量,可以使用
立即执行函数
将闭包函数包装起来,并将其返回值设置为一个包含公开接口
的对象,只有这些公开接口才能访问到闭包变量,可以有效地保护闭包中的私有信息。