欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

循环当中的匿名函数问题

程序员文章站 2022-03-08 23:33:58
...

前言:从《原生JS实现轮播(上)》中JS实现渐变效果引出的循环中匿名函数的问题。

如果匿名函数里使用了循环变量,或者是其他在循环过程中会被改变的变量,则匿名函数的结果可能与预期不一致。
这要看匿名函数的使用方式。

  • 如果是同步使用,因为js是一个严格单线程的语言,所以不会有问题。
  • 如果是异步使用,则在匿名函数中的这些变量的值会是循环结束时候的值(更严谨地说,是匿名函数实际使用的时候的变量的值,包括循环结束之后的同步代码的影响等),而不是循环过程中的值。因为在匿名函数中使用其外部的变量,保留的是对变量的引用,而不是变量的值。

异步使用最常见的方式:

  1. 事件绑定。
  2. 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. 同我的1.4,用闭包。感觉立即执行和闭包差不多?
  2. Function.prototype.bind(thisArg, params...),暂时没用过bind(存疑)
  3. 和我的方法1.5类似,不过是将类数组对象转为标准数组:lis = Array.prototype.slice.call(lis);(存疑)
  4. 用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的更多内容补充了另一篇博文分析。