为什么会有闭包
js之所以会有闭包,是因为js不同于其他规范的语言,js允许一个函数中再嵌套子函数,正是因为这种允许函数嵌套,导致js出现了所谓闭包。
function a(){
function b(){
};
b();
}
a();
在js正常的函数嵌套中,父函数a调用时,嵌套的子函数b的结构,在内存中产生,然后子函数又接着调用了,子函数b就注销了,此时父函数a也就执行到尾,父函数a也会把自己函数体内调用时生成的数据从内存都注销。
function a(){
function b(){
}
return b;
}
var f=a();
这个例子中,父函数调用时,函数体内创建了子函数b,但是子函数并没有立即调用,而是返回了函数指针,以备“日后再调用”,因为“准备日后调用”,此时父函数a执行完了,就不敢注销自己的作用域中的数据了,因为一旦注销了,子函数b日后再调用时,沿着函数作用域链往上访问数据,就没有数据可以访问了,这就违背了js函数作用域链的机制。
正因此,子函数要“日后调用”,导致父函数要维持函数作用域链,而不敢注销自己的作用域,那么这个子函数就是“闭包函数”。
闭包函数在形式上有很多种。
在这个例子中,父函数v()体内定义了好几种子函数,这些子函数有的是异步事件的回调函数,会进入浏览器的事件循环池,等主线程工作结束后日后再调用这些回调函数,这些子函数,都导致父函数调用完了,不敢注销自己的作用域,因此这些子函数都是闭包函数。
js并不是为了创造闭包而创造,完全只是因为js允许函数嵌套,js函数嵌套还有个函数作用域链的机制,让父函数不敢注销自己作用域中的数据,才会产生所谓闭包。
也正因为这个闭包的特性,闭包函数可以让父函数的数据一直驻留在内存中保存,从而这也是后来js模块化的基础。
闭包与函数作用域
如果仅仅只是有函数嵌套,而没有函数作用域链,也或许不会有闭包。理解js函数作用域至关重要。
function a(){
}
函数的作用域实际上是个动态概念,上面的代码,只是定义了一个函数,并没有调用函数,函数的作用域是不存在的。只有函数a调用时,才会在内存中动态开辟一个自己的作用域,函数调用完了这个作用域又关闭了,函数运行过程中在内存创建的数据又被清除了。
function a(){
var n=1;
function b(){
n++;
console.log(n);
}
b();
b();
b();
}
a();
这个例子中,父函数a调用,首先在内存中动态开辟了作用域,然后在运算过程中,定义了函数b,子函数b()每次调用,都会开辟自己的作用域,在自己的作用域内进行运算,运算过程中访问了还处于打开状态的父函数作用域中的变量n的值,这个子函数三次调用,每次调用时候自己子作用域,访问的都是同一个变量n。但是父函数a总有执行完的时刻,总有要关闭作用域的时候。
var q='';
function a(){
var n=1;
q=function b(){
n++;
console.log(n);
}
}
a();
q();
q();
q();
这个例子中,运行父函数,函数开启了作用域,运算过程中生成了函数b,子函数b赋给了全局变量q,导致父函数a运行完了,不敢关闭自己的作用域,,让子函数b成了闭包函数,全局变量q持有了这个闭包函数。
这个得到的结果,和上面例子中常规函数嵌套,得到的效果是一样的。但是区别在于,前一个例子中,父函数a即便执行万年,也有结束要关闭作用域的时候,而这个闭包,就让它的父函数作用域永恒了。
实际上在js的作用域机制中,有一个作用域是永恒的,就是window全局作用域,只要浏览器窗口不关闭,这个windows全局作用域就是永恒的,在全局作用域中定一个函数,无论调用几次,这几次调用都可以共享操作同一个全局变量。除了window作用域可以永恒,其他的函数作用域,总有关闭的时候而无法永恒。只有闭包函数,可以让它的父函数作用域永恒,像windows全局作用域,一直在内存中存在。
当闭包函数调用时,它会动态开辟出自己的作用域,在它之上的是父函数的永恒作用域,在父函数作用域之上的,是window永恒的全局作用域。闭包函数调用完了,它自己的作用域关闭了,从内存中消失了,但是父函数的永恒作用域和window永恒作用域还一直在内存是打开的。闭包函数再次调用时,还能访问这两个作用域,可能还保存了它上次调用时候产生的数据。只有当闭包函数的引用被释放了,它的父作用域才会最终关闭(当然父函数可能创建了多个闭包函数,就需要多个闭包函数全部释放后,父函数作用域才会关闭)。
这个例子是闭包函数的一个典型应用,示例中只有两个函数嵌套,但是加上window全局作用于,一共会有三个嵌套作用域。其中for循环了三次,三次调用了匿名自执行函数,就开了三个函数作用域,开第一个作用域时保存的i的值是0,开第二个作用域保存的是1,第三个保存的是2。三次调用父函数,又创建了三个闭包函数,每个闭包函数沿着它自己的作用域链向上访问,访问的值就都不相同。三个闭包函数调用完了,它们自己的作用域就关闭了,但是各自的父函数作用域还一直在内存中处于打开状态,下次闭包函数再调用时,再接着访问它自己的作用域。就像在window全局作用域定义了一个函数,函数调用几次,全局作用域都在,每次调用都接着访问全局作用域。
闭包与js模块化
日常编码中有很多地方会不经意用到了闭包只是没有察觉,使用闭包的作用就是为了两点:形成命名空间同时保存数据。
在HTML中引入多个js文件,浏览器会从第一个执行到最后一个,这些js文件都共用一个全局作用域,这很多时候就会导致命名冲突。而如果只是为了命名空间,匿名自执行函数也可以实现。
(function(){
var a=1;
})()
alert(a);//访问不到变量a的值,会报错变量a未定义
这个例子中就借助匿名自执行函数实现了命名空间,隔离了数据,不会产生冲突,但是仅仅只是把数据封起来不提供接口有些时候或许也不行,因此这就需要闭包。
这个例子在前一个例子基础上进行了改造,a.js文件中就使用了闭包,无论这个文件引入到哪里,它的数据都是隔离的,不会会任何地方的代码产生冲突,同时它提供了闭包函数作为API接口,让其他地方以指定的方式访问数据,得到需要的结果,其他地方也不需要关心闭包结构里的数据是什么或者怎么操作的,也不需要担心引入它会与自己的代码冲突。
require.js的本质就是如此,每个模块文件就是一个大闭包。
是否使用闭包要考虑两点:隔离和数据保存。如果需要隔离数据形成命名空间,可以使用匿名自执行函数。如果需要隔离数据,同时还需要在隔离状态保存数据,保存了后面还可以继续使用,那就可以使用闭包。如果都不需要,那就使用普通函数,函数调用完作用域就关闭数据就释放了,没有保存,数据不存在了也不需要隔离了。
一个实际应用
淘宝的购物车中,一个商品点击新增数量或减少数量,它会往服务器发送一个请求保存新数量,但是如果快速连续点击,淘宝的购物车并没有跟随快速点击连续发送ajax,而是在连续点击的结束之后才发送了一个请求,把用户真正想要的数量最后才用一个请求发送了服务器,这样就减少了不必要的请求减少服务器的压力。
如果只是单纯用个click事件处理函数,然后把ajax放到处理函数中,点一次按钮就会发一次请求,连续点就会连续发。而要实现淘宝的这个效果,它要的原理是,定一个延时时间,比方1秒,单击之后过1秒种才发请求,而如果单击了之后还没有到1秒又连续单击了,那么重置这个计时,快速连续单击就一直再重置这个计时始终都没有达到一秒,就不会因为连续点击而发送请求,直到最后连续点击停下来了,过了一秒才发一个请求。
这个应用中就借助了闭包函数,实际click事件真正执行的用于发送请求的也就是里面嵌套的红框的闭包函数,每一次单击都会执行这个红框函数,它除了最终发送ajax,还要做个判断,如果上一次点击的时间,到这一次又点击的时间,这之间的间隔小于了指定的1秒,那么就不会发送ajax,同时重置这个计时。而在最初第一次单击的时候,它还需要上一次的时间,这个时间就只能在初始化时候用一个变量保存一个当前时间,然后第一次单击时候的时间与变量保存的时间进行一个对比。单击第二次时,那么该变量又保存了第一次单击时的时间,然后第二次单击的时间又与第一次单击的时间进行比较。
关键也就在于需要个变量保存上一次的时间。这时间不借助闭包函数也完全可以,就把这个变量放在全局环境下,在全局环境下定义一个全局变量startTime,反正就是保存一下上一次单击的时间。但是问题在于,购物车中有多个商品,并不会有只有一个单击按钮需要用到这个,多个按钮要用,给每个按钮都定义全局变量,startOne,startTwo,startThree...那就很麻烦,并且通过json渲染多个商品时候也不可能手动去定义这么多变量。这就必需借助闭包函数。
json在渲染多个商品时按钮时,这个debounce函数就会被多次调用,每一次调用都return返回了一个闭包函数给每个商品的button按钮的click作为其处理函数,那么每个处理函数都有一个专属的永恒父作用域,并且里面都已经自动定义了各自需要使用的startTime变量用于保存每个按钮自己计算时使用的上一次单击的时间。通过闭包解决这个问题这就非常方便。
额...
上面一个通过for()循环创建多个闭包函数,内存开多个作用域来保存不同的数据,不一定是最好的实现。这个例子中,同样是for循环创建三个了函数,但三个函数都是普通函数。由于函数在js中也是对象,因此给函数本身创建一个静态属性来保存不同的值,那么for循环创建的三个普通函数,每个函数的静态属性都保存了不同的值,而不必借助闭包结构保存不同的值,可以减少内存消耗。