写点什么

重学 JS | 聊聊闭包

用户头像
梁龙先森
关注
发布于: 2021 年 01 月 09 日
重学JS | 聊聊闭包

正常情况下,定义一个函数后,会产生一个函数作用域,函数体内的局部变量只能在函数作用域中使用。当函数执行完成,函数所占的空间将会被回收,此时存在函数中的局部变量同样会被回收,便无法被访问到。倘若我们希望函数中的局部变量仍然可以被访问到,这时候就需要通过闭包了。


先看一个经典使用闭包的例子:

// 以下代码例子,无论我们点击哪个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()函数执行上下文,继续执行代码,执行完毕则出栈销毁。最后全局上下文执行完毕,栈被清空,流程执行结束。


倘若当代码执行完毕,执行上下文环境却无法干净的销毁,这就是我们要说的闭包。


官方对闭包有一个通用解释:

一个拥有许多变量和绑定了这些变量执行环境的表达式,通常是一个函数。

闭包有两个明显的特点:

  1. 函数拥有的外部变量的引用,在函数返回时,该变量仍处于活跃状态。

  2. 闭包作为一个函数返回时,其执行上下文不会被销毁,仍处于执行上下文中。

看个例子:

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 = cache()cacheBox.search(1)
复制代码

二、封装

模块化思想中是将具有一定特定的属性封装到一起,只需对外暴露对应的函数,并不关心内部实现。

let stack = function(){	let stack = []  return {  	push(val){    	stack.push(val)    },    pop(){    	return stack.pop()    }  }}
复制代码


小结

闭包使用合理,一定程度上能提高代码执行效率;如果使用不合理,则会造成内存浪费,性能下降。总结下闭包的优缺点。

优点:

  1. 包含函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染。

  2. 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。

缺点:

消耗内存:通常来说,函数的活动对象会随着上下文环境一起被销毁,但是由于闭包引用的是外部函数的活动对象,因此这个活动对象无法被销毁,因为闭包比一般函数消耗更多内存。


至此我们学习了闭包的特性、优缺点以及用法。


发布于: 2021 年 01 月 09 日阅读数: 22
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
重学JS | 聊聊闭包