惊天困惑--到底是闭包的功还是闭包的锅?

目录

解释一:闭包有功,异步函数的锅《JavaScript 设计模式与开发实践》

解释二:闭包的锅,创建匿名函数立即执行来弥补《JavaScript高级设计程序(第3版)》

闭包是什么


我们经常看到一段闭包的经典应用:假设页面上5个div节点,通过循环来给每个div绑定onclick事件,按照索引顺序,分别弹出0,1,2,3,4,5。但下面这段代码的运行结果是,无论点击那个div,最后弹出的都是5。

<html>
	<div>1</div>
	<div>2</div>
	<div>3</div>
	<div>4</div>
	<div>5</div>
<body>

<script>
var  nodes = document.getElementByTagName('div');
for ( var. i =0; i<nodes.length;i++){
	nodes[i].onclick = function(){
		alert(i);
	}
}
</script>
</body>
</html>

解决方案也很统一:

for ( var. i =0; i<nodes.length;i++){
    (function(i){
        nodes[i].onclick = function(){
		                      alert(i);
	                      }
         
    })(i);
};

困惑的关键点是,对出现这种情况的解释,总的来说有两种

  1. 原始代码的问题是异步函数的锅,而解决方案是利用的闭包的特性,因此是闭包的功。
  2. 原始代码的问题是闭包的锅,而解决方案是利用了立即执行函数的特性,弥补了闭包某种意义上的的缺陷。

下面进行详细的阐述。

解释一:闭包有功,异步函数的锅《JavaScript 设计模式与开发实践》

 

最为常见的一种解释是:

div节点的onclick事件是被异步触发的,当事件被触发的时候,for循环已经结束,此时i的值已经是5,所以div的onclick事件函数顺着作用域链从内到外查找到变量i时,查找到的值总是5。

与此种解释对应的解决方案,理论上是:

在闭包的帮助下,把每次循环的i值都封闭起来。当事件函数顺着作用域链从内到外查找变量i时,会先找到被封闭在闭包环境中的i,如果有5个div,这里i就是0,1,2,3,4(《JavaScript 设计模式与开发实践 曾探 2015》)

这样的解释看起来非常合理。

解释二:闭包的锅,创建匿名函数立即执行来弥补《JavaScript高级设计程序(第3版)》

在这本书里的解释是:

作用域链的这种配置机制引出了一个值得注意的副作用,闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。

看到这里的解释,即便看了前面的闭包作用域链的讲述,依然是有点晕的,不知道它讲的是什么。

关键的部分是,它给出了一段代码,这段代码中,除了赋值给onclick的语句不同,其他基本一致:

//此段是书中的代码
function createFunctions(){

	var result = new Array();
	for (var i = 0;i<10;i++){
		result[i] = function(){
			return i;
		}
	}
	return result;
}

//下面是测试代码
var arr = createFunctions();
arr[0]();//返回全是10 
arr[1]();//10
//...
arr[9]();//10

可以发现,在` function(){return i;} `赋值的部分,并没有涉及异步函数。但是同样出现了与onclick相同的结果--只能获取到for循环中变量的最后一个值。按照做实验的控制变量策略来看,我开始偏向于这一种解释。

书中针对代码,结合作用域链,给了更细致的解释:

这个函数返回一个函数数组,···,每个函数的作用链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunction()函数返回后,变量i的值是10,此时每个函数都引用者保存变量i的同一个变量对象,所以在每个函数内部i的值都是10.

对应的解决方案的代码,与第一种解释是同样的代码思路,为了准确对比,也贴上来:

//此段是书中的代码
function createFunctions(){

	var result = new Array();
	for (var i = 0;i<10;i++){
	    result[i] = function(num){
		                return function(){
                                    return num;
                                };
		            }(i);
	};
	return result;
}

//下面是测试代码
var arr = createFunctions();
arr[0]();//返回0
arr[1]();//返回1
//...
arr[9]();//返回9
 

那么解决方案的解释是什么呢?

通过创建另一个匿名函数来强制让闭包的行为符合预期。

在个解决方案中,没有把闭包直接赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。这里的匿名函数又一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数是。我们传入了变量i。由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样以来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。

到这里,这种解释已经完全说服我了。

那么,回过头来想,第一种解释被广泛的接受(一搜索闭包,大部分的网页都是引用的这种理论),那肯定存在其合理性,它的合理性到底是什么?

我想到了以下解释:码中是对onclick进行的赋值操作,是一个函数表达式。函数表达式不会产生函数的声明提升。因此onclick函数的重定义,生效的时刻不是在声明时,而是在运行时。当运行onclick事件时,for循环确实早已经走完了,此时去查询i的值,必然是5。从这个意义上来讲,第一种解释确实是有道理的。

即便是第2个案例,也可以使用第一种说明,解释通顺: 同样的,对 result[i] 以函数表达式的方式进行赋值,在测试代码的情况下无论执行result[i]()多迅速,都是在for循环之外了。

那么,更换一种测试方式呢?

function createFunctions(){

	var result = new Array();
	for (var i = 0;i<10;i++){
		result[i] = function(){
			return i;
		};
        //测试代码:函数表达式赋值后,立即执行
        console.log(result[i]());
	}
    //测试代码:for循环结束后执行
    console.log('after for '+reslut[0]());
	return result;
}

//测试代码
var aa = createFunctions();
// 控制台立即输出 0-9 after for 10
aa[0]();// 输出10
aa[1]();// 输出10
aa[9]();// 输出10

以这种测试方案来看的话,其实是执行时间的问题。作为函数表达式,函数执行时的作用域链,与定义时执行的作用域链,变量的取值不同。

这样一来,可以解释,第一种解释其实是一种特殊情况,被赋值的对象是异步函数,只是其中的一种特色情况。

而第二种解释,是现象性的归纳总结。虽然没有任何过错,但是表达的太含糊。

总之,无论被赋值的对象是异步函数,还是普通变量时,只要执行时的上下文环境,不是for循环中,就会产生问题。在for循环结束后,i作为createFunctions执行环境中的确定值变量 i=10。

并且还得到一个结论,函数表达式并不会保存其中的参数数值,执行后会释放,因此即便 执行后再调用,也无法获得与立即执行同样的结果。

闭包是什么

其实到这里就结束了。但是,还有一个问题,造成这种问题代码的根因,到底是不是闭包的不足呢?解决方案又是不是归因于闭包的功劳呢?

我想,这个问题,最终指向的那个问题是,闭包是什么,哪一块才是闭包呢?

未完待续

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

picoasis

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值