写JS时遇到个很诡异的问题,发现JS的闭包跟C#中的闭包有差异。。不多说,看代码。。
C#版本
List<Action> list = new List<Action>();
for (int i = 0; i < 10; i++)
{
var temp = i;
list.Add(() =>
{
Console.WriteLine(temp);
});
}
list.ForEach((x) => x());
不用多说,输出1,2,3,4,5。。
Console.WriteLine(temp)时,当前作用域找不到temp。。去上一级找,找到var temp。。因为i是值类型,var temp=i时temp已经是在一个新的实例了,存的是i的值。到此已经跟i没半毛钱关系了。。到这步就完结了。。所以结果是1,2,3,4,5。。。9
下面是JS版本
$(function () {
var array = [];
for (var i = 0; i < 10; i++) {
var temp = i;
array.push(function () {
console.log(temp);
});
}
array.forEach(function (item) {
item();
});
})
得出来的是9.9.9.9.9。。。
我就日了。。怎么可能?难道js中temp存的是i的引用?i明明是值类型。。我真的很费解
------解决思路----------------------
c# 的匿名委托机制比较复杂(vb.net等等也同样匿名委托),绝非 javascript 可比的。
javascript 是非常简单、非常低级的语言(还另外一种说法就是“是一种非常自由、随便解释的语言”)。它把 var 定义任意地提到function 的开头去执行,而且它也从来不自动封装匿名委托,更不会像 c# 那也能够因为匿名委托中引用的外部变量需要是否需要共享而产生不同的代码。
你这里的问题,正好相反。你用一个 javascript 代码来理解“闭包”,包括你的“如果找不到temp变量作用域就向上一级查找”的说法都是javascript的这种var的概念,而不是c#/.net本身的“作用域”概念。
------解决思路----------------------
js 因为没有完善的词法定界,它的 var 变量作用域是 function 级别的,不是 for 块(block)级别的,所以是那个结果(引用了同一变量)。ES6 标准加入了 let 关键字,可以定义 block 级别的变量,就可以使用直觉上更容易理解的闭包了。
C# 情况比较特殊,它词法上 for 的循环变量是 block 级别的,然而在 IL 实现层面上并不是(也就是并没有在每次循环迭代时创建新的变量,实际还是属于循环之外)。所以不能直接捕获 i,否则也和 js 一个结果,所以必须定义新的变量用来捕获。而 foreach 的循环变量就是真正属于 block 了(它实际上每次取枚举器的 Current 属性),就可以直接捕获循环变量。