循环当中的匿名函数问题
前言:从《原生JS实现轮播(上)》中JS实现渐变效果引出的循环中匿名函数的问题。
如果匿名函数里使用了循环变量,或者是其他在循环过程中会被改变的变量,则匿名函数的结果可能与预期不一致。
这要看匿名函数的使用方式。
- 如果是同步使用,因为js是一个严格单线程的语言,所以不会有问题。
- 如果是异步使用,则在匿名函数中的这些变量的值会是循环结束时候的值(更严谨地说,是匿名函数实际使用的时候的变量的值,包括循环结束之后的同步代码的影响等),而不是循环过程中的值。因为在匿名函数中使用其外部的变量,保留的是对变量的引用,而不是变量的值。
异步使用最常见的方式:
- 事件绑定。
- setTimeout、setInterval这样的定时调用。(待续)
我的例子和思路
问题1:事件绑定。当点击相应按钮时,分别得到0,1,2
HTML:
<input type="button" value="输出0">
<input type="button" value="输出1">
<input type="button" value="输出2">
JS:先分析了一下错误的写法,1.4和1.5是我的正确写法
var s=document.getElementsByTagName("input");
/*1.1
for(var i=0;i<s.length;i++){
s[i].onclick=function(){
console.log(i);
}
}
*/
//小结:上例是得不到的,因为循环中的匿名函数执行的时候,i已经变成循环解释时的3了。
/*1.2
for(var i=0;i<s.length;i++){
s[i].onclick=(function(num){
console.log(num);
})(i)
}
*/
//小结:为了让函数立即执行修改的。
//但上例也是得不到的,因为循环中的匿名函数被立即执行了,而点击的时候无效了。
/*1.3
for(var i=0;i<s.length;i++){
s[i].onclick=(function(num){
return function(num){
console.log(num);
}
})(i)
}
*/
//小结:为了让函数立即执行时返回一个函数,点击的时候再输出。
//但上例也是得不到的,因为参数的位置写错了。立即执行的时候就已经传了一个参数进去了。
/*1.4
for(var i=0;i<s.length;i++){
s[i].onclick=(function(num){
return function(){
console.log(num);
}
})(i)
}
*/
//小结:按照上面的思路,这样才是对的。这个算闭包吗?
/*1.5
for(var i=0;i<s.length;i++){
s[i].index=i;
s[i].onclick=function(){
console.log(this.index);
}
}
*/
//小结:这是我更常用的,感觉更简洁的方法
/*1.7
for(var i=0;i<s.length;i++){
s[i].onclick=show(i);
}
function show(a){
console.log(a);
}
*/
//小结:写问题2的时候想到的,用辅助函数。错误,这样的话不点已经全部显示了。
/*1.8*/
for(var i=0;i<s.length;i++){
s[i].onclick=show(i);
}
function show(a){
return function(){
console.log(a);
}
}
//小结:写问题2的时候想到的,用辅助函数。正确,感觉和1.4类似,算闭包吗?
扩展1
百度到了这篇文章,讲得更细致一些。
一次性讲清楚这道经典JS面试题,提供了4种解法
- 同我的1.4,用闭包。感觉立即执行和闭包差不多?
- 用
Function.prototype.bind(thisArg, params...)
,暂时没用过bind
(存疑) - 和我的方法1.5类似,不过是将类数组对象转为标准数组:
lis = Array.prototype.slice.call(lis);
(存疑) - 用ES6 的
let
声明i,可以把 i 限定在block level里面。块级作用域参考变量作用域
/*1.6
for(let i=0;i<s.length;i++){
s[i].onclick=function(){
console.log(i);
}
}
*/
//小结:用ES6 的`let`声明i,可以把 i 限定在block level里面
扩展2:定时调用。
艾拉斯的提出的例子和艾拉斯的回答
问题2:定时调用。用x+i方式和setTimeout实现,3s后依次显示1到5。
注意:和问题1类似,下面的代码可以不用看了。不过注意匿名函数是setTimeout
中的函数。
/* 2.1:错误,3s后输出5个6
function test(){
var x=1;
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i+x);
},3000);
}
}
/*
//2.2 考虑用let,正确
/*
function test(){
var x=1;
for(let i=0;i<5;i++){
setTimeout(function(){
console.log(i+x);
},3000);
}
}
*/
//2.3 考虑类似例子1,用立即执行,错误。因为不会等3s,会立即执行
/*
function test(){
var x=1;
for(var i=0;i<5;i++){
setTimeout((function(a){
console.log(a+x);
})(i),3000);
}
}
*/
//2.4 修改2.3,正确。用return,是闭包吗?
/*
function test(){
var x=1;
for(var i=0;i<5;i++){
setTimeout((function(a,x){
return function(){
console.log(a+x);
}
})(i,x),3000);
}
}
*/
//2.5 用辅助函数,错误。问题类似于2.3,会立即输出。
/*
function test(){
var x=1;
for(var i=0;i<5;i++){
setTimeout(Test(i+x),3000);
}
function Test(a){
console.log(a);
}
}
*/
//2.6 用辅助函数。这样就可以了。Test()是闭包吗?
/*
function test(){
var x=1;
for(var i=0;i<5;i++){
setTimeout(Test(i+x),3000); //因为这个括号表示的是立即执行,而Test函数因为设置了return,return回来的函数不会被立即执行。
}
function Test(a){
return function(){
console.log(a);
}
}
}
*/
//test()
扩展3:传入参数为对象时
艾拉斯的提出的例子和艾拉斯的回答
关于值传递or引用传递可以看我另一篇博文。
例子
function test() {
var o = {
value: 1
};
for (var i = 0; i < 5; i++) {
o.value = i;
setTimeout((function(o) {
return function() {
console.log(o.value);
};
})(o), 0);
}
}
test();
实际执行,输出结果为5个4。虽然我们这里用IIFE对变量o进行了值传递,但由于传递的是o的地址,因此在定时任务调用的o.value是循环结束时候的o.value值,即4。
如何解决?尝试如下:
//方法1.思路:要让每次循环都是一个新的对象,才不会修改对象的地址值。
function test() {
for (var i = 0; i < 5; i++) {
let j={};
j.value=i;
setTimeout((function(o) {
return function() {
console.log(o.value);
};
})(j), 0);
}
}
test();
//方法2.同理i也用let,更简洁
function test() {
for (let i = 0; i < 5; i++) {
let j={};
j.value=i;
setTimeout(function() {
console.log(j.value);
}, 0);
}
}
test();
扩展4:let
的坑
function test() {
let x = 1;
for (let i = 0; i < 5; i++) {
x += 2;
setTimeout(function() {
console.log(x * i);
}, 0);
}
}
结果是:0 11 22 33 44,而不是期望的:0,5,14,27,44。因为x的值用的是循环最后的结果11。let x=1
写在循环外面,每次循环不会生成新的变量来存储。
如何解决?尝试如下
function test() {
let x = 1;
for (let i = 0; i < 5; i++) {
x += 2;
let y=x;
setTimeout(function() {
console.log(y * i);
}, 0);
}
}
扩展5:问题解释
对于循环中匿名函数的问题,如2.1
/* 2.1:错误,3s后输出5个6
function test(){
var x=1;
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i+x);
},3000);
}
}
/*
个人理解(可能不是太严谨的):
因为JS是单线程的。像setTimeout这样的函数不是每次都能按照延迟的时间来执行的。比如函数f0在执行开始时创建了定时器timer,timer将在200ms后触发指定函数。加入f0执行时长为250ms(大于200ms),因为js时单线程的,所以timer的函数会在fo执行完成后才执行,也就是250ms后。
对于JS,可以假设函数的执行有2个队列,Q1是执行队列,依次只能执行一个函数。Q2是等待队列,存放即将执行的函数。每当有一个函数要执行,就会被放入等待队列。当Q1空时,就执行等待列队中的。
每次循环都触发一个定时函数。但每次触发时,for循环都还未结束,此时新建的定时函数只能放在等待队列里,无法立即执行。当最后一次for循环执行结束后,执行队列变为空,这时等待队列的函数就立即进入到了执行队列,于是输出5次。但此时i已经是循环结束时的6了,因为setTimeout指定的匿名函数中i的值是一种值传递,所以5次输出都是6。事件绑定也是类似的道理。
P.S. 关于setTimeout
的更多内容补充了另一篇博文分析。
上一篇: php支持多线程吗
下一篇: JS实现底部显示新闻并且有滑动特效