深入基础:js中的闭包

152 阅读4分钟

  在复习js基础的时候,闭包是个回避不了的知识点。在MDN里面对闭包对解释是一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑到一起,这样的组合就是闭包(closure)。更加通俗的说,闭包指的是引用了另外一个函数作用域中变量的函数,闭包的作用其实是在一个内层函数中可以访问其外层函数的作用域。在js的里面每创建一个函数,同时闭包也就被创建出来。所以js里面每个函数都是一个闭包。

为什么js支持闭包

我们可以从执行上下文中去解释为什么js支持闭包。比如下面的这个函数:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope()();

  这个函数的执行顺序我们可以在深入基础:js的上下文是什么中找到。当执行到f函数的时候外层的checkscopeContext已经被销毁了,但是在f的作用域链中维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

  实际上scope的值仍然可以访问到,为"local scope"。所以js实现闭包的根本在于维护了这个作用域链。

经典的闭包面试题

我们可以用这个执行上下文来分析这个经典的面试题

let data = []
for(var i =0;i<3;i++){
     data[i] = function (){
        console.log(i)
    }
}
data[0]()
data[1]()
data[2]()

这段代码大家都知道结果3,3,3,我们来分析一下这段代码执行的过程:
<1>. 在执行完循环函数的时候,全局上下文为:

globalContext = {
    VO:{
        data:[...],
        i:3
    }
}

<2>. 当执行到data[0]的时候,这个函数的作用域链为:

context = [AO,globalContext.VO]

这个时候的i值只能找到globaContext里面,所以很明显得到结果为3,后面类似。

怎么去解决问题呢?有几种方法如下:

  1. 经典解法,使用匿名闭包:
let data = []
for(var i =0;i<3;i++){
     (data[i] = function (){
        console.log(i)
    })(i)
}
data[0]()
data[1]()
data[2]()

  这个时候为什么能解决问题呢?答案就在我们添加的匿名函数上面,通过添加匿名函数,我们把i值绑定到匿名函数的变量对象上面,然后通过作用域链传给data[0]/data[1]/data[3]

匿名函数context={
    AO={
        arguments:{
            0:0,
            length:1
        }
        i:0
    }
}
data[0]Context = [AO,匿名函数context.AO,globalContext.VO]

  1. 使用es6的let语法。
let data = []
for(let i =0;i<3;i++){
     data[i] = function (){
        console.log(i)
    }
}
data[0]()
data[1]()
data[2]()

  这样做的原理是let拥有块级作用域,在每次循环的时候产生一个块级作用域,每次循环都相当于存储链一个新的变量,所以在执行到对应的函数的时候,其上下文如下:

data[0]Context = [AO,iContext{i:0}(第1次循环时候产生的作用域),globalContext.VO]

用闭包模拟私有方法

  js中不支持将方法定义为私有,但是利用js的闭包,也就是保存上下文的能力,我们可以模拟出私有方法。

//MDN中闭包模拟私有方法的示例
var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

  在上面这个例子中,通过闭包,将定义在匿名函数的私有方法通过返回的公共方法暴露出来。同时还有个好处,每次执行makeCounter的时候都是一个单独的闭包,这样生产的计数器相互之间互不影响。这样的特性有利于实现数据隐藏和封装。

闭包的性能问题

  经常性的我们会听到js闭包会引发内存泄漏,但是实际上这句话是不准确的,js里面每个函数都是闭包,不可能每个函数都会内存泄漏。我们说的闭包性能问题实际上是每次创建闭包的时候,都会保存额外的变量,所以不合理的使用闭包的话会引发性能问题。

我们看下面的这段代码:

function setFirstName(firstName){
    return function(lastName){
        return firstName+" "+lastName;
    }
}

var setLastName = setFirstName("kuitos");
var name = setLastName("lau");

  使用setLastName的时候会有一个匿名函数,我们使用这个匿名函数获得了name变量的值,但是在执行完代码之后,setlastName的执行上下文已经销毁了,但是匿名函数的中存在的变量对象一直保存着,因为垃圾回收机制因为变量对象被匿名函数保存着无法回收,这个时候就会内存泄漏。我们想解决这个问题的话就必须手动释放内存,比如setLastName = null,匿名函数的引用没有了,自然保存的变量对象也被销毁了。

  关于闭包引起的内存泄露,这个其实是浏览器的gc(Garbage Collection,垃圾回收)机制问题,在老式浏览器中(IE8以下)存在,跟js本身的闭包机制没有关系

参考文档: