JS面试题之---解释一下什么是闭包?
- 闭包:闭包是一个函数,它能够访问并引用其外部函数作用域中的变量,即使外部函数已经执行完毕。这种特性使得闭包可以保持对外部变量的引用,从而在函数外部也能访问和修改这些变量。
function createCounter() {
let count = 0; // 这是外部函数的局部变量
return function() { // 返回的内部函数形成了闭包
count += 1; // 内部函数可以访问并修改外部函数的局部变量
return count; // 返回当前的计数值
};
}
const counter = createCounter(); // 创建一个计数器
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3
//这个例子展示了闭包的一个典型用途:在 JavaScript 中实现数据的私有性,通过创建函数工厂生成具有独立状态的计数器。闭包使得我们可以在内部函数中访问和操作外部函数的变量,即使外部函数已经返回并结束。
-
闭包需要满足三个条件:
-
函数嵌套:闭包必须是一个内部函数,即一个函数内部定义了另一个函数。
-
引用外部函数变量:内部函数可以访问和修改外部函数的变量。
-
延续外部函数的生命周期:即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。
-
function outerFunction() {
let outerVariable = 'I am from the outer function!'; // 外部函数的局部变量
// 条件1: 函数嵌套
function innerFunction() {
// 条件2: 访问外部变量
console.log(outerVariable);
}
return innerFunction; // 返回内部函数
}
const closureFunction = outerFunction(); // 执行外部函数并返回内部函数
closureFunction(); // 输出: I am from the outer function!
//解释:
//函数嵌套:innerFunction 是定义在 outerFunction 内部的。
//访问外部变量:innerFunction 中可以访问并输出 outerVariable,即使执行 outerFunction 已经结束。
//延续外部变量的生命周期:即使 outerFunction 的执行上下文已经结束,但 innerFunction 仍然保持对 outerVariable 的引用,允许我们在调用 closureFunction() 时访问 outerVariable。
-
优点: 可以重复使用变量,并且不会造成变量污染。
- 缺点: 会引起内存泄漏。
- 内存泄漏是指程序在运行时没有及时释放不再使用的内存,导致可用内存逐渐减少,可能最终导致应用程序性能下降甚至崩溃。
- 闭包可以持有外部作用域的变量引用,如果这些变量的生命周期超出了需要使用的范围,可能会导致内存泄漏。
- 如果使用了某些长生命周期的数据结构(如单例模式或全局缓存)而没有适当地清理不再需要的数据,也可能导致内存泄漏。
-
预防内存泄漏的方法:
- 及时清理引用:确保不再需要的对象或变量被及时置为
null
,允许垃圾回收机制回收内存。 -
function createClosure() { let resource = { /* large object */ }; return function innerFunction() { // 使用 resource }; } // 当不再需要这个闭包,做清理 const closure = createClosure(); // ...使用 closure... closure = null; // 切断与资源的引用
-
避免使用全局变量:尽量减少全局变量的使用,使用模块化的代码组织方式。
-
let globalRef; // 避免使用,如有必要,考虑使用模块化方法。 function createClosure() { let localResource = { /* large object */ }; return function innerFunction() { // 不要把 localResource 放入 globalRef }; }
- 及时清理引用:确保不再需要的对象或变量被及时置为
-
- 闭包的主要用途包括:
- 实现数据封装和私有变量
- 闭包能够通过函数作用域来封装变量,让这些变量不会被外部访问,从而实现数据封装和私有变量的效果。这有助于保护数据不被意外修改。
-
function createCounter() { let count = 0; // 私有变量 return { increment: function() { count++; return count; }, decrement: function() { count--; return count; }, getCount: function() { return count; } }; } const counter = createCounter(); console.log(counter.increment()); // 1 console.log(counter.increment()); // 2 console.log(counter.getCount()); // 2
- 实现函数柯里化
- 函数柯里化是将多个参数的函数转换为一系列单一参数的函数链,闭包可以用来保持对初始参数的访问。
-
function curriedSum(a) { return function(b) { return a + b; }; } const addFive = curriedSum(5); console.log(addFive(10)); // 15 //curriedSum 函数返回一个新函数,该新函数闭包当前的 a 值。这样你可以逐步传递参数。
- 实现函数防抖和节流
- 防抖(Debounce):多次触发 只执行最后一次。
-
function debounce(func, delay) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } const handleResize = debounce(() => { console.log('Window resized!'); }, 200); window.addEventListener('resize', handleResize);
-
- 节流(Throttle):规定时间内 只触发一次。
-
function throttle(func, limit) { let lastFunc; let lastRan; return function(...args) { if (!lastRan) { func.apply(this, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(() => { if ((Date.now() - lastRan) >= limit) { func.apply(this, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; } const logScroll = throttle(() => { console.log('Scroll event triggered!'); }, 1000); window.addEventListener('scroll', logScroll);
-
- 防抖(Debounce):多次触发 只执行最后一次。
- 实现缓存机制
- 闭包可以存储和缓存计算结果,从而提高程序效率,尤其是在频繁调用某个计算开销大的函数时。
-
function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (cache[key]) { return cache[key]; // 从缓存返回结果 } const result = fn.apply(this, args); cache[key] = result; // 保存到缓存 return result; }; } const factorial = memoize(function(n) { if (n === 0) return 1; return n * factorial(n - 1); }); console.log(factorial(5)); // 120 console.log(factorial(5)); // 从缓存返回 120
- 实现数据封装和私有变量
-
使用闭包的注意点:
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
-
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象( object )使用,把闭包当作它的公用方法( Public Method ),把内部变量当作它的私有属性 (private value ),这时一定要小心,不要随便改变父函数内部变量的值。