提问人:leemes 提问时间:10/17/2012 最后编辑:Azeezah Mleemes 更新时间:8/20/2020 访问量:120907
JavaScript 闭包与匿名函数
JavaScript closures vs. anonymous functions
问:
我和我的一个朋友目前正在讨论JS中什么是闭包,什么不是。我们只是想确保我们真正正确地理解它。
让我们举个例子。我们有一个计数循环,并希望延迟在控制台上打印计数器变量。因此,我们使用 和 闭包来捕获计数器变量的值,以确保它不会打印值 N 乘以 N。setTimeout
没有闭包或任何接近闭包的错误解决方案是:
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
当然,这将打印 After the Loop 值的 10 倍,即 10。i
所以他的尝试是:
for(var i = 0; i < 10; i++) {
(function(){
var i2 = i;
setTimeout(function(){
console.log(i2);
}, 1000)
})();
}
按预期打印 0 到 9。
我告诉他,他没有使用闭合来捕捉,但他坚持认为他是。我通过将 for 循环体放在另一个 (将他的匿名函数传递给 ) 来证明他不使用闭包,再次打印 10 乘以 10。如果我将他的函数存储在 a 中并在循环后执行它,同样适用,也打印 10 乘以 10。所以我的论点是,他并没有真正捕捉到 i
的价值,因此他的版本不是一个闭包。i
setTimeout
setTimeout
var
我的尝试是:
for(var i = 0; i < 10; i++) {
setTimeout((function(i2){
return function() {
console.log(i2);
}
})(i), 1000);
}
所以我捕获(在闭包中命名),但现在我返回另一个函数并传递它。就我而言,传递给 setTimeout 的函数确实捕获了 i
。i
i2
现在谁在使用闭包,谁没有?
请注意,两种解决方案在控制台上打印 0 到 9 都是延迟的,因此它们解决了原始问题,但我们想了解这两个解决方案中的哪一个使用闭包来实现此目的。
答:
仔细检查后,看起来你们俩都在使用闭合。
在您的朋友案例中,在匿名函数 1 中访问,并在存在 的匿名函数 2 中访问。i
i2
console.log
在您的情况下,您正在访问存在的匿名功能内部。在 chrome 开发者工具的“范围变量”下添加一条语句,它会告诉变量的作用域。i2
console.log
debugger;
console.log
评论
根据定义:closure
“闭包”是一个表达式(通常是一个函数),它可以将自由变量与绑定这些变量的环境(“关闭”表达式)放在一起。
如果您定义一个函数,该函数使用在函数外部定义的变量,则使用。(我们称该变量为自由变量)。
他们都使用(即使在第一个示例中)。closure
closure
评论
i2
你和你的朋友都使用闭包:
闭包是一种特殊的对象,它结合了两件事:函数和创建该函数的环境。环境由创建闭包时范围内的任何局部变量组成。
MDN:https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures
在你朋友的代码函数中定义了匿名函数的闭包,可以读/写局部变量 i2
。function(){ console.log(i2); }
function(){ var i2 = i; ...
在您的代码函数中定义了函数的闭包,并且可以读/写本地有价值的 i2
(在本例中声明为参数)。function(){ console.log(i2); }
function(i2){ return ...
在这两种情况下,函数都会传入 .function(){ console.log(i2); }
setTimeout
另一个等效项(但内存利用率较低)是:
function fGenerator(i2){
return function(){
console.log(i2);
}
}
for(var i = 0; i < 10; i++) {
setTimeout(fGenerator(i), 1000);
}
评论
你们俩都在使用闭包。
我在这里使用维基百科的定义:
在计算机科学中,闭包(也称为词法闭包或函数 closure) 是一个函数或对函数的引用,以及 引用环境 - 存储对每个 该函数的非局部变量(也称为自由变量)。 与普通函数指针不同,闭包允许函数访问 这些非局部变量,即使在其直接之外调用时也是如此 词法范围。
你的朋友的尝试显然使用了变量 ,它是非局部的,通过获取它的值并制作一个副本来存储到本地 .i
i2
您自己的尝试将(在调用站点的范围内)作为参数传递给匿名函数。到目前为止,这不是一个闭包,但随后该函数返回另一个引用相同 .由于内部匿名函数内部不是本地函数,因此会创建一个闭包。i
i2
i2
评论
i
i2
i
i
setTimeout
setTimeout
让我们看看这两种方式:
(function(){
var i2 = i;
setTimeout(function(){
console.log(i2);
}, 1000)
})();
声明并立即执行在其自己的上下文中运行的匿名函数。通过将 的当前值复制到 first 来保留;它之所以有效,是因为立即执行。setTimeout()
i
i2
setTimeout((function(i2){
return function() {
console.log(i2);
}
})(i), 1000);
声明内部函数的执行上下文,其中当前值 的保留在 ;此方法还使用立即执行来保留值。i
i2
重要
应该提到的是,两种方法之间的运行语义并不相同;你的内在功能被传递给他,而他的内在功能则调用自己。setTimeout()
setTimeout()
将两个代码包装在另一个代码中并不能证明只有第二种方法使用闭包,只是一开始就不是一回事。setTimeout()
结论
这两种方法都使用闭合,因此归结为个人品味;第二种方法更容易“移动”或概括。
评论
setTimeout()
i
()
10
()
(i)
简而言之,Javascript 闭包允许函数访问在词法父函数中声明的变量。
让我们看看更详细的解释。 要理解闭包,了解 JavaScript 如何限定变量范围非常重要。
范围
在 JavaScript 中,作用域是用函数定义的。 每个函数都定义了一个新的范围。
请看下面的例子;
function f()
{//begin of scope f
var foo='hello'; //foo is declared in scope f
for(var i=0;i<2;i++){//i is declared in scope f
//the for loop is not a function, therefore we are still in scope f
var bar = 'Am I accessible?';//bar is declared in scope f
console.log(foo);
}
console.log(i);
console.log(bar);
}//end of scope f
调用 F 打印
hello
hello
2
Am I Accessible?
现在让我们考虑一下我们在另一个函数中定义了一个函数的情况。g
f
function f()
{//begin of scope f
function g()
{//being of scope g
/*...*/
}//end of scope g
/*...*/
}//end of scope f
我们将 的词法父级称为 。
如前所述,我们现在有 2 个范围;范围和范围.f
g
f
g
但是一个作用域“在”另一个作用域内,那么子函数的作用域是父函数作用域的一部分吗?在父函数的作用域中声明的变量会发生什么情况;我是否能够从子函数的范围内访问它们? 这正是关闭介入的地方。
闭 包
在JavaScript中,该函数不仅可以访问在作用域中声明的任何变量,还可以访问在父函数的作用域中声明的任何变量。g
g
f
考虑以下;
function f()//lexical parent function
{//begin of scope f
var foo='hello'; //foo declared in scope f
function g()
{//being of scope g
var bar='bla'; //bar declared in scope g
console.log(foo);
}//end of scope g
g();
console.log(bar);
}//end of scope f
调用 F 打印
hello
undefined
让我们看一下这条线.此时,我们在范围内,我们尝试访问在范围内声明的变量。但如前所述,我们可以访问在词法父函数中声明的任何变量,这里就是这种情况; 是 的词法父级。因此被打印出来。
现在让我们看一下这条线。此时,我们在范围内,我们尝试访问在范围内声明的变量。 未在当前作用域中声明,并且该函数不是 的父函数,因此未定义console.log(foo);
g
foo
f
g
f
hello
console.log(bar);
f
bar
g
bar
g
f
bar
实际上,我们还可以访问在词法“大父级”函数范围内声明的变量。因此,如果函数中定义了一个函数h
g
function f()
{//begin of scope f
function g()
{//being of scope g
function h()
{//being of scope h
/*...*/
}//end of scope h
/*...*/
}//end of scope g
/*...*/
}//end of scope f
然后将能够访问函数 、 和 的作用域中声明的所有变量。这是通过闭包完成的。在 JavaScript 闭包中,我们允许访问在词法父函数、词法父函数、词法父函数等中声明的任何变量。
这可以看作是一个范围链; 直到最后一个没有词法父函数的父函数。h
h
g
f
scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ...
window 对象
实际上,链不会在最后一个父函数处停止。还有一个特殊的范围;全局范围。未在函数中声明的每个变量都被视为在全局作用域中声明。全球范围有两个专业;
- 在全局范围内声明的每个变量都可以在任何地方访问
- 在全局作用域中声明的变量对应于对象的属性。
window
因此,在全局范围内声明变量的方法正好有两种;要么不在函数中声明它,要么设置 window 对象的属性。foo
foo
两次尝试都使用闭包
现在,您已经阅读了更详细的说明,现在很明显,这两种解决方案都使用闭包。 但可以肯定的是,让我们做一个证明。
让我们创建一种新的编程语言;JavaScript-no-closure。 顾名思义,JavaScript-No-Closure 与 JavaScript 相同,只是它不支持 Closures。
换句话说;
var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello
好了,让我们看看第一个使用 JavaScript-No-Closure 的解决方案会发生什么;
for(var i = 0; i < 10; i++) {
(function(){
var i2 = i;
setTimeout(function(){
console.log(i2); //i2 is undefined in JavaScript-No-Closure
}, 1000)
})();
}
因此,这将在 JavaScript-No-Closure 中打印 10 次。undefined
因此,第一种解决方案使用闭包。
让我们看一下第二种解决方案;
for(var i = 0; i < 10; i++) {
setTimeout((function(i2){
return function() {
console.log(i2); //i2 is undefined in JavaScript-No-Closure
}
})(i), 1000);
}
因此,这将在 JavaScript-No-Closure 中打印 10 次。undefined
这两种解决方案都使用闭包。
编辑:假设这 3 个代码片段未在全局范围内定义。否则,变量 和 将绑定到对象,因此可以通过 JavaScript 和 JavaScript-No-Closure 中的对象访问。foo
i
window
window
评论
i
i2
i
i
关闭
闭包不是一个函数,也不是一个表达式。它必须被看作是函数范围之外使用的变量的一种“快照”,并在函数内部使用。从语法上讲,应该说:“取变量的闭包”。
同样,换句话说:闭包是函数所依赖的变量的相关上下文的副本。
再一次(幼稚):闭包可以访问未作为参数传递的变量。
请记住,这些功能概念很大程度上取决于您使用的编程语言/环境。在 JavaScript 中,闭包取决于词法范围(在大多数 C 语言中都是如此)。
因此,返回函数主要是返回匿名/未命名的函数。当函数访问变量(未作为参数传递)且在其(词法)范围内时,已采取闭包。
所以,关于你的例子:
// 1
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // closure, only when loop finishes within 1000 ms,
}, 1000); // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
(function(){
var i2 = i; // closure of i (lexical scope: for-loop)
setTimeout(function(){
console.log(i2); // closure of i2 (lexical scope:outer function)
}, 1000)
})();
}
// 3
for(var i = 0; i < 10; i++) {
setTimeout((function(i2){
return function() {
console.log(i2); // closure of i2 (outer scope)
}
})(i), 1000); // param access i (no closure)
}
所有这些都使用闭包。不要将执行点与闭包混淆。如果在错误的时间拍摄了闭包的“快照”,则值可能是意想不到的,但肯定会采取闭包!
编者按:JavaScript 中的所有函数都是闭包,如本文所述。然而,我们只对识别这些函数的一个子集感兴趣,从理论的角度来看,这些函数很有趣。此后,除非另有说明,否则对“闭包”一词的任何引用都将指此函数子集。
闭包的简单解释:
- 以函数为例。我们称之为 F。
- 列出 F 的所有变量。
- 变量可以有两种类型:
- 局部变量(绑定变量)
- 非局部变量(自由变量)
- 如果 F 没有自由变量,那么它就不能是闭包。
- 如果 F 有任何自由变量(在 F 的父作用域中定义),则:
- 自由变量必须只绑定到一个 F 的父作用域。
- 如果 F 是从该父作用域之外引用的,则它将成为该自由变量的闭包。
- 该自由变量称为闭包 F 的上值。
现在让我们用它来弄清楚谁使用闭包,谁不使用闭包(为了解释,我命名了这些函数):
案例 1:您朋友的计划
for (var i = 0; i < 10; i++) {
(function f() {
var i2 = i;
setTimeout(function g() {
console.log(i2);
}, 1000);
})();
}
在上面的程序中,有两个函数:和 。让我们看看它们是否是闭包:f
g
为:f
- 列出变量:
i2
是一个局部变量。i
是一个自由变量。setTimeout
是一个自由变量。g
是一个局部变量。console
是一个自由变量。
- 查找每个自由变量绑定到的父作用域:
i
绑定到全局范围。setTimeout
绑定到全局范围。console
绑定到全局范围。
- 该函数在哪个作用域中引用?全局范围。
- 因此没有被关闭。
i
f
- 因此没有被关闭。
setTimeout
f
- 因此没有被关闭。
console
f
- 因此没有被关闭。
因此,该函数不是闭包。f
为:g
- 列出变量:
console
是一个自由变量。i2
是一个自由变量。
- 查找每个自由变量绑定到的父作用域:
console
绑定到全局范围。i2
绑定到 的范围。f
- 该函数在哪个作用域中引用?
setTimeout
的作用域。- 因此没有被关闭。
console
g
- 因此被 关闭。
i2
g
- 因此没有被关闭。
因此,当自由变量从内部引用时,该函数是自由变量的闭包(它是 的上值)。g
i2
g
setTimeout
对你不利:您的朋友正在使用闭包。内部功能是闭合。
案例 2:您的程序
for (var i = 0; i < 10; i++) {
setTimeout((function f(i2) {
return function g() {
console.log(i2);
};
})(i), 1000);
}
在上面的程序中,有两个函数:和 。让我们看看它们是否是闭包:f
g
为:f
- 列出变量:
i2
是一个局部变量。g
是一个局部变量。console
是一个自由变量。
- 查找每个自由变量绑定到的父作用域:
console
绑定到全局范围。
- 该函数在哪个作用域中引用?全局范围。
- 因此没有被关闭。
console
f
- 因此没有被关闭。
因此,该函数不是闭包。f
为:g
- 列出变量:
console
是一个自由变量。i2
是一个自由变量。
- 查找每个自由变量绑定到的父作用域:
console
绑定到全局范围。i2
绑定到 的范围。f
- 该函数在哪个作用域中引用?
setTimeout
的作用域。- 因此没有被关闭。
console
g
- 因此被 关闭。
i2
g
- 因此没有被关闭。
因此,当自由变量从内部引用时,该函数是自由变量的闭包(它是 的上值)。g
i2
g
setTimeout
对你有好处:您正在使用闭包。内部功能是闭合。
所以你和你的朋友都在使用闭包。别再吵了。我希望我清除了闭包的概念以及如何为你们俩识别它们。
编辑:关于为什么所有函数都闭包的简单解释(@Peter):
首先,让我们考虑以下程序(它是控件):
lexicalScope();
function lexicalScope() {
var message = "This is the control. You should be able to see this message being alerted.";
regularFunction();
function regularFunction() {
alert(eval("message"));
}
}
- 我们知道,两者都不是上述定义的闭包。
lexicalScope
regularFunction
- 当我们执行程序时,我们希望收到警报,因为它不是闭包(即它可以访问其父范围内的所有变量 - 包括 )。
message
regularFunction
message
- 当我们执行程序时,我们观察到它确实被警告了。
message
接下来,让我们考虑以下程序(这是替代方案):
var closureFunction = lexicalScope();
closureFunction();
function lexicalScope() {
var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";
return function closureFunction() {
alert(eval("message"));
};
}
- 我们知道,这只是上述定义的一个闭合。
closureFunction
- 当我们执行程序时,我们希望不会收到警报,因为是一个闭包(即它只能在创建函数时访问其所有非局部变量(请参阅此答案) - 这不包括 )。
message
closureFunction
message
- 当我们执行程序时,我们观察到它实际上被警告了。
message
我们从中推断出什么?
- JavaScript 解释器处理闭包的方式与处理其他函数的方式不同。
- 每个函数都带有其作用域链。闭包没有单独的引用环境。
- 闭包就像其他所有函数一样。当它们在它们所属范围之外的范围内被引用时,我们只称它们为闭包,因为这是一个有趣的情况。
评论
g
setTimeout
f
我从来不对任何人解释这一点的方式感到满意。
理解闭包的关键是了解没有闭包的 JS 会是什么样子。
如果没有闭包,这将引发错误
function outerFunc(){
var outerVar = 'an outerFunc var';
return function(){
alert(outerVar);
}
}
outerFunc()(); //returns inner function and fires it
一旦 outerFunc 返回了一个虚构的闭包禁用版本的 JavaScript,对 outerVar 的引用将被垃圾收集并消失,不留下任何内容供内部函数引用。
闭包本质上是一种特殊的规则,当内部函数引用外部函数的变量时,这些变量可能会存在。使用闭包时,即使在外部函数完成或“闭包”(如果这有助于记住要点)之后,引用的变量也会保持不变。
即使使用闭包,在没有引用其局部函数的函数的函数中,本地变量的生命周期也与无闭包版本中的生命周期相同。当函数完成时,当地人会收集垃圾。
然而,一旦你在内部函数中引用了外部变量,就像门框被放在了那些引用的变量的垃圾收集中一样。
一个也许更准确的方法来看待闭包,是内部函数基本上使用内部作用域作为它自己的作用域基础。
但引用的上下文实际上是持久的,不像快照。反复触发返回的内部函数,该函数不断递增并记录外部函数的本地 var 将不断提醒更高的值。
function outerFunc(){
var incrementMe = 0;
return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2
评论
我不久前写了这篇文章,以提醒自己什么是闭包以及它在 JS 中是如何工作的。
闭包是一个函数,在调用它时,它使用声明它的范围,而不是调用它的范围。在 javaScript 中,所有函数的行为都是这样的。只要存在指向变量值的函数,作用域中的变量值就会持续存在。该规则的例外是“this”,它指的是调用函数时函数所在的对象。
var z = 1;
function x(){
var z = 2;
y(function(){
alert(z);
});
}
function y(f){
var z = 3;
f();
}
x(); //alerts '2'
请考虑以下几点。
这将创建并重新创建一个函数,该函数关闭于 ,但不同的函数!f
i
i=100;
f=function(i){return function(){return ++i}}(0);
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));
f=function(i){return new Function('return ++i')}(0); /* function declarations ~= expressions! */
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));
而以下内容在“a”函数“本身”
( 他们自己! 之后的片段使用单个指称f
)
for(var i = 0; i < 10; i++) {
setTimeout( new Function('console.log('+i+')'), 1000 );
}
或者更明确地说:
for(var i = 0; i < 10; i++) {
console.log( f = new Function( 'console.log('+i+')' ) );
setTimeout( f, 1000 );
}
铌。打印 is before 的最后一个定义。f
function(){ console.log(9) }
0
警告!闭包概念可能会强制分散对基本编程本质的注意力:
for(var i = 0; i < 10; i++) { setTimeout( 'console.log('+i+')', 1000 ); }
x-refs.:
JavaScript 闭包是如何工作的?
Javascript 闭包解释
(JS) 闭包是否需要函数内部的函数
如何理解 Javascript 中的闭包?
Javascript 局部变量和全局变量混淆
评论
Run' only was desired - not sure how to remove the
我想分享我的例子和关于闭包的解释。我做了一个 python 示例,以及两个图来演示堆栈状态。
def maker(a, b, n):
margin_top = 2
padding = 4
def message(msg):
print('\n’ * margin_top, a * n,
' ‘ * padding, msg, ' ‘ * padding, b * n)
return message
f = maker('*', '#', 5)
g = maker('', '♥’, 3)
…
f('hello')
g(‘good bye!')
此代码的输出如下所示:
***** hello #####
good bye! ♥♥♥
下面是两个图,分别显示堆栈和附加到函数对象的闭包。
当通过参数或非局部变量调用函数时,代码需要局部变量绑定,例如 margin_top、填充以及 a、b、n。为了保证函数代码的工作,应该可以访问很久以前消失的 maker 函数的堆栈帧,该帧与函数消息对象一起备份在闭包中。
评论
delete
评论