循环当中的匿名函数问题

前言:从《原生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的更多内容补充了另一篇博文分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容

  • 前言 相信很多前端小伙伴在工作和学习中,都会或多或少的接触和了解到匿名函数和闭包。被这俩知识点所困扰,也去网上搜索...
    就那ck阅读 8,862评论 7 21
  • 欢迎阅读专门探索 JavaScript 及其构建组件的系列文章的第四章。 在识别和描述核心元素的过程中,我们还分享...
    OSC开源社区阅读 1,141评论 1 10
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,690评论 0 5
  • 在广告年代,NPS(净推荐值)被吹捧为“一个数字就可让你进步”的神器。 我们通常会这样获取NPS:询问受访者“你有...
    李福东阅读 1,934评论 0 2
  • 不知不觉,我也成了二胎妈妈,肚子里这个已经四个月了。这段时间起早贪黑上着班,接送孩子,连吃饭都几乎成了挤时间才能...
    深蓝_4462阅读 148评论 0 0