JavaScript闭包:那些年我们一起踩过的"坑"与"宝藏"
如果说JavaScript是一门魔法语言,那么闭包就是其中最神秘的咒语。有人说它是JavaScript的精髓,有人说它是内存泄漏的罪魁祸首。今天,让我们一起揭开闭包的神秘面纱,看看这个让无数前端工程师又爱又恨的特性到底是何方神圣。
🎭 开场白:从一个"诡异"的现象说起
先来看一段代码,猜猜会输出什么:
showName()
console.log(myname);
var myname = 'zz'
function showName() {
console.log('函数
showName 执行了');
}
如果你猜的是:
函数showName 执行了
undefined
那么恭喜你,你已经初步理解了JavaScript的"声明提升"机制!这就像是JavaScript引擎有一双"透视眼",能够在代码执行前就看到所有的声明。
🏗️ 基础建设:理解JavaScript的执行机制
声明提升:JavaScript的"预知能力"
核心概念 :引擎会先编译代码,再执行
JavaScript引擎就像一个勤奋的学生,总是要先预习(编译)再上课(执行)。在编译阶段,所有的 var 声明和 function 声明都会被"提升"到作用域的顶部。
调用栈:JavaScript的"记忆宫殿"
核心概念 :JS引擎追踪函数的一个机制,管理一份代码的执行关系
调用栈就像是JavaScript的"记忆宫殿",每当函数被调用时,就会在栈顶创建一个新的执行上下文。但要注意:
⚠️ 警告 :调用栈不能设计得太大,否则JS引擎在查找上下文时会花费大量时间!
来看一个递归的例子:
// 递归
function runStack(n) {
if (n === 0) return 100
return runStack(n - 2)
}
runStack(50000) // 这可能会导致栈溢出!
🔍 作用域链:JavaScript的"寻宝图"
核心概念 :JS引擎在查找变量时,会先从当前作用域查找,如果没有,就会向上一级作用域查找,直到找到为止,或者直到全局作用域为止
作用域链就像一张"寻宝图",JavaScript引擎会沿着这张图一层层向上寻找变量。关键是: 作用域链的下一级是谁,是由outer指针决定的 !
让我们看一个经典的例子:
function a() {
var num = 10
function b() {
var num = 20
c()
}
function c() {
console.log
(num); // 输出什么?
}
b()
}
a() // 输出:10
为什么输出10而不是20?
因为函数 c 的outer指针指向的是函数 a 的作用域,而不是函数 b !这就是词法作用域的魅力所在。
🏠 块级作用域:let和const的"领地"
在ES6之前,JavaScript只有函数作用域和全局作用域。ES6引入了 let 和 const ,配合 {} 创造了块级作用域:
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a); // 1
console.log(b); // 3
}
console.log(b); // 2
console.log(c); // 4
console.log(d); // ReferenceError: d is not defined
}
看到了吗? var 声明的变量会"穿透"块级作用域,而 let 声明的变量则被"困"在了块级作用域中。
🎪 闭包:JavaScript的"魔法盒子"
终于到了今天的主角——闭包!
官方定义 :根据作用域链查找规则,内部函数一定有权访问外部函数的变量。另外,一个函数执行完毕后它的执行上下文一定会被销毁。那么当函数A内部声明了一个函数B,而函数B被拿到A的外部执行时,为了保证以上两个规则正常执行,A函数在执行完毕后会将B需要访问的变量保存在一个集合中,并留在调用栈当中,这个集合就是闭包。
听起来很抽象?让我们看一个具体的例子:
function foo() {
var myname = 'zz'
var age = 18
return function bar() {
console.log(myname);
}
}
var baz = foo()
baz() // 输出:zz
神奇的事情发生了!
按理说, foo 函数执行完毕后,它的执行上下文应该被销毁, myname 变量也应该不存在了。但是 baz() 依然能够访问到 myname !
这就是闭包的魔法: JavaScript引擎发现函数 bar 需要访问外部变量 myname ,于是在 foo 执行完毕后,将 myname 保存在了一个特殊的"魔法盒子"里——这就是闭包!
🕳️ 闭包的"陷阱":经典的循环问题
闭包虽然强大,但也容易让人掉坑。来看一个经典的例子:
var arr = [] // function(){} function(){} function(){}...
// 错误的写法(注释掉的部分)
// for (var i = 0; i < 5; i++) {
// arr.push(function () {
// console.log(i); // 这里会输出什么?
// })
// }
// 正确的写法:使用立即执行函数
for (var i = 0; i < 5; i++) {
(function(j) {
arr.push(function ()
{
console.log(j); // 输出 0, 1, 2, 3, 4
})
})(i)
}
// 执行
for (var j = 0; j < arr.length; j++) {
arr[j]()
}
为什么需要立即执行函数?
因为如果直接在循环中创建函数,所有的函数都会共享同一个变量 i 的引用。当循环结束时, i 的值是5,所以所有函数都会输出5。
而使用立即执行函数 (function(j){...})(i) ,我们为每次循环创建了一个新的作用域,并将当前的 i 值"拷贝"给了参数 j ,这样每个函数就有了自己独立的变量副本。
⚠️ 闭包的"副作用":内存泄漏
缺点 :内存泄漏
闭包的强大之处在于它能让变量"永生",但这也是它的危险之处。如果不小心使用,闭包会导致内存泄漏:
function createHeavyObject()
{
var heavyData = new Array
(1000000).fill('大量数据');
return function() {
// 即使这里不使用 heavyData
// 但由于闭包的存在,heavyData 不会被垃圾回收
console.log('我是一个闭包');
}
}
var leak = createHeavyObject();
// heavyData 永远不会被释放,除非 leak = null
🎯 闭包的实际应用
1. 模块化模式
var myModule = (function() {
var privateVar = '我是私有变量';
return {
publicMethod:
function() {
return
privateVar;
}
};
})();
2. 函数柯里化
function add(a) {
return function(b) {
return a + b;
}
}
var add5 = add(5);
console.log(add5(3)); // 8
3. 防抖和节流
function debounce(func, delay) {
var timer;
return function() {
clearTimeout(timer);
timer = setTimeout(func, delay);
}
}
🎨 总结:闭包的"人生哲理"
闭包就像人生中的回忆,有些美好的时光虽然已经过去,但它们会被保存在我们心中的"闭包"里,在需要的时候重新唤起。
闭包的核心要点:
- 词法作用域 :函数的作用域在定义时就确定了
- 变量保持 :内部函数可以访问外部函数的变量
- 生命周期延长 :被引用的变量不会被垃圾回收
- 内存管理 :合理使用,避免内存泄漏
🚀 进阶挑战
理解了闭包的基本概念后,你可以尝试思考这些问题:
- 如何用闭包实现一个计数器?
- 闭包和箭头函数的this绑定有什么关系?
- 在React Hooks中,闭包扮演了什么角色? "闭包不仅仅是一个技术概念,它更像是JavaScript给我们的一份礼物——让我们能够创造出更加灵活和强大的代码。掌握了闭包,你就掌握了JavaScript的精髓!"
愿你在JavaScript的世界里,永远能够优雅地驾驭闭包这个强大的工具! 🎯