让你能看懂的 JavaScript 闭包

没有废话,直入主题,先看一段代码:

var counter = (function() {
  var x = 1;
  return function() {
    return x++;
  };
}());

counter();    // => 1
counter();    // => 2

上面的代码,每执行一次 counter(),返回自增的计数。

这段代码用了匿名函数表达式,格式为 (function() {}()),括号内的匿名函数会自动执行,并以返回值作为表达式的值。上面的匿名函数,返回嵌套的函数:function() { return x++; }。关于匿名函数,这里不赘述。上面的匿名函数,也可以采用传参的形式:

var counter = (function(x) {
  return function() {
    return x++;
  }
}(1));

下面来分析这段代码:

匿名函数返回嵌套的函数。在这个嵌套的函数中,有变量 x,在 x 所在的作用域(JavaScript 以 function 块为作用域,即“词法作用域”)中找不到这个变量,所以会向上级作用域查找,直到找到为止。这段代码在匿名函数所在的作用域中找到了变量 x 后,返回嵌套的函数。如果变量 x 不再被使用,就会被运行环境回收,以节约资源,但在上面的代码中,x 却被嵌套的函数“记住”了,所以不会被运行环境回收,并且,这个变量不能被直接访问,只能通过定义这个变量所在的作用域内的代码操作。 这种 用函数作用域包裹并“记住”住变量 的技术,就是“闭包”。

闭包的作用域


先简单说下 JavaScript 的作用域。JavaScript 用的是词法作用域,以 function(){} 块为词法,一个块就是一个作用域,最外层为全局作用域。ES6 标准支持了块级作用域,但这不是这篇文章的重点。JavaScript 首先在当前作用域查找变量,如果找不到,就向上一级作用域查找,一直到全局作用域,直到查到为止。

现在,我们把闭包“打开”,把变量定义到全局作用域:

var x = 1;
var counter = function() {
  return x++;
};

counter();  // => 1
counter();  // => 2

上面的代码,当访问 counter() 时,也返回一个自增的计数。与闭包函数不同的是,x 被定义到了全局作用域。当访问函数时,当前作用域找不到 x,代码向一级作用域(这儿为全局作用域)查找,找到后返回函数。这儿的 x 定义在全局作用域中,可以被直接访问,而在闭包函数中,x 定义在匿名函数的作用域中,不能被直接访问。

把变量放到全局作用域,这样做会污染全局变量。JavaScript 没有包的概念,大家都是在全局作用域上操作,污染全局变量很可能导致引用的库冲突。上面的代码就定义了两个全局变量,而闭包的写法只需定义一个。最少的定义全局变量,是写 JavaScript 的原则。

我们把这段代码修改下,把计数的变量定义在函数的属性,就能做到少污染全局变量:

var counter = function() {
  return counter.x++;
};
counter.x = 1;

但上面的代码和把变量放到全局作用域,都有一个局限,变量可以被直接访问甚至是修改。

用闭包函数把变量封闭起来,只让定义变量的作用域内的代码操作变量


闭包可以减少全局变量污染,并且让变量只可以被定义变量的作用域内的代码操作。这两个问题,可以被闭包一举解决。

下面,我们把上面的计数器改为可重置的:

var counter = (function() {
  var x = 1;
  return {
    inc: function() {
      return x++;
    },
    reset: function() {
      x = 1;
    },
  };
}());

counter.inc();      // => 1;
counter.inc();      // => 2;
counter.inc();      // => 3;
counter.reset();
counter.inc();      // => 1;

increset 两个方法操作的都是定义在匿名函数的作用域中的变量 x,所以 x 可以被递增和重置。

上面的例子中,操作闭包变量的代码所都在闭包函数的作用域内。

再看一个常见的陷阱,这个陷阱的本质就是把操作闭包变量的代码放到了闭包函数的作用域外:

var fs = (function() {
  var functions = [];
  for (var i = 0; i < 10; i++) {
    functions[i] = function() {
      return i;
    };
  }
  return functions;
}());

fs[0]();    // => 10
fs[1]();    // => 10
fs[2]();    // => 10

上面的代码中, i 定义在匿名函数所在的作用域,随着循环,i 不断地被修改,当循环结束时,i = 10,所以会有上面的结果。

下面,我再演示下怎么把代码改成返回 1, 2, 3...

要返回 1, 2, 3...的话,就需要在匿名函数和嵌套的函数之间,增加一层作用域,在这层作用域中,定义一个变量 _i = i,再让嵌套的函数返回 _i 即可。这样,当代码执行到中间层的作用域时,_i 被赋值,并且在嵌套的函数作用域中保存下来。代码如下:

var fs = (function() {
  var functions = [];
  for (var i = 0; i < 10; i++) {
    functions[i] = (function() {
      var _i = i;
      return function() {
        return _i;
      };
    }());
  }
  return functions;
}());

fs[0]();    // => 0
fs[1]();    // => 1
fs[2]();    // => 2

用 ES6 的 let 语法也可以实现。let 赋值会让变量产生块级作用域:

var fs = (function() {
  var functions = [];
  for (let i = 0; i < 10; i++) {
    functions[i] = function() {
      return i;
    };
  }
  return functions;
}());

fs[0]();    // => 0
fs[1]();    // => 1
fs[2]();    // => 2

在上面的代码中,let 包裹在 for 循环控制语句里,相当于 for 循环中添加了一层作用域,本质上和前一种方法是一样的。


