闭包是js中非常重要又难以掌握的知识点,尤其以理解了简单的闭包之后,各种框架里的语法都会对闭包大量应用。万丈高楼从地起,我们先了解一下简单的闭包吧
理解闭包
看一段简单的代码
php
function foo () {
var name = 'foo'
function bar () {
console.log('bar', name)
}
return bar
}
var fn = foo()
fn() // bar foo
在这个例子中,bar()显然是可以被正确执行的,但是有一个很特殊的现象,bar是定义在foo函数的作用域里的函数,但是执行的地方却不是在foo函数的作用域里,也就是说,它在自己定义的词法作用域以外的地方执行了 。foo()执行后,通常会期待foo()的整个内部作用域被摧毁,而闭包的神奇之处就是阻止了这个过程,原因是bar()在引用着foo()的作用域
拜bar()声明的位置所赐,它拥有涵盖foo()作用域的闭包,使得foo()的作用域可以一直保存。
什么叫闭包呢?bar()依然持有对该作用域的引用,这个引用就是闭包。也就是说,如果函数在定义时的词法作用域以外的地方被调用,就产生了闭包 ,闭包使得这个函数引用的词法作用域一直存在,这就是闭包的定义。
闭包的原理
在程序中,变量存放的位置有全局空间(堆结构)和局部空间(栈结构)。
在JavaScript中,函数的变量一般都存储在局部空间,但是如果被闭包引用的变量会存储在全局空间,也正是因为如此才能在函数执行完毕之后才能使得变量被一直使用。因此在JavaScript中无论是存储在全局空间还是局部空间的变量,声明方式都是一样的,let a = 1,因此要理解闭包,避免在声明变量的时候造成内存泄露
闭包的应用
- 对函数类型的值进行传递,当函数在别处调用时,产生了闭包
scss
function foo(){
var a = 2
function baz(){
console.log(a);
}
bar(baz)
}
function bar(fn){
fn() //妈妈快看呀,这就是闭包!
}
foo()和bar()都是定义在全局作用域下,baz()是定义在foo()的作用域下,当执行bar()时,baz()通过值传递的形式将其传到了bar的作用域里并调用,符合闭包的定义:函数在定义时的作用域以外的地方被调用,因此这里发生了闭包,读到这里,是不是觉得自己平时大多数时间都在使用闭包而不自知。
javascript
var fn
function foo(){
var a = 2
function baz(){
console.log(a);
}
fn = baz //将baz分配个全局变量
}
function bar(){
fn() //妈妈快看呀,这就是闭包!
}
间接传递函数也是闭包。无论通过何种手段将内部函数传递到所在的词法作用域以外,他都会有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包
- 计时器
前面的代码或许有些许死板,但接下来的代码和我们的日常生活息息相关
javascript
function wait(message){
setTimeout(function timer(){
console.log('message');
},1000)
}
wait('hello,closure')
在这里,timer()作为参数,将其传递给setTimeout(),此时timer是定义在wait()函数里面,但是真实调用的地方却是在这个作用域以外,很明显,这是一个闭包,因此timer会抱有wait()作用域的闭包。
深入到引擎的内部原理中,内置的工具函数setTimeout()持有的参数函数的引用,可能叫fn或者func,在setTimeout的作用域中会调用这个函数,而这个函数在这个过程中作用域保持完整。
与setTimeout类似的,许多内置的函数例如事件监听器,Ajax请求,跨窗口通信,Web Workers或者其他的异步(或者同步)任务中,只要使用了回调函数,就是在使用闭包,因为这里都涉及到将函数作为参数传递到另一个作用域中
3.循环和闭包
css
for(var i = 0; i <= 5; i++){
setTimeout(() => {
console.log(i);
},i。*1000)
}
这段代码会以每秒一次的频率输出五次6,这是因为setTimeout作为宏任务,会在for循环结束时js引擎才会去执行,然后通过rhs查找i,找到的是全局的i,此时的i是6。没有出现每隔1秒依次输出1,2,3,4,5的原因正是因为没有每次循环时没有形成闭包,没有记住之前的作用域。
scss
for(var i = 0; i <= 5; i++){
function fn(){
let j = i
setTimeout(() => {
console.log(j);
},i*100)
}
fn()
}
只需要通过闭包创建一个会被记住的作用域即可。
同样我们也知道,这样写的代码也是有一样的效果。
css
for(let i = 0; i <= 5; i++){
setTimeout(() => {
console.log(i);
},i*100)
}
let本质上是将一个块转化成一个可以被关闭的作用域,也是利用了闭包的性质
总结
闭包的本质就是函数在定义的作用域以外的地方被调用了,这个函数会引用着之前的作用域。也正是因为一直被引用着,会导致没有被垃圾清理器清除导致内存越占越大,因此需要将这个函数赋值为null,才能避免内存泄露的问题
本文是参考《你不知道的JavaScript》,相比于网上的其他文章,个人感觉并并没有写得太多太复杂,仅仅解释了最简单的闭包应用,个人认为有一种回归最原始的闭包的定义,让每一个用过js的人都可以理解,希望可以帮助你开始理解闭包,理解闭包的妙用。