JavaScript 深入理解之闭包

160 阅读3分钟

什么是闭包

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。参照阮一峰的博客的理解是,闭包就是能够读取其他函数内部变量的函数。

当一个函数能够记住并访问到其所在的词法作用域及作用域链,特别强调是在其定义的作用域外进行的访问,此时该函数和其上层执行上下文共同构成闭包。

需要明确的几点:

  1. 闭包一定是函数对象
  2. 闭包和词法作用域,作用域链,垃圾回收机制息息相关
  3. 函数一定是在其定义的作用域外进行的访问时,才产生闭包
  4. 闭包是由该函数和其上层执行上下文共同构成

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

产生一个闭包

创建闭包最常见方式,就是在一个函数内部创建另一个函数。 下面例子中的 closure 就是一个闭包:

function func(){
  var a = 1,b = 2;
  
  function closure(){
    return a+b;
  }
  return closure;
}

闭包的用途

  1. 可以读取函数内部的变量
  2. 让这些变量的值始终保持在内存中。

来看下阮一峰博客上的例子:

function f1() {
    var n = 999;
    nAdd = function () {
        n += 1
    }

    function f2() {
        alert(n);
    }
    return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

闭包的注意事项

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

  2. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

有关闭包的面试题

for( var i = 0; i < 5; i++ ) {
	setTimeout(() => {
		console.log( i );
	}, 1000 * i)
}

由于setTimeout中的回调函数会在当前任务队列的尾部进行执行,因此上面例子中每次循环中的setTimeout回调函数记住的i的值是for循环作用域中的值,此时都是5

答案:每秒钟输出一个5,一共输出5次

那么如何做到每秒钟输出一个数,以此为0,1,2,3,4呢?这里介绍下闭包的解决方法

for( var i = 0; i < 5; i++ ) {
	((j) => {
		setTimeout(() => {
			console.log( j );
		}, 1000 * j)
	})(i)	
}

"setTimeout"方法里应用了闭包,使其内部能够记住每次循环所在的词法作用域和作用域链。记住的i的数为setTimeout的父级作用域自执行函数中的j的值,依次为0,1,2,3,4。