知识篇 -- JavaScript闭包详解
Ray Shine
2024/2/28 JavaScript基础知识闭包
本文为博主原创文章,遵循
CC 4.0 BY-SA
版权协议,转载请附上原文出处链接和本声明。
如有侵权,请联系
本博主
删除。
闭包(Closure)是JavaScript中一个强大而又常常令人困惑的概念。简单来说,闭包是指一个函数能够记住并访问其词法作用域(即函数定义时的作用域),即使该函数在其词法作用域之外执行。理解闭包对于掌握JavaScript的高级特性和编写健壮的代码至关重要。
# 词法作用域 (Lexical Scoping) 基础
要理解闭包,首先要理解JavaScript的词法作用域。词法作用域意味着函数的作用域在函数定义时就已经确定了,而不是在函数调用时确定。函数可以访问其自身作用域、父级作用域,直到全局作用域中的变量。
示例:
function outer() {
let outerVar = "我是外部变量";
function inner() {
console.log(outerVar); // inner函数可以访问outerVar
}
inner();
}
outer(); // 输出:我是外部变量
# 闭包的形成条件与机制 核心概念
闭包的形成需要满足以下三个核心条件:
- 函数嵌套:存在一个内部函数。
- 内部函数引用外部函数作用域的变量:内部函数引用了其外部(父级)函数作用域中的变量。
- 内部函数被“带出”外部函数:内部函数被作为返回值、参数传递或赋值给外部变量,使其能够在外部函数执行完毕后仍然被访问。
当外部函数执行完毕,其作用域通常会被销毁,但如果内部函数形成了闭包,那么外部函数作用域中被内部函数引用的变量将不会被垃圾回收机制回收,而是会一直保存在内存中,直到内部函数不再被引用。
示例:
function createCounter() {
let count = 0; // 外部函数作用域中的变量
return function() { // 内部函数,引用了outerVar
count++;
console.log(count);
};
}
const counter1 = createCounter(); // createCounter执行完毕,但其作用域中的count变量被内部函数引用
counter1(); // 输出:1
counter1(); // 输出:2
const counter2 = createCounter(); // 再次调用createCounter,创建了新的作用域和新的count变量
counter2(); // 输出:1
解释
createCounter函数返回了一个匿名函数。- 当
createCounter执行完毕后,count变量并没有被销毁,因为它被返回的匿名函数所引用。 counter1和counter2分别是createCounter两次调用返回的不同闭包实例,它们各自拥有独立的count变量。
# 闭包的常见应用场景 应用
闭包在JavaScript中有着广泛的应用,是许多高级特性和模式的基础。
# 1. 数据私有化 (Private Variables) 封装
通过闭包可以创建私有变量,外部无法直接访问,只能通过暴露的特权方法进行操作。 示例:
function createPerson(name) {
let _age = 0; // 私有变量
return {
getName: function() {
return name;
},
getAge: function() {
return _age;
},
setAge: function(newAge) {
if (newAge >= 0) {
_age = newAge;
}
}
};
}
const john = createPerson("John");
console.log(john.getName()); // John
console.log(john.getAge()); // 0
john.setAge(25);
console.log(john.getAge()); // 25
// console.log(john._age); // undefined,无法直接访问私有变量
# 2. 模块模式 (Module Pattern) 模块化
利用闭包来封装模块,暴露公共接口,隐藏私有实现。 示例:
const myModule = (function() {
let privateVar = "我是私有变量";
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
console.log("我是公共方法");
privateMethod(); // 公共方法可以访问私有方法和变量
}
};
})();
myModule.publicMethod(); // 输出:我是公共方法,我是私有变量
// myModule.privateMethod(); // TypeError: myModule.privateMethod is not a function
# 3. 函数柯里化 (Function Currying) 函数式
通过闭包将一个多参数函数转换为一系列单参数函数。 示例:
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5);
console.log(add5(3)); // 8
console.log(add(10)(2)); // 12
# 4. 节流 (Throttling) 和防抖 (Debouncing) 性能优化
在处理高频事件(如窗口resize、滚动、输入框输入)时,闭包常用于保存定时器ID和上次执行时间,以控制函数执行频率。 示例 (防抖函数骨架):
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// const handleResize = debounce(() => console.log("窗口大小改变"), 300);
// window.addEventListener("resize", handleResize);
# 闭包的内存管理与注意事项 内存
虽然闭包非常强大,但如果不正确使用,可能会导致内存泄漏。
- 内存泄漏:由于闭包会持有对其外部作用域变量的引用,如果闭包本身长时间不被释放,那么它所引用的外部变量也无法被垃圾回收,从而导致内存占用持续增加。
- 避免策略:
- 及时解除不再需要的闭包引用。
- 对于DOM元素,如果闭包引用了DOM元素,确保在DOM元素被移除时,闭包也被解除引用。
- 在循环中创建闭包时要特别小心,确保每个闭包引用的是正确的变量值(通常通过立即执行函数表达式IIFE或
let/const解决)。
理解闭包是JavaScript进阶的标志之一。它不仅是面试中的高频考点,更是编写优雅、高效、模块化JavaScript代码的利器。