本文是翻译此文

预先阅读此文:闭合循环变量时被认为有害的(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. javascript-02

    1.js的特点2.js的数据类型3.js运算符 4.js的全局变量   |-定义在函数体外部的变量   |-定义在函数体内部没有使用var声明 var和没有var声明变量的区别?     |-var ...

  2. Velocity 模板引擎介绍

    一.变量 1. 变量定义 #set($name =“velocity”) 2. 变量的使用 在模板文件中使用$name 或者${name} 来使用定义的变量.推荐使用${name} 这种格式,因为在模 ...

  3. Dom操作--全选反选

    我们经常会在网站上遇到一些多选的情况,下面我就来说说使用Dom写全选反选的思路. 全选思路:首先,我们来分析一下知道,当我们点击"全选"复选框的时候,所有的复选框应该都被选中,那我 ...

  4. Mysql 的一些异常解决

    一.关于大文件存储 1.利用mysql存储大文件时,异常截图 在配置文件中加上如下一行 2.改完后重启mysql,但是又报如下错误: 解决方案: 我的mysql 是5.6版本,查到网上说要修改配置文件 ...

  5. MyEclipse2014中项目名更改后如何使用新的项目名部署到Tomcat中去

    在项目中调试的时候突然发现我复制的项目(项目名修改过了)部署在Tomcat中运行的时候还是显示的是原来的项目名,以至于我使用新的项目名称作为URL请求竟然是404,我去,当时感觉就不怎么好了. 当然, ...

  6. Java根据出生年月日获取到当前日期的年月日

    源码链接:http://pan.baidu.com/s/1sj61IUD

  7. Html+CSS命名规范:

     Html+CSS命名规范: 1.样式命名: 2.样式文件命名:

  8. IAP (In-App Purchase)中文文档

    内容转自:http://yarin.blog.51cto.com/1130898/549141 一.In App Purchase概览 Store Kit代表App和App Store之间进行通信.程 ...

  9. angularJS的核心特性

    前几天师傅让我了解一下angularJS,angularJS是一个前端框架,具体的优缺点和运用场景我现在也还没有搞清楚,暂时就先不做描述了,留到运用以后进行补充吧. angularJS四大核心特性:M ...

  10. js 无刷新分页代码

    /** * 分页事件处理 */function paging(){ $("#firstPage").click(function(){ //首页 var pageNo = getP ...