闭包函数是非闭包函数相比:一、减少了全局变量污染;二、让变量不能被随意修改。

最后,再分亨一个函数,这个函数可以用于给 dom 生成 ID。这个函数的本质就是一个闭包的增量,不过带了一个前缀。

var generateID = (function(prefix, x) {
  return function() {
    return prefix + x++;
  };
}("id-", 1));

generateID();   // => "id-1";
generateID();   // => "id-2";
generateID();   // => "id-3";
generateID();   // => "id-4";

本文章由 KilArmd 原创,转载请注明出处

让你能看懂的 JavaScript 闭包的更多相关文章

  1. 深入理解JavaScript闭包【译】

    在<高级程序设计>中,对于闭包一直没有很好的解释,在stackoverflow上翻出了一篇很老的<JavaScript closure for dummies>(2016)~ ...

  2. 【转】深入理解JavaScript闭包闭包(closure) (closure)

    一.什么是闭包?"官方"的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.相信很少有人能直接看懂这句话,因为他描述 ...

  3. Javascript闭包机制(转)

    原文地址:http://www.felixwoo.com/archives/247 参考:http://www.ruanyifeng.com/blog/2009/08/learning_javascr ...

  4. Javascript闭包——懂不懂由你,反正我是懂了

    摘要:“如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂.”好吧,我试着向一个27岁的朋友就是JS闭包(JavaScript closure)却彻底失败了. 越来越觉得国内没有教书育人的 ...

  5. JavaScript闭包 懂不懂由你反正我是懂了

    原文链接:http://www.jb51.net/article/28611.htm 越来越觉得国内没有教书育人的氛围,为了弄懂JS的闭包,我使出了我英语四级吃奶的劲去google上搜寻着有关闭包的解 ...

  6. 一篇文章看懂JS闭包,都要2020年了,你怎么能还不懂闭包?

     壹 ❀ 引 我觉得每一位JavaScript工作者都无法避免与闭包打交道,就算在实际开发中不使用但面试中被问及也是常态了.就我而言对于闭包的理解仅止步于一些概念,看到相关代码我知道这是个闭包,但闭包 ...

  7. JavaScript闭包理解【关键字:普通函数、闭包、解决获取元素标签索引】

    以前总觉得闭包很抽象,很难理解,所以百度一下"闭包"概览,百度的解释是:“闭包是指可以包含自由(未绑定到特定对象)变量的代码块:这些变量不是在这个代码块内或者任何全局上下文中定义的 ...

  8. JavaScript闭包模型

      JavaScript闭包模型 -----  [原创翻译]2016-09-01  09:32:22 < 一>  闭包并不神秘 本文利用JavaScript代码来阐述闭包,目的是为了使普通 ...

  9. javascript 闭包(转)

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量 ...

随机推荐

  1. curl通过调用WebService查询当前天气

    <?php /** * curl通过调用WebService查询北京的当前天气 */ header("Content-type: text/html; charset=utf-8&qu ...

  2. MySQL各模块工作配合

    MySQL各模块工作配合 在了解了 MySQL 的各个模块之后,我们再看看 MySQL 各个模块间是如何相互协同工作的 .接下来,我们通过启动 MySQL,客户端连接,请求 query,得到返回结果, ...

  3. LODOP之票据连续套打笔记<一>

    之前项目中需要使用套打,费了半天劲,最后找到LODOP,整体感觉还是不错,简单,满足大多数web套打 这是我项目中需要打印的票据 该票据每张做多显示6条数据,数据超过6条的时候需要进行分页打印,当时做 ...

  4. 既然函数也是对象,那么为什么this不指向普通函数?

    function a(){ var b=1; function exp(){ alert(this.b); } exp(); } var b=2; a(); 既然函数是对象,且exp是在a中运行的,那 ...

  5. Example006为弹出窗口加入关闭按钮

    <!-- 实例006为弹出的窗口加入关闭按钮 --> <head> <meta charset="UTF-8"> </head> & ...

  6. 3.MQTT paho

    一.概述 遥测传输 (MQTT) 是轻量级基于代理的发布/订阅的消息传输协议,设计思想是开放.简单.轻量.易于实现.这些特点使它适用于受限环境.例如,但不仅限于此: 网络代价昂贵,带宽低.不可靠. 在 ...

  7. java udp服务器设计源码

    根据个人理解写的,轻喷. 没什么大的逻辑,直接上代码. UDPServer.java package mySocket;/* * 服务器端 */import java.awt.GridLayout;i ...

  8. 一键将Web应用发布到云-Azure Web App

    我们现在越来越多的传统应用,逐步向云端迁移,原先私有云的部署模式,逐步向云端PaaS IaaS转变.例如: 我们在云端Azure中申请VM虚拟机,将我们的Web应用部署到VM的IIS中,同时做云服务的 ...

  9. 不用EF框架,完成完美实体映射,且便于维护!(AutoMapper,petapoco)

    最近,需要搭建一个新项目,在需求分析时确定数据库中需要创建多个存储过程.所以如果还是用原来EF框架生成ADO.net实体模型的话,不利于修改. 主要是解决以下两个问题: 1.比如前端需要一个值,如果存 ...

  10. oracle批量数据导入工具 sqlldr

    sqlldr工具参数: [oracle@server ~]$ sqlldr SQL*Loader: Release - Production on Wed Nov :: Copyright (c) , ...