# 理解js闭包
# 闭包和作用域链
闭包是指有权访问另一个函数作用域中的变量的函数。(《JavaScript高级程序设计》7.2闭包)
所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。(《JavaScript权威指南》8.6闭包)
和其他大多数现代编程语言一样,JavaScript也采用词法作用域(lexical scoping),也就是说,函数的执行依赖于变量作用域, 这个作用域是在函数定义时决定的,而不是函数调用时决定的。(《JavaScript权威指南》8.6闭包)
每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。 这个作用域链是一个对象列表或者链表,这组对象定义了这段代码”作用域中“的变量。(《JavaScript权威指南》3.10.3作用域链)
当JavaScript需要查找变量的x的值的时候,它会从作用域链中的第一个对象开始查找, 如果这个对象没有名为x的属性,就会继续查找链上下一个对象,最后到全局作用域,如果全都没有找到就会抛出一个引用错误(ReferenceError)异常。
# 实例
var count = 0;
function closure() {
var count = 1;
return function(){
return count += 5
}
}
var f = closure();
console.log(f()) // 6
console.log(f()) // 11
closure函数返回了一个匿名函数,将其赋值给f,多次调用f,closure函数内的count累计增加。
由于作用域是在函数定义时决定而不是函数调用时决定的,f return的变量是closure函数作用域内的count,而不是全局作用域的count。
var str = 'IIFE(立即执行函数表达式)同样创建闭包';
(function IIFE(){
console.log(str);
})();
IIFE(立即执行函数表达式)同样创建闭包,可以访问全局作用域和当前函数的变量作用域。
再看一个经典的例子
for(var i = 0; i < 5; i ++){
setTimeout(function() {
console.log(i)
}, 1000)
}
由于setTimeout属于宏任务(相关知识看这里event loop 事件循环 (opens new window)), 所以当5次循环结束,i变成5,才会执行回调函数, 又因为5个回调函数均为1000ms延时,且它们被推入macrotask栈的时间几乎相同, 所以结果是1秒后输出五个5。
如何使输出结果变成0、1、2、3、4?
- 方法一
for(var i = 0; i < 5; i ++){
setTimeout(function(n) {
console.log(n)
}, 1000, i)
}
- 方法二
for(let i = 0; i < 5; i ++){
setTimeout(function() {
console.log(i)
}, 1000)
}
- 方法三
for(var i = 0; i < 5; i ++){
(function(n){
setTimeout(function() {
console.log(n)
}, 1000)
})(i)
}
- 方法四
var fun = function(n){
setTimeout(function() {
console.log(n)
}, 1000)
}
for(var i = 0; i < 5; i ++){
fun(i)
}
其实方法一、三、四的道理是一样的,原来的回调函数打印的i是直接通过闭包取的全局作用域的i,而这三种方法本质上都是将i的值作为函数的入参传入,使得打印的时候直接取函数作用域内的入参。
方法二利用了ES6的let,let声明的变量只在let命令所在的代码块内有效,5次循环其实就有5个代码块,每个代码块都有一个自己的let变量。
如何使输出结果之间间隔1s?
- 方法一
var fun = function(n){
setTimeout(function() {
console.log(new Date(), n)
}, 1000*n)
}
for(var i = 0; i < 5; i++){
fun(i)
}
//Tue Mar 16 2021 23:52:38 GMT+0800 (中国标准时间) 0
//Tue Mar 16 2021 23:52:39 GMT+0800 (中国标准时间) 1
//Tue Mar 16 2021 23:52:40 GMT+0800 (中国标准时间) 2
//Tue Mar 16 2021 23:52:41 GMT+0800 (中国标准时间) 3
//Tue Mar 16 2021 23:52:42 GMT+0800 (中国标准时间) 4
方法一直接利用下标间距为1设置不同的延时。 但当循环体不是i++而是i+=x时,延时就得麻烦的进行1000*n/x计算了。
- 方法二
const fun = gap => new Promise((resolve,reject) => {
setTimeout(resolve, gap);
});
(async () => {
for (var i = 0; i < 5; i++) {
if(i!==0) await fun(1000);
console.log(new Date(), i);
}
})();
//Tue Mar 16 2021 23:53:15 GMT+0800 (中国标准时间) 0
//Tue Mar 16 2021 23:53:16 GMT+0800 (中国标准时间) 1
//Tue Mar 16 2021 23:53:17 GMT+0800 (中国标准时间) 2
//Tue Mar 16 2021 23:53:18 GMT+0800 (中国标准时间) 3
//Tue Mar 16 2021 23:53:19 GMT+0800 (中国标准时间) 4
直接利用await/async、Promise去等待1s然后打印。
# 参考
《JavaScript高级程序设计》
《JavaScript权威指南》
闭包相关的面试题可以参考:
← 模板引擎原理 理解js原型、原型链和继承 →