JavaScript的闭包
闭包
主要了解如下
- 闭包的原理
- 闭包的创建
- 闭包的优缺点及其用处
- 释放闭包
- 循环中的闭包
闭包通俗来讲就是在一个函数外部能调用这个函数内部的函数,这种情况就是使用了闭包,不过更多时候是自己已经使用了闭包,但并不知道那就是闭包。
function foo(){
var a = 1;
function bar(){
console.log(a);
}
}
就如同上面这段代码,我们想要在foo
外部调用bar
,甚至想在外部访问foo
里面的变量,根据javaScript
的作用域来看,正常情况是不可能的,函数外部是不能访问函数内部的变量的,而且这个函数执行完后就会被销毁了,所以针对这种情况下就要使用闭包了。
闭包的原理
从逻辑上看,就是要让函数外部能够调用函数内部的函数,然而,函数执行完后又会被销毁,这种时候就要想办法让函数不被销毁,怎么才能不被销毁,只要有变量一直引用它就行了。
比如我们可以把内部定义的函数返回给函数外部的变量,这时候外部的变量就可以调用里面的函数了
function foo(){
var a = 1;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
就如同上面代码这样,由于baz
有对bar
的引用,而foo
的作用域由于有资源被引用,因此不会被销毁,而bar
也可以访问foo
里定义的变量,如此闭包就创建了。
创建闭包
最简单的创建方法就是在函数内部返回一个函数
代码1
function foo(){
var a = 1;
return function bar(){
++a;
console.log(a)
}
}
var bar = foo();
bar() // 2
bar() // 3
bar() // 4
当然也有其他的方法
代码2
var fn;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
fn = baz;
}
function bar(){
fn(); // 这就是闭包
}
foo();
bar(); // 2
代码3
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz)
}
function bar(fn){
fn(); // 这就是闭包
}
foo();
上面的三段代码都创建了闭包,有什么相同之处?他们都是在函数的外部,调用某个函数内部的函数,这就是闭包。
也就是所,无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会保持对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包
好吧,或许这时候还是不明白什么时候悄悄的使用了闭包,来看下面这段代码
代码4
function waitLog(mes){
setTimeout(function time(){
console.log(mes)
}, 1000)
}
waitLog('hello world');
我相信在实际编程中或多或少会用到定时器,这也是使用了闭包,在引擎内部肯定内置了一个工具函数setTimeout(...)
,这个函数有一个参数,比如fn或func,总之它是用来接收我们传递的time()
这个函数,之后经过内部的一些算法,最终调用了这个fn或func,反馈给我们的效果就是延迟了几秒后再执行函数。
这段代码4和代码3,其实及其相似,把代码3修改下
function foo(mes){
bar(function baz(){
console.log(mes);
})
}
//把 bar 看成 引擎内置的函数
function bar(fn){
fn(); // 这就是闭包
}
foo("hello world");
把bar
看成是setTimeout
,是不是挺相似的。
再看着段jq
代码
function fn(select, mes){
$(select).click(function(){
console.log(mes)
})
}
fn("#btn1", "hello")
fn("#btn2", "world")
这段代码也在使用闭包,这几段代码有什么特点吗?它们都是使用了回调函数,当我们把一个函数作为参数传递给另一个参数时,其实就是使用了回调函数,然而,使用了回调函数,就是在使用闭包。
现在应该知道闭包如何创建了,又在什么情况下使用了闭包。
闭包的优缺点
优点
- 避免全局变量污染
- 变量长期在内存中
- 可以模拟私有变量
缺点
- 内存泄露
- 性能降低
用处
- 在外部操作函数内部变量
- 变量始终保持在内存中,不会函数执行后就被销毁
释放闭包
由于创建闭包后,会一直被引用,内存一直存在,不会被释放,所以在使用完闭包后,要手动释放闭包
function foo(){
var a = 1;
return function bar(){
++a;
console.log(a)
}
}
var bar = foo();
bar() // 2
bar() // 3
bar() // 4
bar = null; // 释放闭包
//重新创建闭包
var bar = foo();
bar() // 2
bar() // 3
bar() // 4
循环中的闭包
还是一个比较经典的代码
for(var i = 1; i<=5; ++i){
setTimeout(function time(){
console.log(i)
}, 1000)
}
上面代码并没有自己想要的结果1,2,3,4,5 ,而是一次性输出了5个6,首先,回调函数是在循环结束后再执行的,所以哪怕是把1000
,换成0
,依然是循环后再执行。
至于为什么没有一次性输出1,2,3,4,5,而是6,这与作用域有关,因为var
实际上是函数作用域,此时更是全局作用域,这5次循环的变量i,实际上都是同一个,因此循环结束后,i已经是6了,这个时候才执行回调函数,所以会一次性输出5个6。
为了一次性输出1,2,3,4,5,就要让每次循环都有一个独立的i,所以只要使用块级作用域就行了,也就是let
for(let i = 1; i<=5; ++i){
setTimeout(function time(){
console.log(i)
}, 1000)
}