前几晚看《The Go Progromming Language》时,在 匿名函数 的最后一小节中,发现了一个比较有趣的小坑。 大概的场景为:首先创建一些目录,然后对于每一个目录,分别声明一个匿名函数删除目录,样例代码如下:
|
|
大家可以先自行考虑一下,有没有什么问题,效果会是怎样
警告:捕获迭代变量
(颇为中二的标题名字是从书上搬过来的
上面的代码是有问题的,运行后rmdirs
中每一个函数的效果都是删除最后一个目录。
为什么没有出现我们预期的效果呢?原因在于循环变量的作用域。
在上面的程序中,for 循环语句引入了新的词法块,循环变量 dir 在这个词法块中被声明。在该循环中生成的所有函数值都共享相同的循环变量。需要注意,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值。以 dir 为例,后续的迭代会不断更新 dir 的值,当删除操作执行时,for 循环已完成,dir 中存储的值等于最后一次迭代的值。这意味着,每次对 os.RemoveAll 的调用删除的都是相同的目录。
正确的代码应该如下修改,通过在块级中声明一个局部临时变量,将其代替循环变量放入匿名函数中。
|
|
为什么这个修改是可行的?原因很简单,我们在匿名函数内部使用的是一个局部的临时变量,因为是临时的,块级代码结束时,变量地址指向内容很可能要回收,所以不能存储地址,只能够存储变量的值。 那为什么前者的代码匿名函数值中记录的就是变量的内存地址?循环变量不也属于这个词法块吗?还真不太一样。循环变量虽说也是由循环词法块被声明,但是相对于词法块内的代码,它实质上是一个全局变量的地位,它对于代码块的每一次执行都是一样的地位,一样的地址,所以实际存储的是变量的地址。
JavaScript 中的循环变量
当时看到书,我第一时间就想到了 JavaScript 中也有着同样的情况,我之前还遇到过类似的问题:给一系列的控件绑定事件触发函数,每个函数中根据循环变量设定条件,然后出现了相似的问题。当时采用的解决方法是提到了参数处理的,治标不治本。
看到这里后,我马上就用 JavaScript 复现了这个问题
|
|
结果就如同预期一样,输出了 5 个 4,没有达到预期。然后用跟在 Go 一样的思路,在循环体内部中使用一个临时局部变量代替,运行后出现预期效果。
|
|
注意到这里用的是let
而不是var
。使用 let 语句声明一个变量,该变量的范围限于声明它的块中。