本文是翻译此文

预先阅读此文:闭合循环变量时被认为有害的(closing over the loop variable considered harmful)

JavaScript也有同样的问题。考虑:

function hookupevents() {
for (var i = 0; i < 4; i++) {
document.getElementById("myButton" + i)
.addEventListener("click",
function() { alert(i); });
}
}

当你在一个循环中涉及event handler时,你就会遇到这样的代码,这是最常见的问题。因此,我用这个问题作为例子。无论你点击哪一个button,他们都显示4,而不是相应的button 号码。在预先阅读链接中给出了原因:你闭合的是循环变量,因此,在函数真正执行时,变量i的值是4,因为循环在此已经结束了。麻烦的是修复这个问题。在C#中,你可以复制这个值给一个在这个作用域中的局部变量并捕获这个局部变量,但是在JavaScript中行不通:

function hookupevents() {
for (var i = 0; i < 4; i++) {
var j = i;//添加一个变量
document.getElementById("myButton" + i)
.addEventListener("click",
function() { alert(j); });
}
}

现在,点击按钮将显示3而不是4。原因是JavaScript变量的作用域是函数作用域,而不是块作用域。即使你再一个块中定义了var j,这个变量的作用域也贯穿整个函数。换句话说,上面这个代码类似于下面:

function hookupevents() {
var j;
for (var i = 0; i < 4; i++) {
j = i;
document.getElementById("myButton" + i)
.addEventListener("click",
function() { alert(j); });
}
}

下面这个函数强调了“变量提升(variable declaration hoisting)”这个行为:

function strange() {
k = 42;
for (i = 0; i < 4; i++) {
var k;
alert(k);
}
}

这个函数显示42四次,因为变量K在整个函数中始终指向同一个变量K,即使他已经被声明过。没错,JavaScript允许你在声明一个变量前就使用它。JavaScript的变量作用域是函数,因此,如果你想要在一个新的作用域中创建一个变量,你就要把它加到一个新的函数中,因为函数定义了作用域。

function hookupevents() {
for (var i = 0; i < 4; i++) {
var handlerCreator = function(index) {
var localIndex = index;
return function() { alert(localIndex); };
};
var handler = handlerCreator(i);
document.getElementById("myButton" + i)
.addEventListener("click", handler);
}
}

现在,事情开始变得奇怪了。我们要把一个变量放到它自己的函数中,因此我们定义了一个帮助函数 handlerCreator ,它可以创建一个事件处理函数。因此我们现在有了一个函数,我们可以创建一个新的局部变量,这个局部变量与在父函数(parent function)中的变量是不同的。我们把这个局部变量称作localIndex。handlerCreator函数把参数保存在localIndex中,然后创建并返回了一个真正的事件处理函数,这个函数使用localIndex而不是变量 i 因此它使用的是捕获值而不是原始变量。现在每个handler都得到一个localIndex的独立副本,你可以看到,每次显示的都是期望得到的值。我用上面那种长方式写代码是为了解释性目的。在实际中,代码可以精简。作为例子,index参数可以用来代替localIndex,应为参数可以被看做方便的已经初始化了的局部变量。

function hookupevents() {
for (var i = 0; i < 4; i++) {
var handlerCreator = function(index) {
return function() { alert(index); };
};
var handler = handlerCreator(i);
document.getElementById("myButton" + i)
.addEventListener("click", handler);
}
}

然后handlerCreator可以改写成内联形式的(即写成立即执行函数)

