闭包是JavaScript的强大的特性,很多强大JavaScript库比如jQuery、Vue.js都使用了闭包的特性来实现的,同时,闭包也是js里的理解难点之一。
热身:循环中的闭包
1 | for (var i = 1; i <= 5; i++) { |
本意是想每隔一秒依次输出“1 2 3 4 5”,结果变成输出“6 6 6 6 6 ”。为什么会这样呢,根据作用域链上变量查找机制,setTimeout
第一个参数的函数体内的i
引用了去全局作用域里面的i
,当for循环完毕后,i
的值为6,所以输出了“6 6 6 6 6 ”。
如何实现正确的输出呢?
其实用闭包就能轻松解决这个问题:
1 | for (var i = 1; i <= 5; i++) { |
什么是闭包
1. 闭包的定义
关于JavaScript闭包的定义有很多种,每本书、每个作者都有不完全相同的描述,虽然笔者认为函数就是闭包这个定义才是最简单最直白的,但其实笔者看到过不下十种定义,到现在一种都记不住。鉴于此,我们干脆不要记住这些五花八门的定义了,只要记住了产生闭包的时机会更实际一些,面试时,把闭包产生的时机告诉面试官就可以了:
内层的作用域访问它外层函数作用域里的参数/变量/函数时,闭包就产生了。
让我们用代码来说事儿吧:
1 | function func(){//func1引用了它外层的变量a,因此func成为了闭包 |
我们在chrome浏览器的“开发者工具”里面的控制台,运行上面的代码,可以很方便看到闭包。
看上面这个图,Closure出现在Scope一栏里面,所以可以认为闭包也是一种作用域。既然闭包也是一种作用域,闭包能够解决经典的“循环中的闭包”的问题,那是不是利用作用域就能解决问题?这让人想到了关键字let,试试看吧,把本文开头的代码改造一下:
1 | for (var i = 1; i <= 5; i++) { |
果然,用let
关键字包上一个作用域,也能和闭包一样解决问题达成目的。因此可以说,闭包是一种作用域,它拷贝了一套外层函数作用域中被访问的参数、变量/函数,这个拷贝都是浅拷贝
2. 写成闭包形式有什么好处呢?
当然有好处!还是以之前的代码为例,变量a
类似于高级语言的私有属性,无法被func
外部作用域访问和修改,只有func
内部的作用域(含嵌套作用域)可以访问。这样可以实现软件设计上的封装,设计出很强大的类库、框架,比如我们常用的jQuery、AngularJS、Vue.js。
看一个ES6出现之前最常见的模块化封装的例子:
1 | //定义一个模块 |
3. 闭包有什么缺点吗?
javascript中的垃圾回收(GC)规则是这样的:如果对象不再被引用,或者对象互相引用形成数据孤岛后且没有被孤岛之外的其他对象引用,那么这些对象将会被JS引擎的垃圾回收器回收;反之,这些对象一直会保存在内存中。
由于闭包会引用包含它的外层函数作用域里的变量/函数,因此会比其他非闭包形式的函数占用更多内存。当外层函数执行完毕退出函数调用栈(call stack)的时候,外层函数作用域里变量因为被引用着,可能并不会被JS引擎的垃圾回收器回收,因而会引起内存泄漏。过度使用闭包,会导致内存占用过多,甚至内存泄漏。
1 | function A(){ |
count
是函数A中的一个变量,它的值在函数B中被改变,B每执行一次,count
的值就在原来的基础上累加1。因此,函数A中的count
一直保存在内存中,并没有因为函数A执行完毕退出函数调用栈后被JS引擎的垃圾回收器回收掉。
避免闭包导致内存泄漏的解决方法是,在函数A执行完毕退出函数调用栈之前,将不再使用的局部变量全部删除或者赋值为null。
其他使用场景介绍
除了上面介绍过的循环中的闭包、模块化封装之外,闭包还有一些其他写法。
1. 返回一个新函数
1 | function sayHello2(name) { |
调用sayHello2()
函数返回了sayAlert
,赋值给say2
。注意say2
是一个引用变量,指向一个函数本身,而不是指向一个变量。
2. 扩展全局对象的方法
下面这种利用闭包扩展全局对象,可以有效地保护私有变量,形成一定的封装、持久性。
1 | function setupSomeGlobals() { |
三个全局函数gAlertNumber
,gIncreaseNumber
,gSetNumber
指向了同一个闭包,因为它们是在同一次setupSomeGlobals()
调用中声明的。它们所指向的闭包是与setupSomeGlobals()
函数关联一个作用域,该作用域包括了num
变量的拷贝。也就是说,这三个函数操作的是同一个num
变量。
3. 延长局部变量的生命
日常开发时,Image对象经常被用于数据统计的上报,示例代码如下:
1 | var report = function(src) { |
这段代码在运行时,发现在一些低版本浏览器上存在bug,会丢失部分数据上报。原因是Image对象是report
函数中的局部变量,当report
函数调用结束后,Image对象随即被JS引擎垃圾回收器回收,而此时可能还没来得及发出http请求,所以可能导致此次上报数据的请求失败。
怎么办呢?我们可以使用闭包把Image对象封闭起来,就可以解决数据丢失的问题,代码如下:
1 | var report = (function() { |