我的一个朋友和我正在讨论什么是JS中的闭包,什么不是 . 我们只是想确保我们真正理解它 .
我们来看看这个例子吧 . 我们有一个计数循环,并希望在控制台上打印计数器变量延迟 . 因此,我们使用 setTimeout
和 closures 来捕获计数器变量的值,以确保它不会打印N倍N值 .
没有 closures 或接近 closures 的错误解决方案将是:
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
这当然会在循环后打印10倍 i
的值,即10 .
So his attempt was:
for(var i = 0; i < 10; i++) {
(function(){
var i2 = i;
setTimeout(function(){
console.log(i2);
}, 1000)
})();
}
按预期打印0到9 .
我告诉他,他并没有使用 closure 捕获 i
,但他坚持认为他是 . 我证明他没有使用 closures 将for循环体放在另一个 setTimeout
(将他的匿名函数传递给 setTimeout
),再次打印10次10 . 如果我将他的函数存储在 var
并在循环之后执行它,同样打印10次10,这同样适用 . 所以我的论点是 he doesn't really capture the value of i ,使他的版本不是一个闭包 .
My attempt was:
for(var i = 0; i < 10; i++) {
setTimeout((function(i2){
return function() {
console.log(i2);
}
})(i), 1000);
}
所以我捕获 i
(在闭包中命名为 i2
),但现在我返回另一个函数并传递它 . In my case, the function passed to setTimeout really captures i.
Now who is using closures and who isn't?
请注意,两个解决方案在控制台上打印0到9都会延迟,因此它们解决了原始问题,但我们想要了解这两个解决方案中的哪一个 uses closures 来实现此目的 .
12 回答
我想分享我的例子和关于闭包的解释 . 我做了一个python示例,并用两个数字来演示堆栈状态 .
此代码的输出如下:
下面是两个显示堆栈和附加到函数对象的闭包的图 .
when the function is returned from maker
when the function is called later
当通过参数或非局部变量调用函数时,代码需要局部变量绑定,例如margin_top,padding以及a,b,n . 为了确保函数代码能够工作,很久以前就已经消失的制造商函数的堆栈框架应该是可访问的,这在我们可以找到的函数消息对象的闭包中备份 .
简而言之 Javascript Closures 允许函数 access a variable ,即 declared in a lexical-parent function .
让我们看一个更详细的解释 . 要理解闭包,了解JavaScript如何定义变量非常重要 .
Scopes
在JavaScript中,范围是使用函数定义的 . 每个函数都定义了一个新范围 .
考虑以下示例;
调用f打印
现在让我们考虑一下我们在另一个函数
f
中定义函数g
的情况 .我们将 lexical parent 的 lexical parent 称为 lexical parent . 如前所述,我们现在有两个范围;范围
f
和范围g
.但是一个范围是“在”另一个范围内,那么子函数范围是父函数范围的一部分吗?在父函数范围内声明的变量会发生什么?我能从子功能的范围访问它们吗?这正是关闭步骤的地方 .
Closures
在JavaScript中,函数
g
不仅可以访问在范围g
中声明的任何变量,还可以访问在父函数f
范围内声明的任何变量 .考虑以下;
调用f打印
我们来看看
console.log(foo);
这一行 . 此时我们在范围g
中,我们尝试访问在范围f
中声明的变量foo
. 但是如前所述,我们可以访问词法父函数中声明的任何变量,这是这里的情况;g
是f
的词汇父级 . 因此打印hello
.我们现在看看
console.log(bar);
行 . 此时,我们在范围f
中,并且我们尝试访问在范围g
中声明的变量bar
.bar
未在当前范围内声明,函数g
不是f
的父级,因此bar
未定义实际上我们也可以访问在词法"grand parent"函数范围内声明的变量 . 因此,如果在函数
g
中定义函数h
然后
h
将能够访问在函数h
,g
和f
范围内声明的所有变量 . 这是通过 closures 完成的 . 在JavaScript中 closures 允许我们访问词法父函数,词法祖父函数,词汇祖父函数等中声明的任何变量 . 这可以看作 scope chain ;scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ...
直到最后一个没有词法父项的父函数 .The window object
实际上链不会停在最后一个父函数 . 还有一个特殊的范围; global scope . 未在函数中声明的每个变量都被视为在全局范围内声明 . 全球范围有两个专业;
声明的每个变量全球范围可以访问 everywhere
在全局范围内声明的变量对应于
window
对象的属性 .因此,在全局范围内有两种方式声明变量
foo
;通过不在函数中声明它或通过设置窗口对象的属性foo
.Both attempts uses closures
现在你已经阅读了更详细的解释,现在很明显两个解决方案都使用了闭包 . 但可以肯定的是,让我们来证明一下 .
让我们创建一种新的编程语言; JavaScript的无闭幕 . 顾名思义,JavaScript-No-Closure与JavaScript完全相同,只是它不支持闭包 .
换一种说法;
好吧,让我们看看使用JavaScript-No-Closure的第一个解决方案会发生什么 .
因此,这将在JavaScript-No-Closure中打印
undefined
10次 .因此第一种解决方案使用闭包 .
让我们看看第二个解决方案;
因此,这将在JavaScript-No-Closure中打印
undefined
10次 .两种解决方案都使用闭包 .
编辑:假设这3个代码片段未在全局范围内定义 . 否则变量
foo
和i
将绑定到window
对象,因此可以通过JavaScript和JavaScript-No-Closure中的window
对象访问 .Editor's Note: JavaScript中的所有函数都是闭包,如post中所述 . 然而,我们只想从理论的角度来识别这些函数的子集,这些函数是interesting . 此后,除非另有说明,否则对闭包一词的任何引用都将指代该功能子集 .
闭包的简单解释:
采取功能 . 我们称之为F.
列出F的所有变量 .
变量可以有两种类型:
局部变量(绑定变量)
非局部变量(自由变量)
如果F没有自由变量,则它不能是闭包 .
如果F有任何自由变量(在F的 a 父范围中定义),则:
必须只有一个F的父作用域, a 自由变量才能绑定到该作用域 .
如果F是来自 that 父范围之外的 referenced ,那么它将成为 that 自由变量的闭包 .
That 自由变量被称为闭包F的upvalue .
现在让我们用它来确定谁使用闭包,谁不使用闭包(为了解释我已经命名了函数):
Case 1: Your Friend's Program
在上面的程序中有两个函数:
f
和g
. 让我们看看它们是否是闭包:对于
f
:列出变量:
i2
是 local 变量 .i
是 free 变量 .setTimeout
是 free 变量 .g
是 local 变量 .console
是 free 变量 .查找每个自由变量绑定到的父作用域:
i
是 bound 到全球范围 .setTimeout
是 bound 到全球范围 .console
是 bound 到全球范围 .在哪个范围是函数 referenced ? global scope .
因此
i
不是 closed over byf
.因此
setTimeout
不是 closed over byf
.因此
console
不是 closed over byf
.因此函数
f
不是闭包 .对于
g
:列出变量:
console
是 free 变量 .i2
是 free 变量 .查找每个自由变量绑定到的父作用域:
console
是 bound 到全球范围 .i2
是 bound 到f
的范围 .在哪个范围是函数 referenced ? scope of setTimeout .
因此
console
不是 closed over byg
.因此
i2
是 closed over byg
.因此函数
g
是自由变量i2
的闭包(这是g
的upvalue) whensetTimeout
来自setTimeout
.对你不好:你的朋友正在使用关闭 . 内部函数是一个闭包 .
Case 2: Your Program
在上面的程序中有两个函数:
f
和g
. 让我们看看它们是否是闭包:对于
f
:列出变量:
i2
是 local 变量 .g
是 local 变量 .console
是 free 变量 .查找每个自由变量绑定到的父作用域:
console
是 bound 到全球范围 .在哪个范围是函数 referenced ? global scope .
因此
console
不是 closed overf
.因此函数
f
不是闭包 .对于
g
:列出变量:
console
是 free 变量 .i2
是 free 变量 .查找每个自由变量绑定到的父作用域:
console
是 bound 到全局范围 .i2
是范围的 boundf
在哪个范围是函数 referenced ? scope of setTimeout .
因此
console
不是 closed overg
.因此
i2
是 closed over byg
.因此函数
g
是一个自由变量i2
的闭包(这是g
的一个upvalue) when 它是setTimeout
setTimeout
.对你有好处:你正在使用一个封闭物 . 内部函数是一个闭包 .
所以你和你的朋友都在使用闭包 . 停止争论 . 我希望我清除了闭包的概念以及如何为你们两个识别它们 .
Edit: 关于为什么所有函数都关闭的简单解释(信用@Peter):
首先让's consider the following program (it' s为control):
我们知道
lexicalScope
和regularFunction
都不是闭包 from the above definition .当我们执行程序 we expect
message
被警告时 becauseregularFunction
不是一个闭包(即它有权访问其父范围内的变量 all - 包括message
) .当我们执行程序 we observe 时,
message
确实被警告了 .接下来让's consider the following program (it'是alternative):
我们知道只有
closureFunction
是一个闭包 from the above definition .当我们执行程序 we expect
message
没有被提醒时 becauseclosureFunction
是一个闭包(即它只能访问 the time the function is created 的所有 non-local variables (see this answer) - 这不包括message
) .当我们执行程序 we observe 时,
message
实际上正在被警告 .我们从中推断出什么?
JavaScript解释器不会将闭包与处理其他函数的方式区别对待 .
每个函数都带有scope chain . 闭包没有 separate 引用环境 .
闭包和其他所有函数一样 . 我们只是在 referenced 范围 outside 中调用它们闭包它们所属的范围 because 这是一个有趣的案例 .
你们都在使用闭包 .
我要去Wikipedia definition这里:
您朋友的尝试通过获取其值并将副本存储到本地
i2
,明确使用非本地变量i
.您自己的尝试将
i
(在调用站点的范围内)传递给匿名函数作为参数 . 到目前为止,这不是一个闭包,但是该函数返回另一个引用相同i2
的函数 . 由于内部匿名函数i2
不是本地函数,因此会创建一个闭包 .你和你的朋友都使用闭包:
在你朋友的代码函数
function(){ console.log(i2); }
中定义了匿名函数function(){ var i2 = i; ...
的闭包,并且可以读/写局部变量i2
.在你的代码函数
function(){ console.log(i2); }
中定义了函数function(i2){ return ...
的闭包,并且可以读/写本地有 Value 的i2
(在本例中声明为参数) .在这两种情况下,函数
function(){ console.log(i2); }
然后传递到setTimeout
.另一个等价物(但内存利用率较低)是:
Closure
闭包不是函数,也不是表达式 . 它必须被视为函数范围外使用的变量的一种“快照”,并在函数内部使用 . 在语法上,人们应该说:“关闭变量” .
换句话说,闭包是:闭包是函数所依赖的变量的相关上下文的副本 .
再一次(naïf):一个闭包可以访问未作为参数传递的变量 .
请记住,这些功能概念在很大程度上取决于您使用的编程语言/环境 . 在JavaScript中,闭包取决于词法范围(在大多数c语言中都是如此) .
因此,返回一个函数主要是返回一个匿名/未命名的函数 . 当函数访问变量时,未作为参数传递,并且在其(词法)范围内,已经采用了闭包 .
所以,关于你的例子:
所有人都在使用闭包 . 不要将执行点与闭包混淆 . 如果在错误的时刻采取闭包的“快照”,则值可能是意外的,但肯定会关闭!
我从来没有对任何人解释这一点的方式感到满意 .
理解闭包的关键是理解没有闭包的JS会是什么样的 .
Without closures, this would throw an error
一旦outerFunc以假想的关闭禁用版本的JavaScript返回,对outerVar的引用将被垃圾收集并且不再留下任何内容以供引用的内部函数 .
闭包本质上是特殊规则,当内部函数引用外部函数的变量时,这些规则可以使这些变量存在 . 对于闭包,即使在外部函数完成后也会保持引用的变量,如果这有助于您记住该点,则将其“关闭” .
即使使用闭包,在没有引用其本地的内部函数的函数中的局部变量的生命周期与无闭包版本中的相同 . 功能完成后,本地人会收集垃圾 .
一旦你有了在内部函数中引用外部var,但它就像一个doorjamb被置于那些引用变量的垃圾收集方式中 .
查看闭包的一种更准确的方法是,内部函数基本上使用内部作用域作为其自己的作用域 .
But the context referenced is in fact, persistent, not like a snapshot. 反复触发返回的内部函数,该函数继续递增并记录外部函数的局部变量将继续警告更高的值 .
仔细检查后,看起来你们两个都在使用关闭 .
在你的朋友案例中,
i
在匿名函数1中被访问,而i2
在匿名函数2中被访问,其中console.log
存在 .在您的情况下,您正在匿名函数中访问
i2
,其中存在console.log
. 在console.log
之前添加debugger;
语句,在"Scope variables"下的chrome开发人员工具中添加debugger;
语句,它将告知变量的范围 .考虑以下 . 这将创建并重新创建一个在
i
上关闭但不同的函数f
!:而以下关闭"a"函数"itself"
(他们自己!之后使用单个指示物的片段
f
)或者更明确:
NB . 在打印
0
之前,f
的最后一个定义是function(){ console.log(9) }
.警告!封闭概念可能是对初级编程本质的强制分心:
X-裁判:
How do JavaScript closures work?
Javascript Closures Explanation
Does a (JS) Closure Require a Function Inside a Function
How to understand closures in Javascript?
Javascript local and global variable confusion
我刚才写了这篇文章,提醒自己一个闭包是什么以及它在JS中是如何工作的 .
闭包是一个函数,当被调用时,它使用声明它的作用域,而不是它被调用的作用域 . 在javaScript中,所有函数都表现得像这样 . 只要存在仍指向它们的函数,范围中的变量值就会持续存在 . 规则的例外是'this',它指的是函数在调用时所在的对象 .
让我们看看两种方式:
声明并立即执行在其自己的上下文中运行
setTimeout()
的匿名函数 . 通过首先复制到i2
来保留i
的当前值;它的作用是因为立即执行 .声明内部函数的执行上下文,其中
i
的当前值保留在i2
中;此方法还使用立即执行来保留值 .Important
应该提到的是,两种方法之间的运行语义并不相同;你的内部函数传递给
setTimeout()
,而他的内部函数本身调用setTimeout()
.将这两个代码包装在另一个
setTimeout()
并不是一回事就开始了 .Conclusion
这两种方法都使用闭合,因此它归结为个人品味;第二种方法更容易“移动”或概括 .
根据
closure
定义:如果定义了一个使用在函数外部定义的变量的函数,则使用
closure
. (我们将变量称为 free variable ) .他们都使用
closure
(即使在第一个例子中) .