浅谈对JavaScript闭包的理解

开发 前端
事实上当函数当做值类型并到处传递时, 基本都会使用闭包,如定时器,跨窗口通信,事件监听,ajax等等 基本只要使用了回调函数, 实际上就是在使用闭包。闭包是一把双刃剑 是JavaScript比较难以理解和掌握的部分, 它十分强大,却也有很大的缺陷,如何使用它完全取决于你自己。

[[171667]]

在谈闭包之前,我们首先要了解几个概念:

  • 什么是函数表达式? 与函数声明有何不同?
  • JavaScript查找标识符的机制
  • JavaScript的作用域是词法作用域
  • JavaScript的垃圾回收机制

先来说说函数表达式

什么是函数表达式? 如果function是声明中的***个词,那么就是函数声明,否则就是函数表达式。

举个例子:

  1. var foo = function(){}; //匿名函数表达式 
  2.  
  3. function foo(){})() //函数表达式,因为function不是声明中的***个词,前面还有一个“(” 
  4.  
  5. function foo(){} //函数声明  

函数表达式也分匿名函数表达式和具名函数表达式:

  1. var foo = function(){} //匿名函数表达式 
  2.  
  3. var foo = function bar(){} //具名函数表达式  

具名函数表达式要注意一点:上例中的bar标识符 只在当前的函数作用域中存在,在全局作用域中是不存在的。

函数声明与函数表达式的重要区别有:

  • 函数声明具有函数声明提升,函数表达式不会被提升
  • 函数表达式可以在表达式后跟个括号来立即执行,函数声明不行
  1. function (){})() //匿名函数表达式,且立即执行 

这种模式的函数,通常称为IIFE(Immediately Invoked Function Expresstion)代表立即执行函数表达式。

关于函数、变量声明的提升这里就不再多说了, 想了解的同学可以查阅一下相关资料

关于JavaScript执行函数时查找标识符的机制

不了解作用域链及变量对象的同学可以先查阅相关资料后再来看。

作用域链本质上是一个由指向变量对象的指针列表,它只引用但不实际包含变量对象,变量,函数等等都存在各自作用域的变量对象中,通过访问变量对象来访问它们。

只有在函数调用的时候,才会创建执行环境和作用域链,同时每个环境都只能逐级向上搜索作用域链,来查询变量和函数名等标识符。

JavaScript的作用域

JavaScript的作用域就是词法作用域而不是动态作用域

词法作用域最重要的特征是它的定义过程发生在代码的书写阶段

