资讯专栏INFORMATION COLUMN

JavaScript中的闭包

Donne / 313人阅读

摘要:权威指南第版中闭包的定义函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中成为闭包。循环中的闭包使用闭包时一种常见的错误情况是循环中的闭包,很多初学者都遇到了这个问题。

闭包简介

闭包是JavaScript的重要特性,那么什么是闭包?

《JavaScript高级程序设计(第3版)》中闭包的定义:

闭包就是指有权访问另一个函数中的变量的函数。

《JavaScript权威指南(第6版)》中闭包的定义:

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中成为“闭包”。

简单来说,在JavaScript中,函数是对象,对象是属性的集合,属性的值也可以是对象,在函数内定义函数就成为一种常见的情况,在函数内部声明函数innerFunction,在innerFunction内部有权访问外部函数的变量对象,这个函数就是我们所说的闭包。

我们来看一个简单的例子:

function checkScope(){
    var scope = "local scope";
    function f() { return scope; }
    return f();
}
checkScope();//输出为“local scope”

当函数第一次被调用时,会创建一个执行环境以及相应的作用域链,作用域链的前端,始终都是当前执行代码所在环境的变量对象,作用域链中的下一个变量对象来自包含外部环境,下一个变量则来自下一个外部环境,这样一直延续到全局执行环境。

在上边的例子中,访问scope时,内部的f()函数可以访问f()外部的变量scope,因为它在作用域链中一级一级往上找的时候可以找到scope变量。

闭包的作用

一、 模拟私有变量。在函数内创建一个闭包,闭包就可以通过自己的作用域链访问函数内部的变量,可以创建用于访问私有变量的方法。访问私有变量和私有函数的方法被称为特权方法。

function MyObject(){
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    //特权方法
    this.publicFunction = function() {
        privateVariable++;
        return privateFunction();
    };
}

二、 模仿块级作用域。 JavaScript中没有块级作用域的概念,这意味着在块语句中定义的变量,实际上是包含在函数中的。如果临时需要一些变量,使用私有作用域。

function block() {
    var a = 1;
    var b = 2;
    (function () {
        var a = 3;//覆盖了父作用域中的变量a
        var c = 4;
        //访问到了当前作用域中的变量
        console.log(a);//3
        //访问了父作用域中的变量
        console.log(b);//2
        //访问当前作用域中的变量
        console.log(c);//4
    })()
    //访问块级作用域中的变量
    console.log(c);//c is not defined
}

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。

循环中的闭包

使用闭包时一种常见的错误情况是循环中的闭包,很多初学者都遇到了这个问题。很常见的一种情况就是给页面中的多个按钮绑定点击事件,JavaScript代码如下所示:

window.onload = function(){
    var inputs = document.getElementsByTagName("input");
    for(var i = 0; i < inputs.length; i++){
        inputs[i].onclick = function(){
            console.log(i);//希望输出0,1,2,3,4...
        }
    }
}

页面中有5个按钮,根据上边的代码,我们需要的是依次点击按钮时,控制台分别打印出0,1,2,3,4,而实际上,控制台打印出来的,如下图所示:

这是为什么呢,当for循环执行完之后,i已经变成了按钮的个数5了,而所有点击函数绑定的都是同一个i,点击按钮时,打印出来的i也都变成了5了。

这一部分理解也可以参考http://www.cnblogs.com/qieguo...。

那么我们为了得到想要的结果,需要在每次循环中创建变量i的拷贝,下面提供三种方法。

第一、使用匿名包装器

window.onload = function () {
    var inputs = document.getElementsByTagName("input");
    for (var i = 0; i < inputs.length; i++) {
        (function (e) {
            inputs[i].onclick = function () {
                console.log(e);
            }
        })(i);

    }
}

依次点击按钮,控制台输出如下:

第二、从匿名包装器中返回一个函数:

window.onload = function () {
    var inputs = document.getElementsByTagName("input");
    for (var i = 0; i < inputs.length; i++) {
        inputs[i].onclick = function (num) {
            return function () {
                console.log(num);
            };
        } (i);
    }
}

首先,定义了匿名函数,并将立即执行该匿名函数的结果赋值给数组,匿名函数有一个参数num,在调用每个函数时,我们传入了变量i,函数按值传递,就将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包,这样一来,每个按钮点击函数都有自己num变量的一个副本,因此可以输出各自不同的数值了。

第三、在循环中使用let

