【重学JS之路】闭包

400 阅读3分钟

根据《高程》中所讲:闭包是指有权访问另一个函数作用域中的变量的函数。 《Javascript权威指南》中指出,从技术角度讲,所有的javascript函数都是闭包。 闭包,之前感觉很神秘,今天我们来揭开它的面纱,看看究竟干了什么! 我们举个例子:

function scope() {
  let a = 1;
  return function () {
    return a;
  }
}
let foo = scope();
foo();

根据前面所写的《执行上下文》中我们可以找到当解析代码时,会执行上下文栈,那我们按照执行上下文来看一下函数内部都做了哪些事情。

var scope = [];
scope.push(globalContext = {
  this: <Global Object>,
  LE: {
    ER: {
      scope: <func>,
      foo: <uninitialized>,
    },
    outer: null
  },
  VR: {
    ER: {},
    outer: null
  }
});
scope.push(<scope>, scopefunctionContext = {
  this: <Global Object>,
  LE: {
    ER: {
      arguments: {
        length: 0
      }
    },
    outer: <globalContext>
  },
  VR: {
    ER: {},
    outer: null
  }
});
scope.pop();
scope.push(<foo>, foofunctionContext = {
  this: <Global Object>,
  LE: {
    ER: {
      arguments: {
        length: 0
      }
    },
    outer: <scopefunctionContext>
  },
  VR: {
    ER: {},
    outer: null
  }
});
scope.pop();

其实会很好奇,scope函数执行完后明明已经在执行栈中移除了,为什么foo函数依旧能访问到其内部的变量。是因为foo函数的对外部引用是scope的词法环境,这个环境还没有消失。正因为JS有这个特点,所以才会生成闭包这个概念。

下面举几个常用的面试题,来自《高程》:

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      console.log(i);
    }
  }
  return result;
}
createFunctions().forEach(item => {
  item();
})

运行上述之后,发现最后输出10个10,为什么不是0-9呢?我们来分析一下,已result[0]为例,在result[0]运行之前,全局上下文是这样的:

var scope = [];
scope.push(globalContext = {
  this: <Global Object>,
  LE: {
    ER: {
      createFunctions: <func>,
    },
    outer: null
  },
  VR: {
    ER: {
      result: [....],
      i: 10
    },
    outer: null
  }
})

当result[0]运行时,它的函数上下文发生变化:

functionContext = {
  this: <Global Object>,
   LE: {
    ER: {},
    outer: <createFunctionsContext>
  },
  VR: {
    ER: {},
    outer: <createFunctionsContext>
  }
}

由于result[0]中没有定义i,所以就会向外部的词法环境中查找,最后找到i,输出10。

如果想输出预期结果0-9,高程中也给出了解决方案:

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function (num) {
      return function () {
        console.log(num);
      }
    }(i)
  }
  return result;
}
createFunctions().forEach(item => {
  item();
})

当再次执行result[0]时,这个匿名函数的上下文:

functionContext = {
  this: <Global Object>,
   LE: {
    ER: {
      匿名function: <func>
    },
    outer: <createFunctionsContext>
  },
  VR: {
    ER: {
      arguments: {
        0: 0,
        length: 1
      },
      num: 0
    },
    outer: <createFunctionsContext>
  }
}

当匿名函数执行时:

匿名functionContext = {
  this: <Global Object>,
   LE: {
    ER: {},
    outer: <functionContext>
  },
  VR: {
    ER: {
      arguments: {
        length: 0
      },
      num: 0
    },
    outer: <functionContext>
  }
}

同样其内部没有num变量,那它就会去外部的此法环境中查找,找到了functionContext,functionContext内部的num为0,则输出0。这样就完美解决了这个问题,也体现了闭包的作用,但现在有let了这种就用的很少了。闭包虽然能解决一些问题,但是尽量还是要少用闭包,因为其外部的词法环境已经销毁了,但其内部还在引用,这样的话闭包过多会造成内存泄漏,最简单直接的办法就是,执行完后设置为null,这样就销毁了。