JS专题: 闭包(持续总结更新)

456 阅读5分钟

闭包总是一个逃不过去的话题, 我就想我能不能写一个能让刚入门的朋友们都能理解的闭包. 所以通篇会比较大白话, 尽量少引用高大上的词. 如果有错误理解请麻烦指正

什么是闭包?

面试的是时候, 被问到 什么是闭包: 我认为的最标准回答就是:

一个函数能够 记住访问所在的词法作用域, 即使函数是在当前词法作用域之外执行

请注意标红的字,这是理解什么是真正的JS闭包的关键, 下面废话不多上代码:

例子 1: 
function foo() {
	var a = 2;
	function bar() {
		console.log( a ); // 2 
		}
    	bar(); 
}
foo();

这个是闭包吗??????? 如果按照闭包的普遍定义(因为不只有 js 才有闭包, 闭包是一个计算机术语), 这就是一个闭包, 因为 bar()foo()所包含,并且 bar() 可以访问 foo()作用域里面的值. 但是, 从 js 角度来说, 这并不是符合 js特性的闭包, 或者说这并不是我们 面试中常问到的 闭包. 请回看那句我认为的标准回答 : 即使函数实在当前作用域之外执行, 那么 bar() 显然在这个代码中没有做到. 那么什么是真的闭包?? 根据 你不知道的 js一书中给出的例子

例子 2: 
function foo() {
	var a = 2;
	function bar() { 
    	console.log( a ); 
    }
	return bar;	 
}
var baz = foo();
baz(); //2 

各位注意了!! 这个例子与上一个例子唯一不同的是, 多了一个 return, foo()函数return出了他内部的一个函数 bar(), 从而在 全局变量 baz 可以去通过这个 return 访问到 foo() 里面的变量 a, 这个就满足了我们定义的 即使函数实在当前作用域之外执行, 所以以后在面试中, 无论被问到各种形式的闭包, 都请紧扣:

一个函数能够 记住访问所在的词法作用域, 即使函数是在当前词法作用域之外执行

闭包怎么了? 为啥这么多人问闭包??

如果你回答上了什么是闭包, 紧接着面试官会问你: 闭包怎么了? 我们为什么要用闭包? 有什么问题会存在?

内存问题

无论是 node 还是 web 端, 我们都知道 js 是运行在我们 js 解释器当中,可以理解为 java中虚拟机. 而在这个解释器中, 有一个重要的内存回收机制 : 引用计数与标记清除 这里我们重点讲标记清除(自己的理解, 如果有错麻烦指正):

js 会全局搜索我们内存中存在的数据, 发现如果该内存中的某个数据已经无法被访问到(无法被访问到: 就是无法通过变量去获取), 那么这个内存数据就认为是无法到达, 因为你没有变量能去获取(废话), 所以就给你回收了, 那么关键点了: 如果我们用了闭包, return 之后赋值给了一个全局变量, 例子 2 中的 baz, 那么也就是说 baz中引用的 foo() 函数永远不会被回收掉, 因为 js 当扫描到 foo(), 发现他是被 baz 这个全局变量引用的. 如果我们不手动切断他们的链接baz=null, 那么这个foo() 永远都不会被回收掉. 所以我们写代码的时候一直需要记着闭包, 然后当这个闭包不再使用, 我们要去手动释放他们. 而在 例子 1中我们则无需担心内存泄露, 因为 foo()在执行完之后, foo()里面的变量会自动回收掉, 因为没有外界变量foo()的引用.

如果是用了闭包会有内存泄漏问题, 那么我是不是得检查我用的所有闭包?

闭包并不是内存的对手, 或者说我们离不开闭包,同样只要我们的代码封装性良好,就可以尽量减少内存泄漏. 下面我会介绍下,我写代码时候往往注意的点

  1. 承接 return的值的时候, 我们要考虑, 这个承接的变量是不是在全局的作用域下, 如果是 那么我们就考虑需要什么时候去释放掉这个变量了. 因为他肯定不会被回收掉(因为这个承接是全局, 根据标记清除,我们顶层的变量,如果不去手动释放,怎不会被释放掉)

  2. 尽量少命名顶层变量(跟第一条有点重复), 也就是我们的封装性, 我从学 js 就一直听到说, 全局污染, 尽量减少全局变量, 但是为啥? (其中之一的弊端就是标记清除, 无法自动回收被全局(顶层)变量所持有的数据)

利用闭包 进行 module 的封装

模块有两个主要特征: (1)为创建内部作用域而调用了一个包装函数; (2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。

js 在 es6 之前的 语法糖 class import export 出来之前, 都是利用闭包的特性去实现类的封装, 典型的就是 getset 方法