本文摘录及参考自:
1. 学习Javascript闭包(Closure)
2. 闭包的秘密
3. JavaScript 闭包
4. JavaScript深入之闭包
5. JavaScript深入之执行上下文
5. 闭包-JavasScript
6. 闭包
7. 闭包,懂不懂由你,反正我是懂了
8. 什么是闭包,我的理解
闭包的由来
1.JS变量的作用域
要理解闭包,首先要理解JavaScript的变量作用域。与大多数语言相同,JavaScript变量的作用域分为全局变量和局部变量。函数内部可以访问全局变量,但是函数外部无法访问函数内部的局部变量
例:
function f1(){
let n =100;
var m=99;
console.log(n);
console.log(m);
}
f1(); //输出:100 , 99
console.log(n); //输出:undefined
console.log(m); //输出:undefined
注意,在函数内部声明变量的时候一定要用let或者var。否则,实际上声明了一个全局变量
2. 函数外部如何读取局部变量
要在函数外部读取局部变量,可以通过在函数内部再定义一个函数的方法来实现
例
function f1(){
let n =100;
var m=99;
function f2(){
console.log(n);
console.log(m);
}
return f2;
}
let result =f1();
result(); //输出100,99
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的(子对象会一级一级向上寻找父对象的变量)。 所以,父对象的所有变量,对于子对象都是可见的,反之则不成立。因此,只要返回f2,我们就可以在f1的外部读取到局部变量了(通过f2访问)。
3. 闭包的概念
上一节代码中的f2函数,就是闭包。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部链接起来的一座桥梁。
4. 闭包的用途
闭包有两个非常重要的用途:
- 可以读取函数内部的变量
- 让这些变量的值使用保持在内存中(不被垃圾回收)
例:
function f1(){
var n = 999;
nAdd = function(){n=n+1};
function f2(){
alert(n);
}
return f2;
}
var result = f1();
result(); //输出:999
nAdd();
result(); //输出:1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次值是999,第二次值是1000。 这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。原因在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。此外,这段代码中,nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。
例:
var funArr = []
for(var i=0;i<6;i++){
funArr[i] = function(){
console.log(i);
}
}
funArr.forEach( (f) => f() ) //输出:6,6,6,6,6,6
查看控制台输出结果为 6,6,6,6,6,6。原因是f()函数保存的变量i,在其执行时,已经被赋值为6。可以使用闭包保存变量
var funArr = []
for(var i=0;i<6;i++){
funArr[i] = (function(j){
return function(){
console.log(j);
}
}
)(i)
}
funArr.forEach( (f) => f() ) // 0,1,2,3,4,5
注意,funArr[i]实际上获得的赋值是一个函数 function(){ console.log(j) } ,所以之后调用funArr[i]实际上就是调用console.log(j)。 闭包会持有父方法的局部变量并且不会随父方法销毁而销毁,因此j被一直保留在内存中。
避免使用过多的闭包,可以用let关键词:
var funArr = []
for(var i=0;i<6;i++){
let temp = i;
funArr[i] = function(){
console.log(temp);
}
}
funArr.forEach( (f) => f() ) // 0,1,2,3,4,5
这个例子使用let而不是var,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。
5. 闭包原理解析
要理解JavaScript闭包的原理,首先要先理解它的执行上下文,以下面这段代码为例进行分析
例:
let scope = "global scope";
function checkscope(){
let scope = "local scope";
function f(){
return scope;
}
return f;
}
let foo = checkscope();
console.log(foo()) ; //local scope
简要分析上述代码的执行过程:
- 进行全局代码,创建全局执行上下文,全局执行上文压入执行上下文栈
- 全局执行上下文初始化
- 执行checkscope函数,创建checkscope函数执行上下文,checkscope执行上下文被压入执行上下文栈
- checkscope执行上下文初始化,创建变量对象、作用域链、this等
- checkscope函数执行完毕,checkscop执行上下文从执行上下文栈中弹出
- 执行f函数,创建f函数执行上下文,f执行上下文被压入执行上下文栈
- f执行上下文初始化,创建变量对象、作用域链、this等
- f函数执行完毕,f函数上下文从执行上下文栈中弹出
问题1: 当f函数执行的时候,checkscope函数上下文已经被销毁,f函数为什么还可以读取到checkscope作用域下的scope值?
在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 checkscope() 执行完毕,我们会认为 scope 变量将不能被访问。
然后JavaScript却是可以的
当我们了解了具体的执行过程后,我们知道f执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。总结一下函数的作用域链的规则,函数会把自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以及类推直至全局对象为止。
6. 自检
- 一道非常有意思的考题,如果能清楚的知道为什么输出99和100,对于闭包算是理解清楚了
let nAdd;
let t = () => {
let n = 99;
nAdd = () => {
n++;
};
let t2 = () => {
console.log(n);
};
return t2;
};
let a1 = t();
let a2 = t();
nAdd();
a1(); //99
a2(); //100
当执行let a1= t() 的时候,变量nAdd被赋值为一个函数,该函数为fn1。接着执行let a2=t()的时候,变量nAdd又被重写了,这个函数跟以前的函数长得一摸一样,是fn2。所以当执行nAdd函数的时候,我们执行的其实是fn2,而不是fn1,我们更改的是a2形成的闭包里的n的值,并没有更改a1形成的闭包里的n的值。所以a1()的结果是99, a2()的结果是100.
7. 闭包可能引起的问题
内存泄漏
当闭包造成JavaScript对象之间的循环引用,导致对象无法被垃圾回收机制回收时,就导致了内存泄漏。
例:
function example(){
var element =document.getElementByID("div"); //①
element.onclick = function() {
alert("This is a leak!"); //②
}
}
由于JavaScript对象element引用了一个DOM对象,该DOM对象的onclick属性引用了匿名函数闭包,而闭包可以引用外部函数example()的整个活动对象,包括element,因此形成了dom对象引用js对象,js对象又引用dom对象的闭环。