由 for 循环引发的关于闭包和作用域的思考
Stage 1
- 起因是在某技术博客里看到了如下代码
function fun(){for(var i=0; i<lis.length; i++){ // 此处的 length=5
lis[i].onclick = function(){console.log(i);
}
}
}
于是我在 console 里写入了如上代码,依次点击 lis,输出了五次 4,这对于写惯了 c 语言的我是一个观念上的颠覆,于是开始了大规模的资料查找,试图解决我的这个疑惑。
Stage 2
- 在经过几番询问和一些技术博客的翻阅之后,得到了如下的一种解释:
“ 在这个函数里面的 i 其实引用的是最后一次 i 的值,为什么不是 1,2,3,4… 呢?因为 for 循环中并没有执行这个函数,这个函数是在你 点击 的时候才执行的,当执行这个函数的时候,它发现它自己没有这个变量 i,于是向它的作用域链中查找这个变量 i,因为当你单击这个 box 的时候已经 for 循环完了,所以找到的 i 是最后一次赋值后的 i ”
- 本以为事情到此结束了,可我感觉还是差了些什么,于是我向高程求助。
Stage 3
- 在《JavaScript 高级程序设计》的 7.2 节我找到了令我受到启发的东西:
“作用域链的机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。”
“表面上看,每个函数都应该返回自己对应的 i 值,但实际上每个函数都返回了一样的值。因为每个函数的作用域链中都保存着 fun()函数的活动对象,所以他们引用的都是同一个变量 i。当 fun()函数返回后,变量的 i 值是 4,此时每个函数都引用着保存变量 i 的同一个变量对象,所以在每个函数内部 i 的值都是 10。”
- 于是在这个阶段我有了如下新的理解:绑定的函数并不是立刻就实现,而是处于等待调用的状态。当程序的执行流进入一个函数的时候,这个函数被推入一个环境栈中,再进行变量读取和函数内容的实现。
- 实例化地,在本篇开头的代码中,五次循环将 lis[i].onclick 事件分别绑定在了五个匿名函数上,开辟了五个执行环境,进而形成了五条作用域链,形如:[闭包]→[fun()的活动对象]→[全局变量对象],而很容易理解地,fun()活动对象是这五条作用域链所共享的,自然 i 值也就是共享的了
Stage 4
- 这部分该讲讲解决方法了
- 高程上推荐的方法:通过创建另一个匿名函数强制让闭包行为符合预期
function fun(){for(var i=0; i<lis.length; i++){ // 此处的 length=5
lis[i].onclick = (function(num){return function(){console.log(num);
}
})(i)
}
}
这种方法在每次循环中,用立即执行的匿名函数记录下了当前的 i 值(num),并创建了单独的作用域,又在匿名函数中创建了一个新的闭包,接收 i(num)值,形成了单独的作用域链。
- es6 中 let 方法:
function fun(){for(let i=0; i<lis.length; i++){ // 此处的 length=5
lis[i].onclick = function(){console.log(i);
}
}
}
虽然本人还没有正式开始 es6 的学习(捂脸,但因涉及本篇博客的解决方法,还是认真地了解了一下 let 关键字。此方法的成功,最大的功臣便是 let 的块级作用域特点,他在每次循环中生成了单独的作用域,达到了与上一种方法相同的效果。
Stage 5
研究这个看似很简单的特性耗费了整整一天的时间,也深深体会到了为什么说 JS 语言的糟粕不少。
-
总结:
- for 循环体内定义函数,若函数体内用了 for 块内的 var 变量,在 for 语句外调用该函数时,该函数采用的是循环结束后的 var 值
- 而块内用 let 变量,与之同级的函数体用了该 let 变量,之后调用函数,函数使用的是定义时块内的 let 变量值。
- 收货:更加明确了关于作用域、闭包等概念。尝到了 ES6 语法的甜头,以后应多使用新标准和新技术。
- 反思:不该在糟粕的地方太过于钻牛角尖,避免浪费时间。