JavaScript中的闭包函数
闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。
原理:利用作用域的嵌套,触发计算机的垃圾回收机制,将原本要删除的变量暂时保存起来,可继续使用。
以下为一些闭包的作用域以及应用:
作用域
作用域决定这个变量的生命周期及其可见性。 当我们创建了一个函数或者 {} 块,就会生成一个新的作用域。需要注意的是,通过 var 创建的变量只有函数作用域,而通过 let 和 const 创建的变量既有函数作用域,也有块作用域。
嵌套作用域
在 Javascript 中函数里面可以嵌套函数,如下:
(function fn(){
var x = 1;
function fn1(){
console.log(x);
}
fn1();
})();
fn1() 即是一个嵌套在 fn() 函数里面的函数。在 fn1() 函数里面可以通过外部函数访问到变量 x。此时,fn1() 函数就是一个闭包。
闭包就是内部函数,我们可以通过在一个函数内部或者 {} 块里面定义一个函数来创建闭包。
外部函数作用域
内部函数可以访问外部函数中定义的变量,即使外部函数已经执行完毕。如下:
(function fn(){
var x = 1;
setTimeout(function fn1(){
console.log(x);
}, 1000);
})();
并且,内部函数还可以访问外部函数中定义的形参,如下:
(function fn(p){
var x = 1;
setTimeout(function fn1(){
console.log(x);//1
console.log(p);//10
}, 1000);
})(10);
外部块作用域
内部函数可以访问外部块中定义的变量,即使外部块已执行完毕,如下:
{
let x = 1;
setTimeout(function fn(){
console.log(x);
}, 1000);
}
作用域链
每一个作用域都有对其父作用域的引用。当我们使用一个变量的时候,Javascript引擎 会通过变量名在当前作用域查找,若没有查找到,会一直沿着作用域链一直向上查找,直到 global 全局作用域。
示例如下:
let x0 = 0;
(function fn1(){
let x1 = 1;
(function fn2(){
let x2 = 2;
(function fn3(){
let x3 = 3;
console.log(x0 + " " + x1 + " " + x2 + " " + x3);//0 1 2 3
})();
})();
})();
我们可以看到,fn3() 这个内部函数可以访问其自身局部变量 x3 ,也可以访问外部作用域中的 x1 和 x2 变量,以及全局作用域中的 x0 变量。即:闭包可以访问其外部(父)作用域中的定义的所有变量。
外部作用域执行完毕后
当外部作用域执行完毕后,内部函数还存活(仍在其他地方被引用)时,闭包才真正发挥其作用。譬如以下几种情况:
- 在异步任务例如 timer 定时器,事件处理,Ajax 请求中被作为回调
- 被外部函数作为返回结果返回,或者返回结果对象中引用该内部函数
考虑如下的几个示例:
Timer
(function fn(){
let x = 1;
setTimeout(function fn1(){
console.log(x);
}, 1000);
})();
变量 x 将一直存活着直到定时器的回调执行或者 clearTimeout() 被调用。
如果这里使用的是 setInterval() ,那么变量 x 将一直存活到 clearInterval() 被调用。
Event
(function fn(){
let x = 1;
$("#btn").on("click", function fn1(){
console.log(x);
});
})();
当变量 x 在事件处理函数中被使用时,它将一直存活直到该事件处理函数被移除。
Ajax
(function fn(){
let x = 1;
fetch("http://").then(function fn1(){
console.log(x);
});
})();
变量 x 将一直存活到接收到后端返回结果,回调函数被执行。
在已上几个示例中,我们可以看到,fn1() 函数在父函数执行完毕后还一直存活着,fn1() 函数就是一个闭包。
除了 timer 定时器,事件处理,Ajax 请求等比较常见的异步任务,还有其他的一些异步 API 比如 HTML5 Geolocation,WebSockets , requestAnimationFrame()也将使用到闭包的这一特性。
变量的生命周期取决于闭包的生命周期。被闭包引用的外部作用域中的变量将一直存活直到闭包函数被销毁。如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁。
闭包与循环
闭包只存储外部变量的引用,而不会拷贝这些外部变量的值。
查看如下示例:
function initEvents(){
for(var i=1; i<=3; i++){
$("#btn" + i).click(function showNumber(){
alert(i);//4
});
}
}
initEvents();
在这个示例中,我们创建了3个闭包,皆引用了同一个变量 i,且这三个闭包都是事件处理函数。由于变量 i 随着循环自增,因此最终输出的都是同样的值。
修复这个问题最简单的方法是在 for 语句块中使用 let 变量声明,这将在每次循环中为 for 语句块创建一个新的局部变量。如下:
function initEvents(){
for(let i=1; i<=3; i++){
$("#btn" + i).click(function showNumber(){
alert(i);//1 2 3
});
}
}
initEvents();
但是,如果变量声明在 for 语句块之外的话,即使用了 let 变量声明,所有的闭包还是会引用同一个变量,最终输出的还是同一个值。