在JavaScript中使用变量的let声明和结果闭包的功能

通过阅读哈勃(Habré)上的文章“ Var,let还是const? 变量和ES6的范围问题及其注释,以及Zakas N.的“对ECMAScript 6的理解”的相应部分。 根据我的阅读,我得出的结论是,在评估varlet的使用时,并不是所有事情都那么简单。 作者和评论员倾向于认为,在不需要支持旧版本浏览器的情况下,完全放弃使用var以及默认情况下使用某些简化的结构代替旧的结构是有意义的。

关于这些广告的范围,包括上面的材料,已经有足够的发言了,所以我只想重点介绍一些不明显的地方。

首先,我想在循环中考虑立即调用的函数的表达式(立即调用函数表达式,IIFE)
let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 1 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

或者您可以使用let使它们不使用:

 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); } func1.forEach(function(func) { func(); }); /*     0 newECMA6add.js:4:37 1 newECMA6add.js:4:37 2 newECMA6add.js:4:37 */ 

Zakas N.声称,给出相同结果的两个相似示例也完全相同:
“此循环的工作原理与使用var和IIFE的循环完全一样,但可以说更干净”
然而,他本人又间接地反驳了这一点。

事实是,使用let时,循环的每次迭代都会创建一个单独的局部变量i ,而发送到数组的函数中的绑定也会从每次迭代中分离出各个变量。

