Skip to content

Js循环事件绑定的坑与作用域 #10

@phenomLi

Description

@phenomLi
Owner

一个场景

假设现在有这么一个场景,在一个<ul/>里面有10个<li/>

<ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
</ul>

然后要求每一个<li/>点击之后都会填入该<li/>index


看起来是十分简单的需求,很多经验不足的人里面就洋洋洒洒写下一段(比如以前的我):

const li = document.querySelectorAll('ul>li');

for(var i = 0; i < li.length; i++) {
    li[i].addEventListener('click', function(e) {
        this.innerHTML = i;
    });
}

这段看起来没有任何问题,按照预期,会是这样子(每一个li被点击后):

但是的代码运行起来却是这样子(每一个li被点击后):



这就奇怪了,为什么明明写明了每次click都会将当前的i赋值给当前li的innerHTML,为什么会出现10个10呢?



函数与作用域

javascript与其他语言不同,js没有块级作用域,而只有函数作用域,也就是说,js中,在forif等代码块中定义的变量都会默认变成全局变量(window对象下的一个属性):

if(exp) {
    var x = 20;
    //这里的x是全局变量
}

for(var i = 0; i < n; i++) {
    //这里的i是全局变量
}

这意味着什么呢?作用域的作用是用作隔离代码块外部对代码块内部的影响,使作用域内部的变量独立于作用域外部的变量,也就是说作用域有着锁定变量的功能。所以这意味着在上面代码中的任何一处地方更改ix的值,都会对iffor内部产生影响。


而函数的作用域要在函数运行的那一刻才会产生,函数表达式并没有作用域。




我们再来看刚刚开始的例子。
现在很明显知道,循环变量i也是一个全局变量,在addEventListener中的回调表达式并没有产生独立的作用域,所以很明显就能想到,functiion里面的i随着循环的自增一直在变化,当用户点击<li/>时,addEventListener中的function才真正执行,产生函数作用域,但是这时的i早已变成了10。

for(var i = 0; i < li.length; i++) {
    //i是全局变量,不断地++
    li[i].addEventListener('click', function(e) {
        //这里面的i是全局的i,也一直在变化
        this.innerHTML = i;
    });
}

原来如此,看来《javascript: the good part》这么薄也不是没有道理的。



解决办法

既然已经知道了原因,那么解决的办法也应该很容易想到。


目前主流的办法有三种:

第一种:使用ES6let关键字

let关键字修复了js没有块级作用域的问题,我们用let代替var改写原始代码:

const li = document.querySelectorAll('ul>li');

//使用let
for(let i = 0; i < li.length; i++) {
    li[i].addEventListener('click', function(e) {
        this.innerHTML = i;
    });
}

let应该是最简单的方法,但是let的兼容性还比较差(至少我知道微信浏览器还不兼容),所以应用的时候尽量慎重。


第二种,用IIFE

IIFE是立即执行函数表达式(Immediately Implement Function Expression)的简称,使用IIFE我们可以让一段函数表达式马上执行。
使用IIFE改写原始代码:

const li = document.querySelectorAll('ul>li');

for(var i = 0; i < li.length; i++) {

    /*使用IIFE,让马上执行一段函数表达式,每次循环都会生成一个新的函数作用域,将变量i的当前值锁定在IIFE的作用域里面,作用域里面的i不会受到外面的i的自增的影响
    /*同时返回一个闭包,这个闭包可以访问到保存后的i的值
    /*可能有点绕,但是就是这个道理
    */
    li[i].addEventListener('click', (function(i) {
        return function(e) {
            this.innerHTML = i;
        }
    })(i));
}

这种方法用得比较多,因为兼容性是最好的。但是要理解起来会花点时间,特别是对闭包和作用域不了解的朋友。


第三种:我觉得是最优雅的方法,使用map

为什么我会觉得这种办法是最优雅呢,因为他很有functional programing的味道:

[].slice.call(document.querySelectorAll('ul>li')).map((item, index) => 
    item.addEventListener('click', e => {
        item.innerHTML = index;
    })
);

真的很美,流畅简洁直观的美。
其实这种方法跟第二种方法的本质是一样的,都是在循环的时候执行一个函数来产生函数作用域(第二种是用执行IIFE,这个是执行作为参数传入map方法的函数,循环变量就是index)。



---EOF---

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @phenomLi

        Issue actions

          Js循环事件绑定的坑与作用域 · Issue #10 · phenomLi/Blog