让你能看懂的 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. JS对象创建常用方式及原理分析

    ====此文章是稍早前写的,本次属于文章迁移@2017.06.27==== 前言 俗话说"在js语言中,一切都对象",而且创建对象的方式也有很多种,所以今天我们做一下梳理 最简单的 ...

  2. PHP中利用redis实现消息队列处理高并发请求

    将请求存入redis 为了模拟多个用户的请求,使用一个for循环替代 //redis数据入队操作 $redis = new Redis(); $redis->connect('127.0.0.1 ...

  3. 【Android Developers Training】 25. 保存文件

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  4. PGI Compiler for OpenACC Output Syntax Highlighting

    PGI Compiler for OpenACC Output Syntax Highlighting When use the PGI compiler to compile codes with ...

  5. 手机app微信支付后台处理流程

    第一步:客户在手机app确认订单,提交订单后,app将订单详情传给后台,后台将订单存入数据库,将存入数据库的id返回给app. 第二步:这时候手机端app会让客户选择哪种付款方式,我们做的是微信,所以 ...

  6. EasyNetQ之多态发布和订阅

    你能够订阅一个接口,然后发布基于这个接口的实现. 让我们看下一个示例.我有一个接口IAnimal和两个实现Cat和Dog: public interface IAnimal { string Name ...

  7. Spark Standalone Mode Configuration

    For currently popular distributed framework Spark, here is the intro and step to configure the spark ...

  8. God 1.1.1 多线程之内存可见性

    共享变量在线程间的可见性 synchronize实现可见性 volatile实现可见性 指令重排序 as-if-serial语义 volatile使用注意事项 synchronized和volatil ...

  9. Redis 错误1067:进程意外终止,Redis不能启动,Redis启动不了

    Redis 错误1067:进程意外终止,Redis不能启动,Redis启动不了 >>>>>>>>>>>>>>> ...

  10. MyBatis的关联关系补充 多对多 继承

    多对多 一个学生有多个课程 一个课程有多个学生 思路分析 :使用一个中间表 用学生表和课程表的主键作为中间表的联合主键 1数据库表的设计 课程表 学生表 中间表 2/实体类的设计 课程类 public ...