闭包理解与总结

Author Avatar
cowboy 7月 23, 2017
  • 在其它设备中阅读本文章

闭包是JavaScript比较重要同时也是不好理解的一个概念,从很早就翻看各种博文以及在书上看相关的内容,但总是一知半解,随着经验的积累,现在对闭包又有了新的认识,在此总结。
先看一下各种对闭包的定义的说法,在《JavaScript高级程序设计》中的定义为:

闭包是指有权访问另一个函数作用域中的变量的函数

很好理解,但实际情况却有多种场景
《你不知道的JavaScript》:

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行

作用域主要有两种,一种最为普遍被大多数编程语言采用的就是词法作用域,比如JavaScript,词法作用域就是定义在词法阶段的作用域,即由你在写代码时将变量和块作用域写在哪里决定的,另一种叫做动态作用域,Bash脚本等在使用。
这里的定义就不太好理解,但其实上和上面的定义也很相似。

《javascript权威指南》 (第六版)第8章第6节

从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链

所有的函数都是闭包因为有一个很大的window全局对象,你写的函数外部有一个默认的全局函数,你所有写的函数都在这个函数内部。
从广义上来讲是这样,狭义闭包即上面js高程定义。闭包的三大特性:1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收
看一个例子

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

从技术上来讲是闭包,但这个例子并不具备闭包的特性,foo()执行后,foo()内部作用域被销毁。
下面是一个典型闭包:

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  return bar
}

var baz = foo();
baz();

这里foo()内部返回bar()函数,函数的特别之处在于可以创建一个独立的作用域,第九行foo()执行返回值复制给变量baz,但bar()并没有被执行,foo()执行后按常理内部作用域被销毁,但由于返回的bar内仍然在引用变量a,因此a不会被销毁,即使执行baz()后bar内的上下文环境中的a活动对象被释放,但foo()内的仍然保留。第一个例子会被销毁的原因是闭包在函数内调用过,它的生命周期会随着父级函数结束被释放。

如果还是觉得不直观可以看以上两种形式的演化版
Alt text Alt text
左边无论执行多少次都是相同的结果,因为每次执行创建新的闭包,每次执行过后内部作用域都被销毁,右边函数中的闭包被父函数当做返回值存储在外部变量中,此时闭包函数的生命周期等于存储其的外部变量的生命周期,并且不会被销毁。

闭包常见的形式还有以下几种:

function wait(message) {

  setTimeout( function timer() {
      console,log(message);
    },1000);
}
wait( "hello,closure");

由于javascript是单线程的,而setTimeout是异步的,异步操作都会被放到“events loop”异步队列中,因此当wait()执行完后,setTimeout的函数还未执行,里面的函数仍在引用message,所以不会被清除。

循环和闭包:

for (var i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log(i)
  },i*1000);
}

结果是以每秒一次的频率输出五次6,上面已经解释了异步,因此当延迟函数执行时,循环已经完成,循环的终止条件是i不在<=5,条件首次成立时i的值是6,循环中的五个函数都被封闭在一个共享的全局作用域中,因此引用的是相同且唯一的i。
闭包与变量:

 function createFunctions() {
   var result = new Array();

   for(var i=0;i<5; i++) {
     result[i] = function() {
      return i;
     };
   }

   return result;
 }

该函数会返回一个函数数组,每个函数都返回5。因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,它们引用的是都是同一个变量i,当createFuntions()函数返回后,变量i的值为5,此时每个函数都引用着保存变量对象i的同一个变量对象,所以在每个函数内部i的值都是5。
总结为闭包只能取得包含函数中任何变量的最后一个值,闭包保存的是整个变量对象,而不是某个特殊的变量。

常用以下方法解决此类问题:
1.可以通过创建另一个匿名函数强制让闭包符合预期。

function createFunctions() {
 var result = new Array();

 for(var i=0;i<5; i++) {
   result[i] = function(num) {
      return function() {
         return num;
      };
   }(i)
 }

 return result;
 }

通过让函数立即执行,使得每个数组的每个函数引用的i与for循环同步,同时js函数参数是按值传递的,所以会将变量i的当前值复制给参数num,实际上数组内存储的函数都是function() { return num },如图
Alt text
在内层闭包中

function(num) {
      return function() {
         return num;
      }
    }

由于返回的函数在引用num,外层函数的参数num没有被销毁,因此可以取得当时num值

2.使用es6 let
Alt text
let作用于块级作用域,因此数组中各函数中的i都引用自己块级作用域内的i.