function hookupevents() {
for (var i = 0; i < 4; i++) {
var handler = (function(index) {
return function() { alert(index); })(i);
document.getElementById("myButton" + i)
.addEventListener("click", handler);
}
}

然后是handler本身也可以写成内联形式

function hookupevents() {
for (var i = 0; i < 4; i++) {
document.getElementById("myButton" + i)
.addEventListener("click",
(function(index) {
return function() { alert(index); })(i));
}
}

(function(x){...})(y)这种模式被具有误导性的称作自调用函数(self-invoking function),说它是误导性的是因为这个函数不会调用本身;外围的代码调用它。一个更好的名字可能是立即执行函数(immedately-invoked function)(貌似国内都是叫立即执行函数,并没有见到自调用函数这一说法)因为这个函数一旦定义就被立即执行了。下一步就是去简单的改变帮助函数的index变量的名字为 这样外层变量和内层变量之间的联系就可以变得更加显然(对于初学者也更加容易迷惑):

function hookupevents() {
for (var i = 0; i < 4; i++) {
document.getElementById("myButton" + i)
.addEventListener("click",
(function(i) {
return function() { alert(i); })(i));
}
}

形如(function(X){...})(X)这样的模式是一种习惯写法,意思是:在封闭的代码块中,按值的方式捕获x。因为函数可以拥有多个参数,所以你可以扩展为(function(x,y,z){...})(x,y,z)用来按值的方式捕获多个变量。把整个循环体放到这个模式中也是很常见的,因为你通常多次引用循环变量,所以你可以只捕获一次然后重用这个捕获变量。

function hookupevents() {
for (var i = 0; i < 4; i++) {
(function(i) {
document.getElementById("myButton" + i)
.addEventListener("click", function() { alert(i); });
})(i);
}
}

也许在JavaScript中修复这个问题十分繁琐也是一件好事。对于C#,这个问题更容易解决,但也是很微妙的。JavaScript版本还是比较明显的。

练习题 : 这个模式不起作用了!

var o = { a: 1, b: 2 };
document.getElementById("myButton")
.addEventListener("click",
(function(o) { alert(o.a); })(o));
o.a = 42;

这个代码显示的是42 而不是 1.尽管我按值的方式捕获了o。请解释原因。

更多阅读:C#和ECMAScript 使用了两种方式解决这个问题(这里指的应该是语言层面上通过修改语义和添加语法糖等方式,而不是上面提到的方法)。在C#5中,foreach循环中的循环变量现在被认为是在循环中的作用域了。ECMAScript提出了一个新的关键字let。

全文翻译完。

后记:

在这篇文章中提到的预先阅读中,是C#(C# 5 之前)中foreach循环中的闭包出现了问题。代码如下:

var values = new List<int>() { , ,  };
var funcs = new List<Func<int>>();
foreach(var v in values)
funcs.Add( ()=>v );
foreach(var f in funcs)
Console.WriteLine(f());

这里显示的是三个120 而不是 100 110 120。作者解释的原因是()=>v意味着“返回当前变量v的值”而不是“返回委托被创建时的值v”。闭包关闭的是变量,而不是变量的值。解决的方法是加一个局部变量:

foreach(var v in values)
{
var v2 = v;
funcs.Add( ()=>v2 );
}

每一次重新开始一个循环我们都重新定义了一个v2,每次闭包闭合的都是一个新的只被复制了当前变量v的当前值的v2。至于问什么foreach会出现这种问题,原因在于foreach只是下面代码的语法糖:

{
IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();
try
{
int m; // OUTSIDE THE ACTUAL LOOP
while(e.MoveNext())
{
m = (int)(int)e.Current;
funcs.Add(()=>m);
}
}
finally
{
if (e != null) ((IDisposable)e).Dispose();
}
}

所以拥有块级作用域的C#在上面代码的作用下闭合了的是循环结束后最终的m值。如果把它改成下面的形式:

   try
{
while(e.MoveNext())
{
int m; // INSIDE
m = (int)(int)e.Current;
funcs.Add(()=>m);
}

代码就可以正确运行了。

剩下的就是作者阐述了修改这个问题的好处与坏处。也还是值的一看的。

然后关于上面的练习题,作者提供的模式只是按值的方式捕获了对象o的引用,所以在最后一行更改了o.a后,所有的都更改了。

【简译】JavaScript闭包导致的闭合变量问题以及解决方法的更多相关文章

  1. [转]权限问题导致Nginx 403 Forbidden错误的解决方法

    权限问题导致Nginx 403 Forbidden错误的解决方法 投稿:junjie 字体:[增加 减小] 类型:转载 时间:2014-08-22 这篇文章主要介绍了权限问题导致Nginx 403 F ...

  2. eclipse上一次没有正确关闭,导致启动的时候卡死错误解决方法

    关于 eclipse启动卡死的问题(eclipse上一次没有正确关闭,导致启动的时候卡死错误解决方法),自己常用的解决方法: 方案一(推荐使用,如果没有这个文件,就使用方案二): 到<works ...

  3. BootStrap Validator 版本差异问题导致的submitHandler失效问题的解决方法

    最近一直在做互金平台,做到后台提交表单的时候出现验证提交数据一直没有提交的问题.于是百度了一下.果然是版本问题造成的.幸好找到了问题所在.我一直仿照的是东钿原微信平台的做法,但是使用byond的后台框 ...

  4. CSS设置浮动导致背景颜色设置无效的解决方法

    float浮动会使父元素高度塌陷,父级元素不能被撑开,所以导致背景颜色不能被撑开 解决方法: 对父元素设置高度 对父元素设置 overflow:hidden清除浮动 把父元素也设置为float浮动 结 ...

  5. 因修改/etc/ssh权限导致的ssh不能连接异常解决方法

    因修改/etc/ssh权限导致的ssh不能连接异常解决方法 现象: $ssh XXX@192.168.5.21 出现以下问题 Read from socket failed: Connection r ...

  6. 项目部署到tomcat Root中后导致 WebApplicationContext 初始化两次的解决方法

    上一篇文章刚说项目部署到tomcat的ROOT中,今天就发现一个问题.通过eclipse启动tomcat时候,WebApplicationContext 初始化两次: 现象:   通过eclipse控 ...

  7. spring项目的 context root 修改之后,导致 WebApplicationContext 初始化两次的解决方法

    修改了 spring web 项目的 context root 为 / 之后,在启动项目时,会导致 WebApplicationContext  初始化两次,下面是其初始化日志: 第一次初始化: 四月 ...

  8. Mongo导出数据文件导致错误 Got signal: 6 (Aborted)解决方法

    一哥们要导出一个数据表的数据,结果导出一半,硬盘不够用,卡死了, 然后重启主机,导致mongo启动后进程自动死掉, 报错如下. Mon Oct 28 10:39:02.270 [initandlist ...

  9. 由于SSH配置文件的不匹配,导致的Permission denied (publickey)及其解决方法。

    读者如要转载,请标明出处和作者名,谢谢.地址01:http://space.itpub.net/25851087地址02:http://www.cnblogs.com/zjrodger/作者名:zjr ...

随机推荐

  1. onitemcommand 的作用以及onitemdatabound的具体作用

    Repeater控件.DataList控件的属性:OnItemCommand 当在ItemTemplate 中所宣告的Button 或LinkButton 控件触发事件时,如果该控件CommandNa ...

  2. css怎么引用某张图片?链接要怎么写

    总结一下 <a href="D:\xxx"> <img src="./xxx.jpg">

  3. asp.net Ajax Post 请求一般处理程序

    其实很早就开通博客园了,一直想写些有价值的东西,供自己以后查阅的同时,也可以帮助别人遇到此类问题时能有一个好的解决方法.但是由于各种原因, 就没有实施我的想法.今天突然很想写下一篇文章,不知道我的第一 ...

  4. ASP.NET Identity 用户注册相关设定

    此部分可以在 Web项目中的App_Start目录下的 IdentityConfig.cs 文件进行设置. 1.配置密码的验证逻辑 manager.PasswordValidator = new Pa ...

  5. Asp.Net静态资源动态压缩之WebOptimization

    一.Asp.Net中对Css/Js的动态压缩工具 WebOptimization 在Asp.NetMVC自带的模板项目中自动引入了当前WebOptimization工具.如果使用的空模板Nuget命令 ...

  6. 开源控件ViewPagerIndicator学习

    导航条指示器.ViewPagerIndicator 地址 https://github.com/JakeWharton/ViewPagerIndicator Style是重用控件的一种技术.类似CSS ...

  7. Java中正确使用hashCode() 和equals() 、==

    在java编程中,经常会遇到两个对象中某个属性的比较,常常会用到的方法有: == .equals().但是两者使用起来有什么区别呢? 一.== java中的==是比较两个对象在JVM中的地址.比较好理 ...

  8. 伪Base16的构思和实现

    最近看见了一个迅雷地址,发现将其转换为普通链接的工具后,发现所谓专用地址地址就是原地址前加一个表示迅雷的前缀,后进行Base64编码.查阅Base64编码过程后,突发奇想:能否做一个Base16算法? ...

  9. java新手笔记13 继承

    1.Person类 package com.yfs.javase; //可以有多个子类 public class Person { private String name;// 私有属性不能继承 pr ...

  10. sql server经典sql

    1. sql server构造oracle rownum列 select * from ( select row_number() over(order by t.修改时间 desc) rownum, ...