在JS的世界中,闭包一直是一个神奇的存在,它无处不在,却又很难感知;对于很多JS程序员来说,也许写了多年的程序之后也并不清楚闭包为何物,但是这也并不妨碍他们编写JS程序,从另一方面来说,也许他们无意中就写了一段产生闭包的代码,自己却毫不知情
何为闭包
在解释一个事物之前,一般都需要对其进行概念抽象,我想,闭包的概念可以定义为:所谓闭包, 即一个方法内部能够持续的访问其语义作用域(包括其嵌套作用域),即使对该方法的调用发生在该语义作用域之外(如果对语义作用域的概念不了解,可以参考:JS的作用域),这句概念定义过于抽象,下面举例来说明:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2
再一张图例来解释这段程序
如果基于该代码示例对闭包概念进行重新描述可以是:语义作用域
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也经常与闭包一起使用,来看代码:
通过闭包,在IIFE内部可以访问外部作用域的变量var a = 2; (function IIFE(){ console.log( a ); })();
- 场景三:模块化编程
随着前端工程化的发展,为了方便组织,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并不会这么做;那么该怎么办呢?一种方法是使用块级作用域,如:
改进后的写法里,将大量临时数据的处理放置于外部作用域的块级作用域中,它不会受到闭包的影响,在执行完成后会被JS的垃圾回收机制及时清理function doSomething(selector) { { var someReallyBigData = { .. }; process( someReallyBigData ); } $( selector ).click( function activator(){ //doSomething with selector } ); }
- 问题二:循环体中不合理的使用闭包
在循环体内使用闭包也是JS很常见的写法,但需要注意的是,这种写法稍不注意就会产生错误的结果,来看代码:
我想,写这段代码的人肯定是希望实现"每隔一秒执行一次打印操作,并且打印的值逐次递增,一共打印5次",但实际的结果却是打印5个6,究其原因在于通过for (var i=1; i<=5; i++) { setTimeout( function timer(){ console.log( i ); }, i*1000 ); }
timer()
访问外部作用域变量i
的值时,由于已经延时了一秒,此时i的值已经循环了5次,变成了6,5次调用timer()
,5次皆通过闭包获取到的是i
的最新值,该场景下你也许不想使用闭包,但是很遗憾,这段代码就是简单直接的产生了闭包并使用了其特性;如何规避呢?看改进后的代码:
运行这段代码后发现正是我们想要的结果,下面来分析这段代码:闭包依然存在,不同的是5次循环,每一次for (var i=1; i<=5; i++) { (function(j){ setTimeout( function timer(){ console.log( j ); }, j*1000 ); })( i ); }
timer()
的闭包都是针对一个新的作用域(IIFE所产生的作用域),每个新的作用域里j
值各不相同(每次循环加1),所以闭包虽然依然存在,但得到的却是正确的结果
总结
本文对闭包的概念进行了解释,并给出了产生闭包的典型场景和需要注意的点,虽然实际的情况会比本文所列举的实例更复杂,更多样,但万变不离其中,把握闭包的本质,就能合理的利用闭包带来的魔力,为我所用;同时需要注意,闭包不是一个你需要努力掌握并刻意使用的技术,它只是基于JS的其他特性自然产生(就如本文中一直用的一个词是产生闭包)的一种特性,你需要做的就是了解它,拥抱它