动态作用域的作用域链是基于调用栈的 词法作用域的作用域链是基于代码中的作用域嵌套

  1. function foo(){ 
  2.     console.log(num) 
  3.     
  4. function bar(){ 
  5.     var num = 2; 
  6.     foo(); // 1 
  7.      
  8. var num = 1; 
  9. bar();      

bar函数执行时,会执行foo函数,因为JavaScript是词法作用域,所以函数执行时,会沿着定义时的作用域链查找变量,而不是执行时,foo函数定义在全局中,所以查找到了全局的num,输出了1而不是2。

下面来说闭包

关于什么是闭包,其实有很多种说法,这取决于各自的理解,最主要的有两种:

  • Nicolas C.Zakas:闭包是指有权访问另一个函数作用域中的变量的函数
  • KYLE SIMPSON:当函数可以记住并访问所在的词法作用域时,就产生了闭包,这个函数持有对该词法作用域的引用,这个引用就叫做闭包

我个人更倾向于后者对于闭包的定义,即闭包是一个引用。下面来看一些代码:

  1. function foo() { 
  2.     var a = 5; 
  3.     return function() { 
  4.     console.log(a); 
  5.     } 
  6.  } 
  7.  
  8. var bar = foo(); 
  9. bar();       // 5  

这段代码里 foo执行时会返回一个匿名函数表达式,这个函数能够访问foo()的作用域,并且引用能引用它,然后将这个匿名函数赋值给了变量bar,让bar能引用这个匿名函数并且可以调用它。

这个例子,匿名函数在自己定义的词法作用域以外的地方成功执行。

这正是闭包强大的地方,比如通过闭包实现模块模式: 

  1. function aModule() { 
  2.  
  3.     var sometext = "module"
  4.      
  5.     function doSomething() { 
  6.         console.log(sometext); 
  7.     } 
  8.      
  9.     return { 
  10.         doSomething: doSomething 
  11.         }; 
  12.  
  13. var obj = aModule(); 
  14. obj.doSomething()   //module  

我们通过调用aModule函数创建了一个模块实例,函数返回的这个对象,实质上可以看做是这个模块的公告API,是不是有些像其它面向对象语言中的class?

再来通过闭包实现一个单例模式: 

  1. var application = function() { 
  2.      
  3.     var components = []; 
  4.     /* 
  5.     一些初始化操作 
  6.     */ 
  7.     return {              //公共API 
  8.         getComponentCount: function() { 
  9.         return components.length; 
  10.         }, 
  11.         registerComponent: function(component) { 
  12.         components.push(component); 
  13.         } 
  14.     }; 
  15. }();  

这个例子通过IIFE创建了一个单例对象,函数里返回的对象字面量是这个单例模式的公共接口。

通过闭包实现模块模式,可以做到很多强大的事情,模块模式能成功实现,最关键的是返回的API还能继续引用定义时所在的作用域,从而进行一些操作,也就是说,作用域并没有因为函数执行后被销毁,也就是没有被内存回收,之所以没有被回收是因为闭包的存在和JavaScript的垃圾回收机制。

JavaScript的垃圾回收机制

JavaScript最常用的垃圾收集方式是标记清除,垃圾收集器会给存储在内存中的所有变量都加上标记,然后去除环境中的变量,以及被环境中的变量引用的变量的标记,说明这些变量还有作用,暂时不能被删除,然后在此之后被加上标记的变量就是要删除的变量了,等待垃圾收集器对他们完成清除工作。

对函数来说,函数执行完毕后,会自动释放掉里面的变量,可是如果函数内部存在闭包,它们就不会被删除,因为这个函数还在被内部的函数所引用,所以他不会被加上标记,不会被清除,而是会一直存在内存中得不到释放!除非使用闭包的那个内部函数被销毁,外部函数才能得到释放

所以,虽然闭包强大,但是我们不能滥用它,且在没有必要的情况下尽量不要创建闭包,不然将会有大量的变量对象得不到释放,过度占用内存。

关于循环和闭包

当循环和闭包结合在一起时,经常会产生让初学者觉得匪夷所思的问题。来看一段Nicolas C.Zakas 在《JavaScript高级程序设计》中的代码: 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.         result[i] = function() { 
  5.             return i; 
  6.         }; 
  7.     } 
  8.     return result; 
  9.  

这个函数执行后,会创建一个由十个函数组成的数组,并且产生十个互不相干的函数作用域,表面上看调用第几个函数就会输出几,但是结果并不是这样 

  1. var result = createFunction(); 
  2. result[0]();  // 10 
  3. result[9]();  // 10  

产生这种奇怪的现象的原因就是之前说的,createFunction的变量对象因为闭包的存在没有被释放,注意闭包保存的是整个变量对象,而不是只保存只被引用的变量,在createFunction执行后,创建了十个函数,同时变量 i 没有被释放,依然保存在内存中,所以此时它的值保留为停止循环后的10。

当我们在外部调用函数时,函数沿着它的作用域链开始搜索所需要的变量,前面说过,JavaScript的作用域链是基于定义时的作用域嵌套,所以当我们调用某个函数比如 result[0] 它就会首先在自己的作用域里通过RSH搜索 i ,显然 i 不存在这个作用域中,于是它又沿着作用域链向上一级作用域中搜索 i ,然后找到了 i ,但是此时createFunction函数已经执行,循环也已经执行完毕了, i 的值为10,所以获取到的i,值就为10,同理,其他的函数执行时,查找的i 也会是10, 所以每个函数执行结果都是输出10。

关键所在就是尽管循环中的十个函数是在各自的迭代中分别定义的,但是它们都处于一个共享的上一级作用域中,所以它们获取到的都是一个 i

所以解决此类问题的关键就是让函数查找i时,不找到createFunction的变量对象那一级 ,因为一旦向上搜索到createFunction那里,得到的就是10。所以我们可以通过一些方法在中间来截断本该搜索到createFunction变量对象的一次查找。

首先我们可以这样: 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.     (function (){ 
  5.         result[i] = function() { 
  6.             return i; 
  7.         };})(); 
  8.     } 
  9.     return result; 
  10.  

我们通过定义一个立即执行函数表达式,在result[i]函数上一级创建了一个块级作用域,如果我们把这个块级作用域叫做a,那么它查找i时是这样一条链 result[i]->a->createFunction,之所以还会查找到createFunction中,是因为a中没有i这个变量,所以我们需要做些什么,让它搜索到a时就停下 

  1. function createFunctions() { 
  2.     var result = new Array(); 
  3.     for (var i = 0; i < 10; i++) { 
  4.         (function(i){ 
  5.         result[i] = function() { 
  6.             return i; 
  7.         };})(i); 
  8.         } 
  9.      
  10.     return result; 
  11.  

现在a这个块级作用域里定义了一个变量 i ,这个 i 与上级的 i 不会互相影响,因为它们存在各自的作用域里, 同时我们将该次迭代时的 i 值赋给了 a这个块级作用域里的 i ,即a中的 i 保存了当次迭代的 i ,result[i]在外部执行时,是这样的调用链result i -> a在a中就能找到需要的变量,不需要再向上搜索,也不会查找到值为10的 i ,所以调用哪个result[i]函数,就会输出哪个 i 。

在 ES6 中我们还可以使用 let 来解决此类问题 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.         let j = i; 
  5.         result[i] = function() { 
  6.             return j; 
  7.         }; 
  8.     } 
  9.     return result; 
  10. //输出一下 
  11. console.log(createFunction()[2]());  //2  

let会创建一个块级作用域,并在这个作用域中声明一个变量。所以我们相当于在result[i]上套了一层块级作用域 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (var i = 0; i < 10; i++) { 
  4.         //块的开始 
  5.         let j = i; 
  6.         result[i] = function() { 
  7.             return j; 
  8.         }; 
  9.         //块的结束 
  10.     } 
  11.     return result; 
  12.  

这种方式解决此类问题,与前面没有多大分别,总之就是为了不让函数调用时去查找到最上级的那个 i 。

其实,如果在for循环头部来进行let声明还会有一个有趣的行为: 

  1. function createFunction() { 
  2.     var result = []; 
  3.     for (let i = 0; i < 10; i++) {    //每次迭代,都会声明一次i,总共声明10次 
  4.         result[i] = function() { 
  5.             return i; 
  6.         }; 
  7.     } 
  8.     return result; 
  9. console.log(createFunction()[2]());  //2  

这样在for头部使用let声明, 每次迭代都会进行声明,随后每次迭代都会使用上一个迭代结束时的值来初始化这个变量。

事实上当函数当做值类型并到处传递时, 基本都会使用闭包,如定时器,跨窗口通信,事件监听,ajax等等 基本只要使用了回调函数, 实际上就是在使用闭包。

闭包是一把双刃剑 是JavaScript比较难以理解和掌握的部分, 它十分强大,却也有很大的缺陷,如何使用它完全取决于你自己。

以上皆为个人观点 如若有误 还望指正。

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2011-03-02 12:33:00

JavaScript

2017-05-22 16:08:30

前端开发javascript闭包

2021-02-21 16:21:19

JavaScript闭包前端

2016-10-27 19:26:47

Javascript闭包

2009-07-24 17:30:37

Javascript闭

2016-09-14 09:20:05

JavaScript闭包Web

2011-08-05 09:33:30

Func局部变量作用域

2020-10-14 15:15:28

JavaScript(

2011-05-25 14:48:33

Javascript闭包

2022-10-24 08:08:27

闭包编译器

2010-06-23 10:24:42

Javascript闭

2012-11-29 10:09:23

Javascript闭包

2011-03-22 10:16:48

程序员

2009-04-24 09:43:09

.NETASP.NET框架

2017-09-14 13:55:57

JavaScript

2011-05-12 18:26:08

Javascript作用域

2009-03-17 15:36:29

JavaScript循环事件

2021-01-13 11:25:12

JavaScript闭包函数

2010-07-26 11:27:58

Perl闭包

2022-05-06 16:18:00

Block和 C++OC 类lambda
点赞
收藏

51CTO技术栈公众号