window.onload = function () {
    var inputs = document.getElementsByTagName("input");
    for (let i = 0; i < inputs.length; i++) {
        inputs[i].onclick = function () {
            console.log(i);
        };
    }
}

let是ES6新增的命令,用法类似于var,但是所声明的变量只能在let命令所在代码块内有效。上述代码中,变量i是let声明的,当前的i只在本轮循环有效。所以每一次循环的i其实都是一个新的变量。关于let的用法可参考《ECMAScript 6 入门》中第二章。

内存泄漏

产生内存泄漏的原因是IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些问题。(JavaScript垃圾收集机制可参考《JavaScript高级程序设计(第3版)》4.3) 例如:

function assignHandle() {
    var element = document.getElementById("elementId");
    element.onclick = function () {
        //element的onclick引用了匿名函数,
        //匿名函数又通过闭包引用了element,造成了循环引用
        console.log(element.id);
    };
}

这个例子中,循环引用导致了element引用数至少为1,element所占内存就永远不会被回收,从而导致了内存泄漏问题。要解决这个问题,就需要解除对DOM对象的引用,减少引用数,确保正常回收其所占用的内存。

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再次访问这个值了,因而就可以将其占用的内存空间回收回来。

在采用引用计数策略的实现中,出现循环引用时,由于变量的引用次数永远不会是0,函数被多次调用时,就会导致大量内存得不到回收。 这一部分理解可以参考MDN中JavaScript内存管理。

结语

JavaScript闭包是极其有用的特性,但是由于闭包会携带包含它的函数的作用域,占用更多内存,过多使用闭包可能会导致内存占用过多。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88242.html

相关文章

  • JavaScript中的闭包

    摘要:闭包引起的内存泄漏总结从理论的角度将由于作用域链的特性中所有函数都是闭包但是从应用的角度来说只有当函数以返回值返回或者当函数以参数形式使用或者当函数中自由变量在函数外被引用时才能成为明确意义上的闭包。 文章同步到github js的闭包概念几乎是任何面试官都会问的问题,最近把闭包这块的概念梳理了一下,记录成以下文章。 什么是闭包 我先列出一些官方及经典书籍等书中给出的概念,这些概念虽然...

    HmyBmny 评论0 收藏0
  • JavaScript闭包,只学这篇就够了

    摘要:当在中调用匿名函数时,它们用的都是同一个闭包,而且在这个闭包中使用了和的当前值的值为因为循环已经结束,的值为。最好将闭包当作是一个函数的入口创建的,而局部变量是被添加进这个闭包的。 闭包不是魔法 这篇文章使用一些简单的代码例子来解释JavaScript闭包的概念,即使新手也可以轻松参透闭包的含义。 其实只要理解了核心概念,闭包并不是那么的难于理解。但是,网上充斥了太多学术性的文章,对于...

    CoderBear 评论0 收藏0
  • 还担心面试官问闭包

    摘要:一言以蔽之,闭包,你就得掌握。当函数记住并访问所在的词法作用域,闭包就产生了。所以闭包才会得以实现。从技术上讲,这就是闭包。执行后,他的内部作用域并不会消失,函数依然保持有作用域的闭包。 网上总结闭包的文章已经烂大街了,不敢说笔者这篇文章多么多么xxx,只是个人理解总结。各位看官瞅瞅就好,大神还希望多多指正。此篇文章总结与《JavaScript忍者秘籍》 《你不知道的JavaScri...

    tinyq 评论0 收藏0
  • 深入javascript——作用域和闭包

    摘要:注意由于闭包会额外的附带函数的作用域内部匿名函数携带外部函数的作用域,因此,闭包会比其它函数多占用些内存空间,过度的使用可能会导致内存占用的增加。 作用域和作用域链是javascript中非常重要的特性,对于他们的理解直接关系到对于整个javascript体系的理解,而闭包又是对作用域的延伸,也是在实际开发中经常使用的一个特性,实际上,不仅仅是javascript,在很多语言中都...

    oogh 评论0 收藏0
  • Javascript闭包入门(译文)

    摘要:也许最好的理解是闭包总是在进入某个函数的时候被创建,而局部变量是被加入到这个闭包中。在函数内部的函数的内部声明函数是可以的可以获得不止一个层级的闭包。 前言 总括 :这篇文章使用有效的javascript代码向程序员们解释了闭包,大牛和功能型程序员请自行忽略。 译者 :文章写在2006年,可直到翻译的21小时之前作者还在完善这篇文章,在Stackoverflow的How do Java...

    Fourierr 评论0 收藏0

发表评论

0条评论

Donne

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<