说说JS中那个神奇的存在-Closure(闭包)

在JS的世界中,闭包一直是一个神奇的存在,它无处不在,却又很难感知;对于很多JS程序员来说,也许写了多年的程序之后也并不清楚闭包为何物,但是这也并不妨碍他们编写JS程序,从另一方面来说,也许他们无意中就写了一段产生闭包的代码,自己却毫不知情

何为闭包

在解释一个事物之前,一般都需要对其进行概念抽象,我想,闭包的概念可以定义为:所谓闭包, 即一个方法内部能够持续的访问其语义作用域(包括其嵌套作用域),即使对该方法的调用发生在该语义作用域之外(如果对语义作用域的概念不了解,可以参考:JS的作用域),这句概念定义过于抽象,下面举例来说明:

function foo() {
    var a = 2;

    function bar() {
        console.log( a );
    }

    return bar;
}

var baz = foo();

baz(); // 2

再一张图例来解释这段程序

closure_desc.png

如果基于该代码示例对闭包概念进行重新描述可以是:语义作用域foo内部的方法bar()存在对foo的内部变量a的引用,此处即产生了闭包,然后即便在语义作用域foo外通过方法bar()的引用baz()调用方法bar(),此时依然可以访问变量a,并得到它的值;需要注意的是,在bar()方法内部引用变量a时闭包已经产生,或者称之为方法bar()在语义作用域foo上产生了一个闭包,而在语义作用域foo外对bar()的调用只是让闭包显现的方式,并不是在此处产生闭包,那么进一步也可以说,在方法bar()内部通过语义作用域的变量找寻规则持有语义作用域foo的变量a的访问权即是闭包最重要的实质(也是唯一的实质)

闭包的使用

当学到一门新技术或语言的新特性,很多人都想知道其应用场景,并迫不及待的在下一次实战中进行使用,这是一种好的学习新知识的方式,但我认为对于闭包,很多时候不是你不会用,而是你的代码已经产生了闭包自己却浑然不知,所以我认为我们需要做的是了解闭包的概念并在自己的JS代码中找寻自己已经不经意间写出的闭包,进一步加深对其的认识,然后做到从无意识的产生闭包到有意识的利用闭包,需要记住:闭包在JS中无处不在,下面我将给出闭包的几种典型应用场景并简单罗列不合理的使用闭包会带来哪些问题:

  • 场景一:方法回调
    在JS中,存在着大量的回调,这种回调也是JS异步编程的基础,同时也弥补了JS单线程运作的缺陷,来看代码:
    function wait(message) {
        setTimeout( function timer(){
          console.log( message );
        }, 1000 );
    }
    
    wait( "Hello, closure!" );
    
    在函数setTimeout()中存在一个回调函数timer(),通过闭包持有其语义作用域中变量message的引用(产生闭包),那么就可以在语义作用域wait()的外部调用wait()方法并给message设值
  • 场景二:IIFE(Invoking Function Expressions Immediately,具体可参考JS的作用域
    IIFE作为一种简单的作用域隔离方法在JS中也有大量的使用,同时IIFE也经常与闭包一起使用,来看代码:
    var a = 2;
    
    (function IIFE(){
        console.log( a );
    })();
    
    通过闭包,在IIFE内部可以访问外部作用域的变量
  • 场景三:模块化编程
    随着前端工程化的发展,为了方便组织,JS的代码组织已经越来越趋向于模块化,而模块里存在大量的闭包,来看代码:
    function CoolModule() {
        var something = "cool";
        var another = [1, 2, 3];
    
        function doSomething() {
            console.log( something );
        }
    
        function doAnother() {
            console.log( another.join( " ! " ) );
        }
    
        return {
            doSomething: doSomething,
            doAnother: doAnother
        };
    }
    
    var foo = CoolModule();
    
    foo.doSomething(); // cool
    foo.doAnother(); // 1 ! 2 ! 3
    
    其实模块化的主要目的在于作用域隔离,而基于闭包,各模块内部可以互不干扰的进行自身的功能扩展,当然上面的代码只是举例,实际的模块化编写形式不是这样的,但本质不变
  • 问题一:闭包导致的大量无用数据得不到回收
    function doSomething(selector) {
        var someReallyBigData = { .. };
        process( someReallyBigData );
    
        $( selector ).click( function activator(){
            //doSomething with selector
        } );
    }
    
    这是一段很常见的处理特定元素click事件的代码,很明显由于引用了外部作用域变量selector,此时闭包已经产生,点击后函数正常触发,一切OK,但是需要注意,由于在click事件代码的上方,有关于someReallyBigData的处理,我们称之为大量临时数据的处理,正常来讲,JS的垃圾回收机制应该会及时的将这些大量临时数据回收掉,遗憾的是,由于闭包的存在,JS并不会这么做;那么该怎么办呢?一种方法是使用块级作用域,如:
    function doSomething(selector) {
        {
            var someReallyBigData = { .. };
            process( someReallyBigData );
        }
        $( selector ).click( function activator(){
            //doSomething with selector
        } );
    }
    
    改进后的写法里,将大量临时数据的处理放置于外部作用域的块级作用域中,它不会受到闭包的影响,在执行完成后会被JS的垃圾回收机制及时清理
  • 问题二:循环体中不合理的使用闭包
    在循环体内使用闭包也是JS很常见的写法,但需要注意的是,这种写法稍不注意就会产生错误的结果,来看代码:
    for (var i=1; i<=5; i++) {
        setTimeout( function timer(){
            console.log( i );
        }, i*1000 );
    }
    
    我想,写这段代码的人肯定是希望实现"每隔一秒执行一次打印操作,并且打印的值逐次递增,一共打印5次",但实际的结果却是打印5个6,究其原因在于通过timer()访问外部作用域变量i的值时,由于已经延时了一秒,此时i的值已经循环了5次,变成了6,5次调用timer(),5次皆通过闭包获取到的是i的最新值,该场景下你也许不想使用闭包,但是很遗憾,这段代码就是简单直接的产生了闭包并使用了其特性;如何规避呢?看改进后的代码:
    for (var i=1; i<=5; i++) {
        (function(j){
            setTimeout( function timer(){
                console.log( j );
            }, j*1000 );
        })( i );
    }
    
    运行这段代码后发现正是我们想要的结果,下面来分析这段代码:闭包依然存在,不同的是5次循环,每一次timer()的闭包都是针对一个新的作用域(IIFE所产生的作用域),每个新的作用域里j值各不相同(每次循环加1),所以闭包虽然依然存在,但得到的却是正确的结果

总结

本文对闭包的概念进行了解释,并给出了产生闭包的典型场景和需要注意的点,虽然实际的情况会比本文所列举的实例更复杂,更多样,但万变不离其中,把握闭包的本质,就能合理的利用闭包带来的魔力,为我所用;同时需要注意,闭包不是一个你需要努力掌握并刻意使用的技术,它只是基于JS的其他特性自然产生(就如本文中一直用的一个词是产生闭包)的一种特性,你需要做的就是了解它,拥抱它

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

推荐阅读更多精彩内容