JavaScript深入总结系列之闭包

238 阅读7分钟

闭包是JavaScript中最让人迷惑的特性之一,对于刚接触甚至接触比较久的JavaScript开发人员来说常常也会弄得一脸懵逼。

先看一段代码:

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

这段代码的真实意图是希望每隔1秒钟输出0,1,2,3,4,但是实际结果却是一秒后同时输出5。

要理解上面代码为什么每次都输出5而不是按顺序输出,就得弄明白什么是JavaScript闭包。

什么是闭包

广义上说,有权访问其他函数作用域中的变量的函数就是闭包。

从这个概念上讲,JavaScript中函数都可以看做是闭包,但是我们通常所说的闭包指的是一个函数包含了它在创建时作用域内任何局部变量的访问权限

理解闭包需要理解它的两个前置概念:词法作用域和作用域链,具体可见我的另外一篇文章《JavaScript深入总结系列之彻底弄懂作用域》

词法作用域即函数的执行依赖于变量作用于,这个作用域是在函数定义时决定的,而不是函数调用时决定的。

为了实现词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。

作用域链即变量对象有序访问的链接关系。

每个函数都有自己的变量对象,变量对象中保存着这个函数执行环境中变量的声明,函数声明及函数参数。当我们执行一个函数时就会在当前执行环境中的变量对象中查找,如果找不到就去上一级执行环境中的变量对象中查找,以此类推就形成了作用域链。

如何创建闭包

创建闭包的常见方式就是在一个函数内部创建另外一个函数

闭包的作用域链包含了自己的作用域,以及包含它的函数的作用域全局作用域

function doSomething(){
    var num1=0;
    function closure(){
        var num2=10;
        return num1+num2;
    }
    return closure;
}
doSomething();

上面函数closure作用域中包含了局部变量num2以及包含它的函数doSomething中的变量num1的访问权限。正常情况下doSomething()调用完毕后变量num1应该被销毁,但是内部函数closure保留了num1的引用,按照JavaScript内存管理中垃圾收集的原则,此时num1是不能被销毁的。这也是为什么我们说闭包会造成内存泄漏的原因。

注意事项

  • 1 . 闭包创建后,闭包会一直保存包含它的函数的作用域,直到闭包被销毁。
  • 2 . 闭包只能取得包含函数中任何变量的最后一个值。闭包所保存的是整个变量对象,而不是某个特殊的变量。

举个例子:

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

还是回到文章开始提到的代码,表面上看,似乎setTimeout中每个函数都应该返回自己的索引值,但实际上,每个函数都返回5。因为每个函数的作用域链中保存的都是全局活动对象,所以它们引用的都是同一个变量i。for循环执行完后,变量i的值都是5。所以每个函数取得的值都是5,也就是上面所说的只能取得包含函数中任何变量的最后一个值。

如何解决

既然循环结果不是我们想要的,那么我们如何解决这个问题呢。 我们可以采用几种方式修改上面代码:

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

这段代码的区别是,我们创建了一个函数fun,并且将变量i作为参数。执行完后发现结果是一秒后分别输出0,1,2,3,4。为什么呢?

  1. 上述5次循环执行的时间是非常短的,而setTimeout的执行是在任务队列中,也就是说5次循环执行的时间和setTimeout延迟1秒的时间比起来基本忽略,所以看到的是几乎同时输出结果;
  2. 在循环内定义了一个函数表达式,参数是i,函数的参数都是按值传递的,fun这个函数表达式中i变成了局部变量,它是外部变量i的拷贝,是独立的,具体可看我之前的文章JavaScript深入总结之函数为什么是一等公民,所以局部变量i的值会随着循环变量i的值发生变化,于是输出了0,1,2,3,4

闭包的作用

1. 模拟私有方法和变量

ES5中我们经常使用立即执行函数来创建私有变量和私有方法。

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

立即执行函数Counter创建了私有变量privateCounter,私有方法increment,decrement,value,私有方法都公用一个私有变量privateCounter,按照上面说明说的 ,闭包会一直保存包含它的函数的作用域,直到闭包被销毁。 因此每个方法操作都会直接修改privateCounter的值。

2. 延长函数作用域

function doSomething(){
    var value=1;
    return function(){
        return value;
    }
}
var value=doSomething()();

根据作用域规则,正常情况下,函数的局部变量是无法在外部被访问的,上面代码通过内部函数创建闭包,并将内部变量返回,这样外部也能获取到了,相当于延长了函数的作用域,但是这会有个弊端,doSomething()函数执行完毕后,返回函数会一直保存value变量的引用。所以如果使用完毕后不再需要应手动销毁。

3. 模拟块级作用域

在前面文章JavaScript深入总结系列之彻底弄懂作用域 中提到JavaScript(ES6之前)没有块级作用域,也是前文代码中输出相同值的原因,闭包就能解决这个问题,模拟块级作用域。

for(var i=0;i<5;i++){
  setTimeout(function(){
      console.log(i);//5,5,5,5,5
  })
}

修改后:

for(var i=0;i<5;i++){
  setTimeout((function(i){
    console.log(i);//0,1,2,3,4
  })(i),100)
}

为加深理解闭包的强大,我们再看几个例子:

例子1:

function funA(){
  var a = 10;  
  return function(){   
        alert(a);
  }
}
var b = funA();
b();//10

例子2:

function outerFn(){
  var i = 0; 
  function innerFn(){
      i++;
      console.log(i);
  }
  return innerFn;
}
var inner = outerFn();  //每次外部函数执行的时候,内部变量都会重新创建
inner();//1
inner();//2
inner();//3
var inner2 = outerFn();
inner2();//1
inner2();//2

例子3:

var i = 0;
function outerFn(){
  function innnerFn(){
       i++;
       console.log(i);
  }
  return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();//1
inner2();//2
inner1();//3
inner2();//4
//都是引用了全局变量i

例子4:

(function() { 
  var m = 0; 
  function getM() { return m; } 
  function seta(val) { m = val; } 
  window.g = getM; 
  window.f = seta; 
})(); 
f(100);
console.info(g());//闭包只能取得包含函数中任何变量的最后一个值

例子5:


var lis = document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++){
  (function(i){
      lis[i].onclick = function(){
           console.log(i);
      };
  })(i); //通过闭包模拟了块级作用域
} 

例子6:

function fn(){
   var arr = [];
   for(var i = 0;i < 5;i ++){
	 arr[i] = function(){
		 return i;
	 }
   }
   return arr;
}
var list = fn();
for(var i = 0,len = list.length;i < len ; i ++){
   console.log(list[i]());//5 5 5 5 5
} 
//闭包只能取得包含函数中任何变量的最后一个值

当然有了ES6后,闭包的很多用法也会退出历史舞台,但是通过闭包原理的理解,对我们理解JavaScript的作用域非常有帮助,也是JavaScript进阶的必经之路。