理解javascript函数式编程中的闭包(closure)
闭包(closure)是函数式编程中的概念,出现于20世纪60年代,最早实现闭包的语言是Scheme,它是LISP的一种方言。之后闭包特性被其他语言广泛吸纳。
闭包的严格定义是“由函数(环境)及其封闭的自由变量组成的集合体。”这个定义对于大家来说有些晦涩难懂,所以让我们先通过例子和不那么严格的解释来说明什么是闭包,然后再举例说明一些闭包的经典用途。
什么是闭包
通俗地讲,JavaScript中每个的函数都是一个闭包,但通常意义上嵌套的函数更能够体
现出闭包的特性,请看下面这个例子:
vargenerateClosure=function(){ varcount=0; varget=function(){ count++; returncount; }; returnget; }; varcounter=generateClosure(); console.log(counter());//输出1 console.log(counter());//输出2 console.log(counter());//输出3
这段代码中,generateClosure()函数中有一个局部变量count,初值为0。还有一个叫做get的函数,get将其父作用域,也就是generateClosure()函数中的count变量增加1,并返回count的值。generateClosure()的返回值是get函数。在外部我们通过counter变量调用了generateClosure()函数并获取了它的返回值,也就是get函数,接下来反复调用几次counter(),我们发现每次返回的值都递增了1。
让我们看看上面的例子有什么特点,按照通常命令式编程思维的理解,count是generateClosure函数内部的变量,它的生命周期就是generateClosure被调用的时期,当generateClosure从调用栈中返回时,count变量申请的空间也就被释放。问题是,在generateClosure()调用结束后,counter()却引用了“已经释放了的”count变量,而且非但没有出错,反而每次调用counter()时还修改并返回了count。这是怎么回事呢?
这正是所谓闭包的特性。当一个函数返回它内部定义的一个函数时,就产生了一个闭包,闭包不但包括被返回的函数,还包括这个函数的定义环境。上面例子中,当函数generateClosure()的内部函数get被一个外部变量counter引用时,counter和generateClosure()的局部变量就是一个闭包。如果还不够清晰,下面这个例子可以帮助
你理解:
vargenerateClosure=function(){ varcount=0; varget=function(){ count++; returncount; }; returnget; }; varcounter1=generateClosure(); varcounter2=generateClosure(); console.log(counter1());//输出1 console.log(counter2());//输出1 console.log(counter1());//输出2 console.log(counter1());//输出3 console.log(counter2());//输出2
上面这个例子解释了闭包是如何产生的:counter1和counter2分别调用了generateClosure()函数,生成了两个闭包的实例,它们内部引用的count变量分别属于各自的运行环境。我们可以理解为,在generateClosure()返回get函数时,私下将get可能引用到的generateClosure()函数的内部变量(也就是count变量)也返回了,并在内存中生成了一个副本,之后generateClosure()返回的函数的两个实例counter1和counter2就是相互独立的了。
闭包的用途
1、嵌套的回调函数
闭包有两个主要用途,一是实现嵌套的回调函数,二是隐藏对象的细节。让我们先看下面这段代码示例,了解嵌套的回调函数。如下代码是在Node.js中使用MongoDB实现一个简单的增加用户的功能:
exports.add_user=function(user_info,callback){ varuid=parseInt(user_info['uid']); mongodb.open(function(err,db){ if(err){callback(err);return;} db.collection('users',function(err,collection){ if(err){callback(err);return;} collection.ensureIndex("uid",function(err){ if(err){callback(err);return;} collection.ensureIndex("username",function(err){ if(err){callback(err);return;} collection.findOne({uid:uid},function(err){ if(err){callback(err);return;} if(doc){ callback('occupied'); }else{ varuser={ uid:uid, user:user_info, }; collection.insert(user,function(err){ callback(err); }); } }); }); }); }); }); };
如果你对Node.js或MongoDB不熟悉,没关系,不需要去理解细节,只要看清楚大概的逻辑即可。这段代码中用到了闭包的层层嵌套,每一层的嵌套都是一个回调函数。回调函数不会立即执行,而是等待相应请求处理完后由请求的函数回调。我们可以看到,在嵌套的每一层中都有对callback的引用,而且最里层还用到了外层定义的uid变量。由于闭包机制的存在,即使外层函数已经执行完毕,其作用域内申请的变量也不会释放,因为里层的函数还有可能引用到这些变量,这样就完美地实现了嵌套的异步回调。
2、实现私有成员
我们知道,JavaScript的对象没有私有属性,也就是说对象的每一个属性都是曝露给外部的。这样可能会有安全隐患,譬如对象的使用者直接修改了某个属性,导致对象内部数据的一致性受到破坏等。JavaScript通过约定在所有私有属性前加上下划线(例如_myPrivateProp),表示这个属性是私有的,外部对象不应该直接读写它。但这只是个非正式的约定,假设对象的使用者不这么做,有没有更严格的机制呢?答案是有的,通过闭包可以实现。让我们再看看前面那个例子:
vargenerateClosure=function(){ varcount=0; varget=function(){ count++; returncount; }; returnget; }; varcounter=generateClosure(); console.log(counter());//输出1 console.log(counter());//输出2 console.log(counter());//输出3
我们可以看到,只有调用counter()才能访问到闭包内的count变量,并按照规则对其增加1,除此之外决无可能用其他方式找到count变量。受到这个简单例子的启发,我们可以把一个对象用闭包封装起来,只返回一个“访问器”的对象,即可实现对细节隐藏。
以上就是本文的全部内容,希望能够帮助大家更好的学习理解javascript闭包。