首页 » 前端开发 » 正文

一个js/jquery点击事件与闭包的学习笔记

闭包在JavaScript的学习中一定会遇到,他是JavaScript一个非常有趣的属性,但是又非常容易出错,一不小心就弄错了。一次偶然的机会和公司2位前端开发人员一起讨论了jquery点击事件的闭包实现,在此扩充一下内容,记录下来,方便日后查阅。

最后一次更新是2017.01.15

为了方便后文描述,首先我们建立一个简单的HTML文件,内容只有8个a标签(如下),希望实现的功能是,点击任何a标签之后,提示当前a标签的所属下标,比如,当点击以下HTML中的”a标签0″时提示0;点击以下HTML中的”a标签3″时提示3。下面从原生JavaScript、jquery这2方面来简单谈谈。

    <a>a标签0</a>
    <a>a标签1</a>
    <a>a标签2</a>
    <a>a标签3</a>
    <a>a标签4</a>
    <a>a标签5</a>
    <a>a标签6</a>
    <a>a标签7</a>

原生JavaScript的实现

几乎所有的讨论闭包的JavaScript书籍都会给出以下的反面例子:

        var a = document.getElementsByTagName('a');
        for(var i= 0; i < 8; i++){
            a[i].onclick = function(){
                console.log(i);
            }
        }

以上代码的第3-5行建立了一个闭包,此段代码的最终运行结果是:当你点击任何a标签的时候,会打印出8.

为什么会全部输出8呢?对闭包有所了解的读者应该知道,上述代码实际上创建了8个闭包,他们都引用着i这个变量的值。必须清楚的是,在同一个作用域链中定义两个或者多个闭包,这些闭包共享同样的私有变量或变量[1],关联到闭包的作用域链都是“活动的”,嵌套的函数不会将作用域链内的私有成员复制一份,也不会对所绑定的变量生成静态快照[2]。所以,这8个闭包实际上引用的是同一个i的值,由于i在for循环结束后变为了8,所以最终点击任何a标签的时候,会打印出8.

要解决这个问题,这里给出两种方案(他们实质上是相同的),方案一是将此处的闭包修改为一个外部函数,进行调用,参考以下代码:

var func = function(i){
    return function(){
        console.log(i);
    }
}
var a = document.getElementsByTagName('a');
for(var i= 0; i < 8; i++){
    a[i].onclick = func(i);
}

这里,将点击事件赋值到func()函数中,这样就保证了每一个a[i]点击执行的都是都是func(i)。而上面错误的例子中,每个a[i]点击执行的都是function(){ console.log(i); }。当然也得注意func()函数需要返回一个函数,这样才能保证是在点击a标签的时候执行打印。

方案二是直接将闭包代码改为立即执行,代码如下:

var a = document.getElementsByTagName('a');
for(var i= 0; i < 8; i++){
    a[i].onclick = function(num){
        return function(){
            console.log(num) ;
        }
    }(i);
}

当然,以上代码如果你愿意,可以把变量名num改为i,这样看起来就只用了一个变量去表示,当然实际上还是一个实参到形参的变化。细心的读者还会发现,方案二实际上就是方案一,只不过把方案一中函数func的调用直接放在了onclick等号的右边。

jquery实现

jquery的实现方式很多,我们可以用each去实现,当然也可以用for循环(for循环也有多种解决方案,在这里我们只讨论和闭包相关的解决方案),下面代码先给出each的解决方案,我们不对此进行解析,请读者自己去理解:


$("a").each(function(){
    $(this).click(function(){
        console.log($(this).index());
    })
});

下面主要讨论一下for循环的解决方案。

在jquery中,点击事件都是通过回调事件来实现的(比如$().click(fn)),那么根据上面的方案二,我们很容易想到通过立即执行的方式来实现,于是有了下面的代码:

var a = $("a");
for(var i = 0; i < 8; i++){
    a.eq(i).click(function(num){
       return function(){
           console.log(num);
       }
    }(i));
}

以上代码在执行for循环时,每次都将i作为变量传递给了里边的匿名函数的num,使得num保留了每一个i的值,如此创建8个闭包,每一个都有自己的局部变量num,所以能够正常实现功能。但是要写出上面的代码不容易,因为很容易出现错误。比如,如果第7行不传入i,并在上述代码中第5行改为console.log(i);将会导致最终点击结果是8;或者在上述代码的第4行带上参数num,最终点击将会输出一个事件(在jquery中,事件的回调如果有一个参数,那么他表示的是触发当前事件的元素,在原生JavaScript中,即event)。

从上面的思考中,我们能够想到,解决这个问题的方案是需要保存每次for循环时i的值,所以也可以在立即执行时不传入参数i,而在函数中通过作用域链来获取i的值,代码如下:

var a = $("a");
for(var i = 0; i < 8; i++){
    a.eq(i).click(function(){
       var j = i;
       return function(){
           console.log(j);
       }
    }());
}

上述代码中,将i的值保存在j中,这样,每一个闭包都能引用到自己的变量j,而不是像之前的错误代码一样,引用了同一个作用域里边的i导致了错误。

ES2015的解决方案

这篇文章已经已经是3年前发布的了,对3年后的现在来说,用ES2015的解决方案要简单得多,只是简单地将var i = 0改为let i = 0即可,如下代码:

        var a = document.getElementsByTagName('a');
        for(let i= 0; i < 8; i++){
            a[i].onclick = function(){
                console.log(i);
            }
        }

原理其实很简单,因为var的作用域问题,导致虽然是在for循环语句中定义的,但实际作用域是和for循环相等的,所以在for循环外也能取到i的值,于是在最上面出错的方案中,i的值被修改保存了。但是let的作用域不同,他只能在for循环语句中,于是当JavaScript解释器发现在for循环内部需要使用i时,只能使用当前传递过来的i,而不是for循环到最后的i,因为后者在for循环结束后就被销毁了,而在a[i].onclick的方法中还要继续使用,所以采用了当前的i。关于let的细节已经不在本文范围内,所以就不深入讨论,有兴趣的读者可以自行查找一下相关资料。

结束语

本文从一个实践的例子来简单阐述了一下个人对于闭包的一些处理方式;本文也不算是一篇初级文档,因为其中没有对任何概念(包括闭包、匿名函数、作用域链等)进行详细解释;对于闭包,因为很容易出错,所以可能是一些BUG的根源,所以理解闭包很重要。本人不太推荐使用闭包,因为闭包携带了包含它的函数的作用域,因此会比其他函数占用更多内存,过度使用闭包可能会导致内存占用过多[3]。同时,由于IE自身问题,会导致一些特殊的内存泄漏问题[4]。有兴趣的读者可以自行阅读,在此不进行展开。

参考文档:

[1] .《JavaScript权威指南(第六版)》 8.6 闭包

[2] .《JavaScript权威指南(第六版)》 8.6 闭包

[3]. 《JavaScript高级程序设计(第二版)》 7.2 闭包

[4].《JavaScript高级程序设计(第二版)》 7.2.3  内存泄漏

 

本文共 2 个回复

  • Lea 2015/02/26 16:21

    请问,事件绑定属于哪种呢。 比如: Test $("#test").on("click",function(){ ... }) $(("#test").click(function(){ .... });

    • coolguy 博主 2015/02/27 11:50

      @ Lea 这种直接用click的和用on去绑定的基本可以理解成同一种,实现的话可以直接参考文中jquery实现部分。我觉得是一样的。

发表评论