作用域决定这个变量的生命周期及其可见性。 当我们创建了一个函数或者 {} 块,就会生成一个新的作用域。需要注意的是,通过 var 创建的变量只有函数作用域,而通过 let const 创建的变量既有函数作用域,也有块作用域。

嵌套作用域

Javascript 中函数里面可以嵌套函数,如下:

(function autorun(){
    let x = 1;
    function log(){ 
       console.log(x); 
    log();
})();
复制代码

log() 即是一个嵌套在 autorun() 函数里面的函数。在 log() 函数里面可以通过外部函数访问到变量 x 。此时, log() 函数就是一个闭包。

闭包就是内部函数,我们可以通过在一个函数内部或者 {} 块里面定义一个函数来创建闭包。

外部函数作用域

内部函数可以访问外部函数中定义的变量,即使外部函数已经执行完毕。如下:

(function autorun(){
    let x = 1;
    setTimeout(function log(){
      console.log(x);
    }, 10000);
})();
复制代码

并且,内部函数还可以访问外部函数中定义的形参,如下:

(function autorun(p){
    let x = 1;
    setTimeout(function log(){
      console.log(x);//1
      console.log(p);//10
    }, 10000);
})(10);
复制代码

外部块作用域

内部函数可以访问外部块中定义的变量,即使外部块已执行完毕,如下:

let x = 1; set Timeout( function log (){ console.log(x); }, 10000); 复制代码

词法作用域

词法作用域是指内部函数在定义的时候就决定了其外部作用域。

如下 代码:

(function autorun(){
    let x = 1;
    function log(){
      console.log(x);
    function run(fn){
      let x = 100;
      fn();
    run(log);//1
})();
复制代码

log() 函数是一个闭包,它在这里访问的是 autorun() 函数中的 x 变量,而不是 run 函数中的变量。

闭包的外部作用域是在其定义的时候已决定,而不是执行的时候。

autorun() 的函数作用域即是 log() 函数的词法作用域。

每一个作用域都有对其父作用域的引用。当我们使用一个变量的时候, Javascript引擎 会通过变量名在当前作用域查找,若没有查找到,会一直沿着作用域链一直向上查找,直到 global 全局作用域。

示例 如下:

let x0 = 0;
(function autorun1(){
 let x1 = 1;
 (function autorun2(){
   let x2 = 2;
   (function autorun3(){
     let x3 = 3;
     console.log(x0 + " " + x1 + " " + x2 + " " + x3);//0 1 2 3
    })();
  })();
})();
复制代码

我们可以看到, autorun3() 这个内部函数可以访问其自身局部变量 x3 ,也可以访问外部作用域中的 x1 x2 变量,以及全局作用域中的 x0 变量。即:闭包可以访问其外部(父)作用域中的定义的所有变量。

外部作用域执行完毕后

当外部作用域执行完毕后,内部函数还存活(仍在其他地方被引用)时,闭包才真正发挥其作用。譬如以下几种情况:

  • 在异步任务例如 timer 定时器,事件处理, Ajax 请求中被作为回调
  • 被外部函数作为返回结果返回,或者返回结果对象中引用该内部函数
  • 考虑如下的几个 示例

    Timer

    (function autorun(){
        let x = 1;
        setTimeout(function log(){
          console.log(x);
        }, 10000);
    })();
    复制代码

    变量 x 将一直存活着直到定时器的回调执行或者 clearTimeout() 被调用。 如果这里使用的是 setInterval() ,那么变量 x 将一直存活到 clearInterval() 被调用。

    译者注:原文中说变量 x 一直存活到 setTimeout() 或者 setInterval() 被调用是错误的。

    Event

    (function autorun(){
        let x = 1;
        $("#btn").on("click", function log(){
          console.log(x);
    })();
    复制代码

    当变量 x 在事件处理函数中被使用时,它将一直存活直到该事件处理函数被移除。

    (function autorun(){
        let x = 1;
        fetch("http://").then(function log(){
          console.log(x);
    })();
    复制代码

    变量 x 将一直存活到接收到后端返回结果,回调函数被执行。

    在已上几个示例中,我们可以看到, log() 函数在父函数执行完毕后还一直存活着, log() 函数就是一个闭包。

    除了 timer 定时器,事件处理, Ajax 请求等比较常见的异步任务,还有其他的一些异步 API 比如 HTML5 Geolocation WebSockets , requestAnimationFrame() 也将使用到闭包的这一特性。

    变量的生命周期取决于闭包的生命周期。被闭包引用的外部作用域中的变量将一直存活直到闭包函数被销毁。如果一个变量被多个闭包所引用,那么直到所有的闭包被垃圾回收后,该变量才会被销毁。

    闭包与循环

    闭包只存储外部变量的引用,而不会拷贝这些外部变量的值。 查看如下 示例

    function initEvents(){
      for(var i=1; i<=3; i++){
        $("#btn" + i).click(function showNumber(){
          alert(i);//4
    initEvents();
    复制代码

    在这个示例中,我们创建了3个闭包,皆引用了同一个变量 i ,且这三个闭包都是事件处理函数。由于变量 i 随着循环自增,因此最终输出的都是同样的值。

    修复这个问题最简单的方法是在 for 语句块中使用 let 变量声明,这将在每次循环中为 for 语句块创建一个新的局部变量。如下:

    function initEvents(){
      for(let i=1; i<=3; i++){
        $("#btn" + i).click(function showNumber(){
          alert(i);//1 2 3
    initEvents();
    复制代码

    但是,如果变量声明在 for 语句块之外的话,即使用了 let 变量声明,所有的闭包还是会引用同一个变量,最终输出的还是同一个值。

    闭包与封装性

    封装性意味着信息隐藏。

    函数与私有状态

    通过闭包,我们可以创建拥有私有状态的函数,闭包使得状态被封装起来。

    工厂模式与私有原型对象

    我们先来看一个通过原型创建对象的常规方式,如下:

    let todoPrototype = {
      toString : function() {
        return this.id + " " + this.userName + ": " + this.title;
    function Todo(todo){
      let newTodo = Object.create(todoPrototype);
      Object.assign(newTodo, todo);
      return newTodo;
    复制代码

    在这个例子中, todoPrototype 原型对象是一个全局对象。

    我们可以通过闭包,只用创建原型对象一次,也能够被所有 Todo 函数调用所公用,并且保证其私有性。 示例 如下:

    let Todo = (function createTodoFactory(){
      let todoPrototype = {
        toString : function() {
          return this.id + " " + this.userName + ": " + this.title;
      return function(todo){
        let newTodo = Object.create(todoPrototype);
        Object.assign(newTodo, todo);
        return newTodo;
    })();
    let todo = Todo({id : 1, title: "This is a title", userName: "Cristi", completed: false });
    复制代码

    这里, Todo() 就是一个拥有私有状态的函数。

    工厂模式与私有构造函数

    查看 如下 代码:

    let Todo = (function createTodoFactory(){
     function Todo(spec){
       Object.assign(this, spec);
     return function(spec){
       let todo = new Todo(spec);
       return Object.freeze(todo);
    })();
    复制代码

    这里, Todo() 工厂函数就是一个闭包。通过它,不管是否使用 new ,我们都可以创建不可变对象,原型对象也只用创建一次,并且它是私有的。

    let todo = Todo({title : "A description"});
    todo.title = "Another description"; 
    // Cannot assign to read only property 'title' of object
    todo.toString = function() {};
    //Cannot assign to read only property 'toString' of object
    复制代码

    而且,在内存快照中,我们可以通过构造函数名来识别这些示例对象。

    翻译功能与私有map

    通过闭包,我们可以创建一个 map ,在所有翻译调用中被使用,且是私有的。

    示例 如下:

    let translate = (function(){
      let translations = {};
      translations["yes"] = "oui";
      translations["no"]  = "non";
      return function(key){
        return translations[key];
    })();
    translate("yes"); //oui
    复制代码

    自增生成器函数

    通过闭包,我们可以创建自增生成器函数。同样,内部状态是私有的。 示例 如下:

    function createAGenerate(count, increment) {
      return function(){
        count += increment;
        return count;
    let generateNextNumber = createAGenerate(0, 1);
    console.log(generateNextNumber()); //1
    console.log(generateNextNumber()); //2
    console.log(generateNextNumber()); //3
    let generateMultipleOfTen = createAGenerate(0, 10);
    console.log(generateMultipleOfTen()); //10
    console.log(generateMultipleOfTen()); //20
    console.log(generateMultipleOfTen()); //30
    复制代码

    译者注:原文中依次输出0,1,2,0,10,20是有误的,感谢@Round的指正

    对象与私有状态

    以上示例中,我们可以创建一个拥有私有状态的函数。同时,我们也可以创建多个拥有同一私有状态的函数。基于此,我们还可以创建一个拥有私有状态的对象。

    示例 如下:

    function TodoStore(){
      let todos = [];
      function add(todo){
        todos.push(todo);
      function get(){
        return todos.filter(isPriorityTodo).map(toTodoViewModel);
      function isPriorityTodo(todo){
         return task.type === "RE" && !task.completed;
      function toTodoViewModel(todo) {
         return { id : todo.id, title : todo.title };
      return Object.freeze({
    复制代码

    TodoStore() 函数返回了一个拥有私有状态的对象。在外部,我们无法访问私有的 todos 变量,并且 add get 这两个闭包拥有相同的私有状态。在这里, TodoStore() 是一个工厂函数。

    闭包 vs 纯函数

    闭包就是那些引用了外部作用域中变量的函数。

    为了更好的理解,我们将内部函数拆成闭包和纯函数两个方面:

  • 闭包是那些引用了外部作用域中变量的函数。
  • 纯函数是那些没有引用外部作用域中变量的函数,它们通常返回一个值并且没有副作用。
  • 在上述例子中, add() get() 函数是闭包,而 isPriorityTodo() toTodoViewModel() 则是纯函数。

    闭包在函数式编程中的应用

    闭包在函数式编程中也应用广泛。譬如, underscore 源码中 函数相关小节 中的所有函数都利用了闭包这一特性。

    A function decorator is a higher-order function that takes one function as an argument and returns another function, and the returned function is a variation of the argument function — Javascript Allongé

    装饰器函数也使用了闭包的特性。

    我们来看如下 not 这个简单的装饰器函数:

    function not(fn){
      return function decorator(...args){
        return !fn.apply(this, args);
    复制代码

    decorator() 函数引用了外部作用域的fn变量,因此它是一个闭包。

    如果你想知道更多关于装饰器相关的知识,可以查看这篇 文章

    Javascript 中,局部变量会随着函数的执行完毕而被销毁,除非还有指向他们的引用。当闭包本身也被垃圾回收之后,这些闭包中的私有状态随后也会被垃圾回收。通常我们可以通过切断闭包的引用来达到这一目的。

    在这个 例子 中,我们首先创建了一个 add() 闭包。

    let add = (function createAddClosure(){
        let arr = [];
        return function(obj){
           arr.push(obj);
    })();
    复制代码

    随后,我们又定义了两个函数:

  • addALotOfObjects() 往闭包变量 arr 中加入对象。
  • clearAllObjects() 将闭包函数置为 null
  • 并且两个函数皆作为事件处理函数:

    function addALotOfObjects(){
        for(let i=1; i<=10000;i++) {
           add(new Todo(i));
    function clearAllObjects(){
        if(add){
           add = null;
    $("#add").click(addALotOfObjects);
    $("#clear").click(clearAllObjects);
    复制代码

    当我点击 Add 按钮时,将往 闭包变量 arr 中加入10000个 todo 对象,内存快照如下:

    当我点击 Clear 按钮时,我们将闭包引用置为 null 。随后,闭包变量 arr 将被垃圾回收,内存快照如下:

    避免全局变量

    Javascript 中,我们很容易创建出全局变量。任何定义在函数和 {} 块之外的变量都是全局的,定义在全局作用域中的函数也是全局的。

    这里以定义创建不同对象的工厂函数为例。为了避免将所有的工厂函数都放在全局作用域下,最简单的方法就是将他们挂在 app 全局变量下。

    示例 如下:

    let app = Loader();
    app.factory(function DataService(args){ return {}});
    app.factory(function Helper(args){ return {}});
    app.factory(function Mapper(args){ return {}});
    app.factory(function Model(args){});
    复制代码

    app.factory() 方法还可以将不同的工厂函数归类到不同的模块中。下面这个示例就是将 Timer 工厂函数归类到 tools 模块下。

    app.factory("tools")(function Timer(args){ return {}});
    复制代码

    我们可以在 app 对象上暴露一个 start 方法来作为应用的入口点,通过 回调函数中 factories 参数来访问这些工厂函数。这里 start() 函数只能被调用一次,如下:

    app.start(function startApplication(factories){
      let helper = factories.Helper();
      let dataService = factories.DataService();
      let model = factories.Model({
          dataService : dataService,
          helper : helper,
          timer : factories.tools.Timer()
    

    A Composition Root is a (preferably) unique location in an application where modules are composed together.
    Mark Seemann

    loader 对象

    让我们来将 app 完善为一个 loader 对象,示例如下:

    function Loader(){
      let modules = Object.create(null);
      let started = false;
      function getNamespaceModule(modulesText){
        let parent = modules;
        if(modulesText){
          let parts = modulesText.split('.');
          for(let i=0; i<parts.length; i++){
            let part = parts[i];
            if (typeof parent[part] === "undefined") {
              parent[part] = Object.create(null);
            parent = parent[part];
        return parent;
      function addFunction(namespace, fn){
        if(typeof(fn) !== "function") {
          throw "Only functions can be added";
        let module = getNamespaceModule(namespace);
        let fnName = fn.name;    
        module[fnName] = fn;
      function addNamespace(namespace){
        return function(fn){
          addFunction(namespace, fn)
      function factory(){
        if(typeof(arguments[0]) === "string"){
          return addNamespace(arguments[0]);
        } else {
          return addFunction(null, arguments[0]);
      function start(startApplication){
        if(started){
          throw "App can be started only once";
        startApplication(Object.freeze(modules));
        started = true;
      return Object.freeze({
        factory,
        start
    let app = Loader();
    复制代码

    factory() 方法用于添加新的工厂函数到内部变量 modules 中。

    start() 方法则会调用回调函数,在回调函数中访问内部变量。

    通过 factory() 定义工厂函数,将 start() 作为整个应用中调用各种工厂函数生成不同对象的唯一入口点,这是如此简洁优雅的方式。

    在这里,factorystart 都是闭包。

    闭包是一个可以访问外部作用域中变量的内部函数。

    这些被引用的变量直到闭包被销毁时才会被销毁。

    闭包使得 timer 定时器,事件处理,AJAX 请求等异步任务更加容易。

    可以通过闭包来达到封装性。

    最后,想获得更多关于 Javascript 函数相关知识,可以查看以下文章:

    Discover Functional Programming in JavaScript with this thorough introduction

    Discover the power of first class functions

    How point-free composition will make you a better functional programmer

    Here are a few function decorators you can write from scratch