闭包
转载:原文链接
正常情况下,定义一个函数后,会产生一个函数作用域,函数体内的局部变量只能在函数作用域中使用。当函数执行完成,函数所占的空间将会被回收,此时存在函数中的局部变量同样会被回收,便无法被访问到。倘若我们希望函数中的局部变量仍然可以被访问到,这时候就需要通过闭包了。
先看一个经典使用闭包的例子:
// 以下代码例子,无论我们点击哪个div,输出的都是doms.length的值。
// 这是因为在我们触发click事件之前,for循环已经结束了,此时i的值为doms.length。
var doms = document.querySelectorAll('div')
for(var i=0;i<doms.length;i++){
doms[i].onclick = function(){
console.log(i)
}
}
// 解决方案:文章将闭包,这里用闭包方式解决
var doms = document.querySelectorAll('div')
for(var i=0;i<doms.length;i++){
(function(i){
doms[i].onclick = function(){
console.log(i)
}
})(i)
} // 点击任何div,均输出对应的i值。
聊闭包之前,我们先看下执行上下文环境。
执行上下文
JS中每段代码都会存在一个执行上下文环境中,而任何一个执行上下文都会存在于整体的执行上下文中。根据栈先进后出的特点,全局环境产生的执行上下文会最先入栈,存在于栈底。当新的函数调用时,变会产生新的执行上下文环境,压入栈中。当函数调用完成后,这个上下文环境及其中的数据都会被销毁,并弹出栈,从而进入之前的执行上下文环境中。
需要注意的是,处于活跃的执行上下文只能通过有一个。通过代码查看执行上下文的变化过程:
var a = 1 // 1. 进入全局执行上下文
var foo = function(y){
console.log(y)
}
var boo = function(x){
var y = 10
foo(x+y) // 3. 执行foo执行上下文
}
boo(10) // 2. 进入boo执行上下文
从进入第1行代码,进入全局执行上下文环境,此时执行上下文环境只存在全局执行上下文,推入栈底。
执行到第9行代码,调用boo()函数,进入boo()函数执行上下文,为当前活跃的执行上下文,加入栈中,
执行到第7行代码,调用foo()函数,进入fo0()函数执行上下文,为当前活跃的执行上下文,加入栈中,当foo()执行完毕,则foo执行上下文出栈销毁。回到bar()函数执行上下文,继续执行代码,执行完毕则出栈销毁。最后全局上下文执行完毕,栈被清空,流程执行结束。
倘若当代码执行完毕,执行上下文环境却无法干净的销毁,这就是我们要说的闭包。
闭包
官方对闭包有一个通用解释:
一个拥有许多变量和绑定了这些变量执行环境的表达式,通常是一个函数。
闭包有两个明显的特点:
- 函数拥有的外部变量的引用,在函数返回时,该变量仍处于活跃状态。
- 闭包作为一个函数返回时,其执行上下文不会被销毁,仍处于执行上下文中。
看个例子:
function fn(){
var max = 1
return function bn(x){
if(x>max){
console.log(x)
}
}
}
var f1 = fn()
f1(2)
代码开始执行时,生成全局执行上下文环境,并压入栈底;
当执行到第9行代码时,调用fn()函数,生成fn()函数上下文环境,并压入栈中,返回bn()函数,赋值给f1。
当执行到第10行代码时,调用f1()函数,因为f1函数中包含了对max的引用,而max变量是存在fn中的,因此fn函数执行上下文环境并不会被直接销毁,依然存在于执行上下文中。
当第10行代码执行结束,bn函数执行上下文环境才会被销毁,同时max变量的引用会被释放,fn函数的执行环境一同被销毁。
最后全局上下文环境执行完毕,栈被清空,流程执行结束。
从例子我们看出,闭包所存在的最大的问题就是消耗内存!
根据闭包的特点,我们可以利用闭包的特性来实现一些特性:
一、结果缓存
如果函数调用处理耗时,我们可以将结果在内存中缓存起来,下次执行时,若存在内存中,则直接返回,提升执行效率。
let cacheObj = function(){
let cache = {} // 缓存对象
return {
search:function(key){
if(key in cache){
// 存在缓存中,直接返回
return cache[key]
}
// 耗时的函数处理
let result = dealFn(key)
// 更新缓存结果
cache[key] = result
return result
}
}
}
let cacheBox = cacheObj()
cacheBox.search(1)
二、封装
模块化思想中是将具有一定特定的属性封装到一起,只需对外暴露对应的函数,并不关心内部实现。
let stack = function(){
let stack = []
return {
push(val){
stack.push(val)
},
pop(){
return stack.pop()
}
}
}
小结
闭包使用合理,一定程度上能提高代码执行效率;如果使用不合理,则会造成内存浪费,性能下降。总结下闭包的优缺点。
优点:
- 包含函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。
- 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。
缺点:
消耗内存:通常来说,函数的活动对象会随着上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,因为闭包比一般函数消耗更多内存。
至此我们学习了闭包的特性、优缺点以及用法。