简单的函数闭包问题

在这个点击事件函数中,为了记录点击的参数按钮的参数是多少定义index变量并赋值,但是为什么这句话一定要放在外面的函数才生效,而放在点击函数里面却会出现报错

先上一张正确的图

clipboard.png

下面这张是错误的图

clipboard.png

阅读 4.3k
7 个回答

的确是函数闭包问题。
第一种

相当于把 i 赋值给 oLi的一个属性 index,这样相当于每个oLi[i] 保存着对应的i 比如:oLi[1].index==1,oLi[2].index==2 然后后面可以通过 this.index 来改变相应的 类名。
第二种
因为 ioLi.onclick 的事件中会用到,所以导致包含函数不会被销毁,其作用域也不会被销毁 所以i还存在于包含函数的作用域中 这样 for循环完后 i 的值就等于oLi.length的值,所以每个点击事件中的i 值都等于oLi.length的值。

主要涉及知识点有:
  1. 在函数区域内,如果有声明变量,则使用内部的变量,如果没有声明该变量,它会一直沿着作用域向上找,找不到时,则使用函数外声明的全局变量。

  2. 函数中有变量声明,会提前到函数体的最前面。

因此:

  • 图2:因为for循环中声明了i变量,因此变量提前至函数体最前面,因为没有初始化因此值为undefined;

  • 图1:i在外面赋值因此取得的是外围函数i值通过循环赋值。

上图吧:

  • 代码:

clipboard.png

  • 点击之后输出:

clipboard.png

先看第二种情况:
赋值在click的匿名函数里面,这个是循环结束之后,当你点击的时候才发生,此时内存里面的i已经执行到了length,所以你再点击的时候就是数字length,
再看第一种情况:
赋值在匿名函数之上,每一次循环的时候会给index赋值i,当你点击的时候,此时没有再设置变量,所以你点哪个,就是你最初想要的效果,

如何解决:

1.按照你的第一种写法可以的

2.

oLi[i].click = (function(i){
//把i分配进去
})(i)

3.使用ES6的let试一下

看第二张图,onclick = function只是绑定事件,函数并未执行,在你触发点击事件的时候执行,这时执行oLi[i].index中的i是什么值,此时它是等于oLi.length的。

你自己都说了是闭包问题了。

第二张图 i 的值是循环结束后最后的值。

的确涉及到了一个比较隐蔽的闭包,但从需求上来说这个闭包是不应该像这样存在的。
首先JS是事件驱动,在给onclick事件赋值的过程中只是将函数体放到了一块内存区域中(匿名函数都是如此,除非用立即执行的方式进行书写),当click事件触发时才回调用这个事件,因此你在这个循环内声明的所有onclick事件在声明时都不会立即引用i的值,只有在每次执行click事件的时候会将这个值引用,这就形成了闭包。但循环体在脚本加载完以后就已经执行完毕,这个时候i已经等于了oLi.length值,再进行引用的时候所有的结果都是最后一个按钮的索引值。
在这种类型的需求中,应当将事件的声明封装成一个方法,在循环体中直接执行这个方法,才能捕捉到这个i的值。

function binde( index, obj ) {
    obj.index = index;//将索引值传递给要绑定事件的对象
    obj.onclick = function() {
        console.log( this.index );
    }
}

//循环体
for ( var i = 0; i < oLi.length; i ++ ) {
    binde( i, oLi[i] );//这个时候将i赋值到binde中,并且立即执行,这个时候i的值就被binde的形参index捕捉了
}

PS:其实在这里还是形成了闭包,在binde执行完以后onclick依然引用这个函数里的index,但逻辑上来说每个事件也应当有属于自己闭包来一一对应。

首先要纠正下 “为什么这句话一定要放在外面的函数才生效,而放在点击函数里面却会出现报错” 这句话, 其实放在里面也是可以的,但是你写的方法不对。

这个是我根据题主写的一段测试代码:

var oLi = $('li');

for (var i = 0; i < oLi.length; i++) {
    oLi.eq(i).on('click',function() { console.log('click li');
        oLi[i].index = i;
        for (var i = 0; i < oLi.length; i++) {
            oLi[i].className = i;
        }
    })
}

直接在 segmantfault 运行会报错,错误提示 VM722:5 Uncaught TypeError: Cannot set property 'index' of undefined(…)

这个错误不是由于闭包问题引起的,而是由于 变量提升 导致的,我们知道在 JS 中只有函数才有作用域,而在作用域中, 由于 JS 有变量提升机制,用 var 定义的变量会被提前到作用域的顶部声明, 但是赋值还是在原来的位置赋值,所以上面的代码在执行的时候实际上会变成这样:

var oLi = $('li');

for (var i = 0; i < oLi.length; i++) {
    oLi.eq(i).on('click',function() { console.log('click li');
        var i; // i 声明了,但是未定义
        oLi[i].index = i; // 这里 i === undefined,报错
        for (i = 0; i < oLi.length; i++) {
            oLi[i].className = i;
        }
    })
}

所以如果想使用外部的 i, 那么函数里面不能声明另一个变量 i ,否则函数是不会循着作用域链去获取外部的变量 i的,这个时候,你就知道得将函数里面的循环 i 变量名改为其他名称如 j:

var oLi = $('li');

for (var i = 0; i < oLi.length; i++) {
    oLi.eq(i).on('click',function() { console.log('click li', i);
        oLi[i].index = i;
        for (var j = 0; j < oLi.length; j++) {
            oLi[j].className = j;
        }
    })
}

这个时候就不报错了吧?但是你会发现,点击oLi的某个元素依旧报错,看打印出来的 i值, 你会发现值为 oLi的长度,这个时候就是闭包的问题了,闭包的意思你可以理解为可以获取外部对象的值,也就是说只要闭包没有结束,外部被引用的对象不会被销毁,但是你要记住,闭包引用的是变量的整个对象,而不是某个阶段的值,所以当使用闭包的时候,你获取到的永远是执行的时候引用对象最后的值,这个时候你就明白为什么 i的值为 oLi.length, 因为闭包的执行是在循环完成之后,所以当执行到 oLi[i].index = i , 此时 i 的值为 oLi.length, 所以边界溢出,报错。
解决的方法就是把i作为一个参数传入并即时执行:

var oLi = $('li');

for (var i = 0; i < oLi.length; i++) {
    (function(i){
        oLi.eq(i).on('click',function() { console.log('click li', i);
            oLi[i].index = i;
            for (var j = 0; j < oLi.length; j++) {
                oLi[j].className = j;
            }
        })
    })(i)
}

这个时候 i不再是作为外部引用的变量,而是作为参数传入, 让我们能正常拿到想要的 i 值。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题