闭包
红宝书上对闭包的定义:有权访问另外一个函数作用域中变量的函数。
MDN对闭包的定义是:是能够访问自由变量的函数。
自由变量:是指在当前函数中可以使用的(但是既不是arguments也不是本函数定义的局部变量)。
两个点:
- 是个函数
- 能访问另一个函数作用域中的变量,即使外层函数的上下文已经被销毁
就是说我们常见的比如内部函数从外部函数返回这种状态,该内部函数就是闭包。可以看如下特性中的示例!
说明闭包的几个特性:
- 可以访问当前函数以外的变量
-
function outer() { var date = '11月1日'; function inner(str) { console.log(str + date) } return inner('today is ') } outer() function outer() { var date = '11月1日'; return function () { console.log('today is ' + date) }() } outer() // 上下两例均返回“today is 11月1日”
- 即使外部函数已经返回,闭包仍然能够访问外部定义的变量
-
function outer() { var date = '11月1日'; function inner() { console.log('today is ' + date) } return inner } // 以下是拆成分步执行,实际等同于outer()();
// 先执行outer()得到一个返回值inner,此时outer函数执行完毕,跳出outer这个外层函数
// 然后执行inner(),但是此时依然可用outer定义的变量date var getDate = outer() getDate() - 闭包可以修改外部函数的变量的值
-
function outer() { var date = '11月1日'; function inner(newDate) { date = newDate // 将传入的值替换掉外层的date console.log('today is ' + date) } return inner } var getDate = outer() getDate('191101') // “today is 191101”
闭包的作用域链:
以下例分析:
var scope = 'global';
function checkscope() {
var scope = 'local';
function fun() {
return scope;
}
return fun;
}
var check = checkscope();
console.log(check()); // 'local'
执行过程:
- 进入全局代码,创建全局执行上下文并压入执行上下文栈
- 全局上下文初始化
- 执行checkscope函数,创建checkscope执行上下文并将其压入执行栈
- checkscope函数上下文初始化,创建变量对象、作用域链、this
- checkscope函数执行,执行完后checkscope执行上下文从执行栈中弹出
- 由于checkscope函数返回了一个f函数,因此创建f()执行上下文,将fun()的执行上下文压入执行栈,然后执行fun(),同样的,创建变量对象、作用域链、this
- fun()执行完毕后,从执行栈中弹出。
这个流程可以看到,checkscope执行完毕后是带着返回值弹出了执行栈的,在fun执行的时候checkscope函数的上下文已经被销毁了,但是,函数fun执行上下文维护了一个作用域链,结构如下:
funContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
指向关系是:当前作用域 -> checkscope -> 全局,不论checkscope 是否被销毁,fun函数都可以通过fun的作用域链找到它,这是闭包实现的关键!
有关全局环境下函数嵌套与非嵌套时作用域链的指向分析,参考:深入浅出图解作用域链和闭包
一些常见的闭包考题
题1:
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2]();
答案很显然:3、3、3
上面这个题的分析参照我的另一篇博客分析:let和const,里面详细的分析了这个题的过程~
如何才能让这个题输出我们想要的0、1、2呢?
解法一:博客当中给出了使用let的写法,很简单只需要将for中的var替换成let即可
解法二:
还有别的方法吗?本片将采用闭包的方式解决这个问题~~~回想一下闭包的状态是什么?内层函数可以使用外层函数定义的变量呀!
所以第一步:我们在function中return一个新的函数,在内层函数中访问变量 i。
然后考虑我们如何才能把当前的i传到外层function中呢?立刻我们联想到利用参数!
第二步:外层函数自执行,将 i 作为参数传入外层的function中
因此得到如下优化后的代码:
var data = []; for (var i = 0; i < 3; i++) { data[i] = (function (i) { return function () { console.log(i); } })(i) } data[0](); data[1](); data[2]();
结果为:0、1、2
正是我们要的结果啦~
解法三:
这里还可以改成我们常见的定时器写法:
for (var i = 0; i < 3; i++) { (function (i) { setTimeout(function () { console.log(i) }, 100 * i) })(i) }
解法三其实和解法二的本质相同,都是将变量 i 的值复制给外层function的参数 i ,在函数内部又创建一个用于访问 i 的匿名函数,这样每个函数都有一个 i 的副本,就不会相互影响!
题2:这两段代码在checkscope执行完后,f所引用的自由变量scope会被垃圾回收吗?why?
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } var foo = checkscope(); foo();
结论是:第一个代码段中的scope特定时间后会被回收,第二段代码的自由变量不会回收
分析:
现在主流浏览器的垃圾回收算法是:标记清除。当垃圾回收开始时,从root开始寻找这个对象的引用是否可达,也就是找是否存在相互引用,如果引用链断裂,那么这个对象就可以被回收!
对于第一段代码,checkscope()执行完毕后被弹出执行栈,并且也没有其他引用,Root开始查找时不可达,因此闭包引用的自由变量scope过段时间可以被回收
对于第二段代码,由于var foo = checkscope(),checkscope()执行完成后,将foo()执行上下文压入执行栈,foo()指向堆中的自由变量 f ,对于Root来说可达,因此不会被回收!!
如果想要scope一定可以被回收,只要加:foo = null;即可!