在这种情况下,结果确实没有什么不同,但是如果我们使代码复杂一点怎么办?
 let func1 = []; for (var i = 0; i < 3; i++) { func1.push(function(i) { return function() { console.log(i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); /*    0 newECMA6add.js:4:59 2 newECMA6add.js:4:59 */ 

在这里,加上++ i,我们的结果证明是相当可预测的,因为即使在循环本身通过时,我们调用的函数的i值在调用时也是相关的,因此后续操作++ i不会影响传递给数组中函数的值,因为它已经在函数(i)中以特定值i关闭。

现在与没有IIFE的let版本进行比较
 let func1 = []; for (let i = 0; i < 3; i++) { func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); /*    1 newECMA6add.js:4:37 3 newECMA6add.js:4:37 */ 

结果显然发生了变化,这种变化的本质是我们没有立即调用带有值的函数,而是函数在循环的特定迭代中获取了闭包中可用的值。

为了更好地了解正在发生的事情的本质,请考虑具有两个数组的示例。 对于初学者,让我们采用不带IIFE的var:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    5 newECMA6add.js:6:37 6 newECMA6add.js:6:37 7 newECMA6add.js:5:37 8 newECMA6add.js:5:37 */ 

到目前为止,一切都是显而易见的-没有闭包(尽管可以说是闭包,但在全球范围内,尽管这并不完全正确,因为访问i基本上到处都是),即类似地,但在局部区域显然,变量i将具有类似的条目:
 let func1 = [], func2 = []; function test() { for (var i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } } test(); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*     5 newECMA6add.js:7:41 6 newECMA6add.js:7:41 7 newECMA6add.js:6:41 8 newECMA6add.js:6:41 */ 

在两个示例中,将发生以下情况:

1.在循环的最后一次迭代开始时, i == 2 ,然后加1(++ i) ,最后,从i ++再加上1 ,结果,在整个循环结束时, i == 4

2.位于func1func2数组中的函数被一一调用 ,并且在它们中的每个变量中,相同的变量i依次递增,这相对于其作用域是封闭的,当我们不处理全局变量而是处理局部变量时,这一点尤其明显。

添加IIFE
第一种选择:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 */ 
第二种选择:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(i); } }(++i)); func1.push(function(i) { return function() { console.log(i); } }(++i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:56 1 newECMA6add.js:5:56 */ 

在第一种情况下添加IIFE时,我们仅在函数(i)中分别调用i的固定值分别在循环的第一和第二遍期间分别为02 ),并将它们的值加1,因此每个函数彼此独立,因为这是对公共变量的闭包由于没有在循环通过期间立即传输值i的事实,因此没有循环。 在第二种情况下,循环变量也没有闭包,但是该值以同时递增的方式传输,因此在第一遍结束时i == 4 ,循环没有继续进行。 但是,我提请注意以下事实:在第一个和第二个变体中,对于每个函数,内部函数中外部函数的变量闭包仍然存在。 例如:
 let func1 = [], func2 = []; for (var i = 0; i < 3; i++) { func2.push(function(i) { return function() { console.log(++i); } }(i)); func1.push(function(i) { return function() { console.log(++i); } }(i)); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    1 newECMA6add.js:6:56 3 newECMA6add.js:6:56 1 newECMA6add.js:5:56 3 newECMA6add.js:5:56 2 newECMA6add.js:6:56 4 newECMA6add.js:6:56 2 newECMA6add.js:5:56 4 newECMA6add.js:5:56 */ 
注意:即使您使用函数构造循环,普通的闭包自然也不会。

现在考虑分别没有IIFE的let语句。
 let func1 = [], func2 = []; for (let i = 0; i < 3; i++) { func2.push(function() { console.log(++i); }); func1.push(function() { console.log(++i); }); ++i; } func1.forEach(function(func) { func(); }); func2.forEach(function(func) { func(); }); /*    2 newECMA6add.js:6:41 4 newECMA6add.js:6:41 3 newECMA6add.js:5:41 5 newECMA6add.js:5:41 */ 

鉴于众所周知的循环周期原理,在这里,我们再次形成了循环变量的短路,而不是一个短路,而是两个短路,并且不是分离的而是常见的短路。

结果,在第一个闭包中,在调用数组中的函数之前,值是i == 1 ,而在第二个i == 3中 。 这些是变量,它们是在i ++和循环迭代之前,但在循环块中的所有指令之后,并且在每次特定迭代中均关闭的。

然后调用位于func1数组中的函数,它们在两个闭包中递增相应的变量,结果是第一个i == 2和第二个i == 4

随后对func2的调用进一步增加分别得到i == 35

我故意将func2func1放置在块中,以使其与位置的独立性更清晰可见,并强调读者注意闭环变量的事实。

最后,我将举一个简单的例子,旨在加深对闭包和let范围的理解:
 let func1 = []; { let i = 0; func1.push(function() { console.log(i); }); ++i; } func1.forEach(function(func) { func(); }); console.log(i); /* 1 newECMA6add.js:5:34 ReferenceError: i is not definednewECMA6add.js:10:1 */ 

我们总共有什么


1.调用立即调用函数的表达式并不等同于在循环中的函数中使用可迭代的let变量,并且在某些情况下会导致不同的结果。

2.由于在对迭代器使用let声明时,在每次迭代中都会创建一个单独的局部变量,因此产生了一个问题,即如何对不必要的数据使用垃圾回收器。 我承认,在这一点上,我想首先引起注意,怀疑分别在大型循环中创建大量变量会降低编译器的速度,但是,当仅使用let变量声明对测试数组进行排序时,执行时间几乎增加了100,000个单元的阵列两次:
带有var的选项:
 const start = Date.now(); var arr = [], func1 = [], func2 = []; for (var i = 0; i < 100000; i++) { arr.push(Math.random()); } for (var i = 0; i < 99999; i++) { var min, minind = i; for (var j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function(i) { return function() { return i; } }(arr[i])); } func1.push(function(i) { return function() { return i; } }(arr[99999])); for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 9.847 


和带有let的选项:
 const start = Date.now(); let arr = [], func1 = [], func2 = []; for (let i = 0; i < 100000; i++) { arr.push(Math.random()); } for (let i = 0; i < 99999; i++) { let min, minind = i; for (let j = i + 1; j < 100000; j++) { if (arr[minind] > arr[j]) minind = j; } min = arr[minind]; arr[minind] = arr[i]; arr[i] = min; func1.push(function() { return arr[i]; }); } func1.push(function() { return arr[99999]; }); for (let i = 0; i < 100000; i++) { func2.push(func1[i]()); } const end = Date.now(); console.log((end - start)/1000); // 5.3 


同时,执行时间实际上与指令的存在与否无关:

与IIFE
 func1.push(function(i) { return function() { return i; } }(arr[i])); 

要么
没有IIFE
 func1.push(function() { return arr[i]; }); 

函数调用
 for (var i = 0; i < 100000; i++) { func2.push(func1[i]()); } 


注意:我了解关于速度的信息并不是新鲜事物,但出于完整性考虑,我认为这两个示例值得一提。

从所有这些我们可以得出结论,在不需要与早期标准向后兼容的应用程序中,使用let声明而不是var是合理的,特别是在使用循环的情况下。 但是,与此同时,值得记住带有闭包的情况下的行为特征,并在必要时继续使用立即调用函数的表达式。

Source: https://habr.com/ru/post/zh-CN462971/


All Articles