再说js的闭包

最近一直忙于工作,杂七杂八的事情一大堆。为了弄懂公司里的项目,研究了backbone,也写了点关于backbone的小例子,但是始终不明白为啥最后弃用了backbone。用户嫌打开网页的速度太慢了,由于页面嵌套了很多的元素,而且页面是以json字符串的格式存储于数据库中,每次页面加载的时候,会向数据库去取当前页面的json,然后动态生成html,最后进行页面和模型的绑定。由于javascript的单线程特点,在执行js的时候,页面的渲染就会挂起,等到js执行完毕,渲染才会进行。反之亦然。可是这也没法解释,公司为什么会弃用backbone。作为一个成熟的框架,在富文本页面,backbone有很多天然的优势。现在的做法是,先渲染一部分的页面出来,然后进行js动态生成html,最后进行元素和事件的绑定。其实理论上来说,速度并没有快多少,但是却能给用户一种假象,至少我打开页面的时候,并不是空白的一片。可是这也不足以成为换掉backbone的理由,我只需要想办法,先渲染一部分页面不就可以了吗?我会后续跟进这个项目,继续研究下去,找到更好的解决办法。
闲话扯到这儿,我在研究javascript单线程性的时候,看了陈皓的一片文章《javascript装载和执行》,对其中的一个异步ajax调用的小例子有一点不解的地方。源程序是这样子的:

1
2
3
4
5
6
7
// 你需要解决的问题
var total = 20;
for(var i = 0; i < total; i++){
xss_rpc_call(i, function(result){
alert(i + ',' + result); // i是错的
});
}

其中xss_rpc_call是接口,根据输入的十进制数返回对应的十六进制数。而返回的结果,以回调函数参数的方式给出,即result。但是这个程序有一个问题,最后alert出来的结果,i都是20。前一章我们讨论过闭包,知道了闭包的定义,有权访问另一个函数作用域中的变量的函数。在这里就是匿名函数function(result){alert(i + ‘,’ + result);}。我们很明显的可以看出来,这个问题的关键在于闭包所引用的变量i,在最后闭包执行的时候,已经等于20了。我们知道闭包有一个特点,闭包永远只能取得包含函数内的任何变量的最后一个值。在这里,包含函数内的变量i的最后一个值就是20。所以,对此我们得想一个办法,解决alert时十进制和十六进制数不匹配的问题。

1
2
3
4
5
6
7
8
9
// 解决方法
var total = 20;
for(var i = 0; i < total; i++){
(function(num){
xss_rpc_call(num, function(result){
alert(num + ',' + result); // 还是错的,原因在于xss_rpc_call这个接口里定义的回调函数被重写了好几次
});
})(i);
}

以上这种写法,保证了闭包所引用的包含函数内的变量num的最后一个值是不一样的。这段代码会生成20个闭包,每一个闭包所引用的num,都是i在递增过程中的一个复本,从0到19。可惜的是,xss_rpc_call这个接口所定义的回调函数并没有像数组或者对象一样,将每一个闭包的状态都保存起来。这个接口定义的回调函数只是一个变量,它被不断地重写,结果就是它永远只能取得最后一个闭包的状态。在闭包被执行的时候,闭包所引用的num的值是i-1,即20。而想要看到正确的效果,那么接口在定义的时候,其要执行的回调函数,就得是数组或者是对象形式的。这样才能保证每一个闭包的状态都能够被记录下来。而不是永远被下一个覆盖。
来看一下xss_rpc_call这个接口的定义:

1
2
3
4
5
6
7
8
9
10
function xss_rpc_call(n, callback)
{
var callbackName="xss_rpc_callback";
var url = "http://coolshell.cn/t.php?n="+n+"&callback="+callbackName;
xss_ajax(url);
xss_rpc_callback = function(result){
var timeout = Math.round(Math.random() * 1000)
callback && setTimeout(function(){callback(result);}, timeout);
}
}

很明显xss_rpc_callback这个回调函数这么定义的话,肯定会被重写。
再看一下改进过的xss_rpc_call2的接口定义:

1
2
3
4
5
6
7
8
9
10
function xss_rpc_call2(n, callback)
{
var callbackName="xss_rpc_callback"+ n + Math.round(Math.random() * 100000);
var url = "http://coolshell.cn/t.php?n="+n+"&callback="+callbackName;
xss_ajax(url);
window[callbackName] = function(result){
var timeout = Math.round(Math.random() * 1000)
callback && setTimeout(function(){callback(result);}, timeout);
}
}

同样一目了然,window[callbackName]这个匿名函数(即回调函数)是以对象属性的形式保存,保证了每一次闭包状态的保存都能够正常执行。在回调函数被执行的时候,就能调用出对应闭包状态。
最后,正确程序应该是:

1
2
3
4
5
6
7
8
var total = 20;
for(var i = 0; i < total; i++){
(function(num){
xss_rpc_call2(num, function(result){
alert(num + ',' + result); // 执行正确
});
})(i);
}

avatar

chilihotpot

You Are The JavaScript In My HTML