JavaScript闭包使用姿势指南

引言

闭包就是指能够访问另一个函数作用域的变量的函数,闭包就是一个函数,能够访问其他函数的作用域中的变量,js有一个全局对象,在浏览器下是window,node下是global,所有的函数都在这个对象下,也能访问这个对象下的变量,这也就是说,js中的所有函数都是闭包

闭包的定义

函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript,函数在每次创建时生成闭包。[1]

MDN对闭包的定义中说道了词法环境和引用同时也说道了每次创建时生成闭包

参考代码

  1. const eg = ()=>{
  2. let a ='测试变量' // 被eg创建的局部变量
  3. let inner = ()=>{ // eg的内部函数,一个闭包
  4. console.log(a) // 使用了父函数中声明的变量
  5. }
  6. return inner // inner就是一个闭包函数 可以访问到eg函数的作用域
  7. }

来个有趣的例子吧

  1. function init() {
  2. var name = "Mozilla"; // name 是一个被 init 创建的局部变量
  3. function displayName() { // displayName() 是内部函数,一个闭包
  4. alert(name); // 使用了父函数中声明的变量
  5. }
  6. displayName();
  7. }
  8. init();

由于js作用域的原因,dispplayName可以访问到父级作用域init的变量name,这点母庸质疑

那么再看这个例子

  1. function makeFunc() {
  2. var name = "Mozilla";
  3. function displayName() {
  4. alert(name);
  5. }
  6. return displayName;
  7. }
  8. var myFunc = makeFunc();
  9. myFunc();

这段代码和之前的代码执行结果完全一样,其中的不同 — 也是有意思的地方 — 在于内部函数 displayName() 在执行前,被外部函数返回。你很可能认为它无法执行,那么我们再改变一下代码


  1. var name2 = 123
  2. function makeFunc() {
  3. var name = "Mozilla";
  4. function displayName() {
  5. alert(name2);
  6. }
  7. return displayName;
  8. }
  9. var myFunc = makeFunc();
  10. myFunc();

你几乎不用想就能知道结果肯定是123那么我们在返回之前的代码,为什么你就无法肯定代码的执行结果了呢

答案是,JavaScript中的函数会形成闭包。 闭包是由函数以及创建该函数的词法环境组合而成。请仔细阅读这段话,js的闭包是由函数及创建该函数的词法环境组合而成,创建它的词法环境有这个变量,所有直接使用这个变量,没有则向上查找,直至在全局环境都找不到,返回undefind

那么我们再把例子换一下


  1. var object = {
  2. name: ''object",
  3. getName: function() {
  4. return function() {
  5. console.info(this.name)
  6. }
  7. }
  8. }
  9. object.getName()() // underfined

这个时候this指向哪里呢?答案是全局因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows

现在我们换个例子吧


  1. function outer() {
  2. var a = '变量1'
  3. var inner = function () {
  4. console.info(a)
  5. }
  6. return inner // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
  7. }
  8. var inner = outer() // 获得inner闭包函数
  9. inner() //"变量1"

当程序执行完var inner = outer(),其实outer的执行环境并没有被销毁,因为他里面的变量a仍然被被inner的函数作用域链所引用,当程序执行完inner(), 这时候,inner和outer的执行环境才会被销毁调;《JavaScript高级编程》书中建议:由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。[2]

我们再来个有趣的例子


  1. function makeAdder(x) {
  2. return function(y) {
  3. return x + y;
  4. };
  5. }
  6. var add5 = makeAdder(5);
  7. var add10 = makeAdder(10);
  8. console.log(add5(2)); // 7
  9. console.log(add10(2)); // 12

add5和add10都是闭包,也共享函数的定义,但是保存了不同的词法环境,在add5中x=5而在add10中x为10

内存泄露问题

闭包函数引用外层的变量,当执行完外层函数是,变量会无法释放


  1. function showId() {
  2. var el = document.getElementById("app")
  3. el.onclick = function(){
  4. aler(el.id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
  5. }
  6. }
  7. // 改成下面function showId() {
  8. var el = document.getElementById("app")
  9. var id = el.id
  10. el.onclick = function(){
  11. aler(id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
  12. }
  13. el = null // 主动释放el
  14. }

  1. function factorial(num) {
  2. if(num<= 1) {
  3. return 1;
  4. } else {
  5. return num * factorial(num-1)
  6. }}var anotherFactorial = factorial
  7. factorial = nullanotherFactorial(4) // 报错 ,因为最好是return num* arguments.callee(num-1),arguments.callee指向当前执行函数,但是在严格模式下不能使用该属性也会报错,所以借助闭包来实现
  8. // 使用闭包实现递归function newFactorial = (function f(num){
  9. if(num<1) {return 1}
  10. else {
  11. return num* f(num-1)
  12. }
  13. }) //这样就没有问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial

用闭包解决递归调用问题

用闭包模拟私有方法

编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern)


  1. var Counter = (function() {
  2. var privateCounter = 0;
  3. function changeBy(val) {
  4. privateCounter += val;
  5. }
  6. return {
  7. increment: function() {
  8. changeBy(1);
  9. },
  10. decrement: function() {
  11. changeBy(-1);
  12. },
  13. value: function() {
  14. return privateCounter;
  15. }
  16. }
  17. })();
  18. console.log(Counter.value()); /* logs 0 */
  19. Counter.increment();
  20. Counter.increment();
  21. console.log(Counter.value()); /* logs 2 */
  22. Counter.decrement();
  23. console.log(Counter.value()); /* logs 1 */

在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。

该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。


  1. var makeCounter = function() {
  2. var privateCounter = 0;
  3. function changeBy(val) {
  4. privateCounter += val;
  5. }
  6. return {
  7. increment: function() {
  8. changeBy(1);
  9. },
  10. decrement: function() {
  11. changeBy(-1);
  12. },
  13. value: function() {
  14. return privateCounter;
  15. }
  16. }
  17. };
  18. var Counter1 = makeCounter();var Counter2 = makeCounter();
  19. console.log(Counter1.value()); /* logs 0 */
  20. Counter1.increment();
  21. Counter1.increment();
  22. console.log(Counter1.value()); /* logs 2 */
  23. Counter1.decrement();
  24. console.log(Counter1.value()); /* logs 1 */
  25. console.log(Counter2.value()); /* logs 0 */

请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

在循环中使用闭包

  1. <p id="help">Helpful notes will appear here</p>
  2. <p>E-mail: <input type="text" id="email" name="email"></p>
  3. <p>Name: <input type="text" id="name" name="name"></p>
  4. <p>Age: <input type="text" id="age" name="age"></p>
  1. function showHelp(help) {
  2. document.getElementById('help').innerHTML = help;
  3. }
  4. function setupHelp() {
  5. var helpText = [
  6. {'id': 'email', 'help': 'Your e-mail address'},
  7. {'id': 'name', 'help': 'Your full name'},
  8. {'id': 'age', 'help': 'Your age (you must be over 16)'}
  9. ];
  10. for (var i = 0; i < helpText.length; i++) {
  11. var item = helpText[i];
  12. document.getElementById(item.id).onfocus = function() {
  13. showHelp(item.help);
  14. }
  15. }
  16. }
  17. setupHelp();

看到这里你一定能想到,由于共享了同一个词法作用域,最终结果是所有的item.help都指向了helptext的最后一项,解决方法是使用let关键字或者使用匿名闭包

  1. // 匿名闭包
  2. function showHelp(help) {
  3. document.getElementById('help').innerHTML = help;
  4. }
  5. function setupHelp() {
  6. var helpText = [
  7. {'id': 'email', 'help': 'Your e-mail address'},
  8. {'id': 'name', 'help': 'Your full name'},
  9. {'id': 'age', 'help': 'Your age (you must be over 16)'}
  10. ];
  11. for (var i = 0; i < helpText.length; i++) {
  12. (function() {
  13. var item = helpText[i];
  14. document.getElementById(item.id).onfocus = function() {
  15. showHelp(item.help);
  16. }
  17. })(); // 马上把当前循环项的item与事件回调相关联起来
  18. }
  19. }
  20. setupHelp();
  21. // 使用let关键字
  22. function showHelp(help) {
  23. document.getElementById('help').innerHTML = help;
  24. }
  25. function setupHelp() {
  26. var helpText = [
  27. {'id': 'email', 'help': 'Your e-mail address'},
  28. {'id': 'name', 'help': 'Your full name'},
  29. {'id': 'age', 'help': 'Your age (you must be over 16)'}
  30. ];
  31. for (var i = 0; i < helpText.length; i++) {
  32. let item = helpText[i];
  33. document.getElementById(item.id).onfocus = function() {
  34. showHelp(item.help);
  35. }
  36. }
  37. }
  38. setupHelp();

性能考虑

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。


  1. function MyObject(name, message) {
  2. this.name = name.toString();
  3. this.message = message.toString();
  4. this.getName = function() {
  5. return this.name;
  6. };
  7. this.getMessage = function() {
  8. return this.message;
  9. };
  10. }

在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:


  1. function MyObject(name, message) {
  2. this.name = name.toString();
  3. this.message = message.toString();}MyObject.prototype = {
  4. getName: function() {
  5. return this.name;
  6. },
  7. getMessage: function() {
  8. return this.message;
  9. }
  10. };

也可以这样


  1. function MyObject(name, message) {
  2. this.name = name.toString();
  3. this.message = message.toString();
  4. }
  5. MyObject.prototype.getName = function() {
  6. return this.name;};MyObject.prototype.getMessage = function() {
  7. return this.message;
  8. };

JavaScript闭包使用姿势指南的更多相关文章

  1. 《Web 前端面试指南》1、JavaScript 闭包深入浅出

    闭包是什么? 闭包是内部函数可以访问外部函数的变量.它可以访问三个作用域:首先可以访问自己的作用域(也就是定义在大括号内的变量),它也能访问外部函数的变量,和它能访问全局变量. 内部函数不仅可以访问外 ...

  2. JavaScript闭包(Closure)

    JavaScript闭包(Closure) 本文收集了多本书里对JavaScript闭包(Closure)的解释,或许会对理解闭包有一定帮助. <你不知道的JavsScript> Java ...

  3. JavaScript 闭包环境非常奇特 - 相当于类与实例的关系?!

    JavaScript 闭包环境非常奇特 - 相当于类与实例的关系?! 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一 ...

  4. 攻破javascript面试的完美指南【译】

    攻破javascript面试的完美指南(开发者视角) 0. 前言 本文适合有一定js基础的前端开发人员阅读.原文是我google时无意发现的, 被一些知识点清晰的解析所打动, 决定翻译并记录下来.这个 ...

  5. 那些年,我们误解的 JavaScript 闭包

    说到闭包,大部分的初始者,都是谈虎色变的.最近对闭包,有了自己的理解,就感觉.其实我们误解闭包.也被网上各种说的闭包的解释给搞迷糊. 一句话:要想理解一个东西还是看权威的东西. 下面我来通俗的讲解一个 ...

  6. JavaScript ---- 闭包(什么是闭包,为什么使用闭包,闭包的作用)

    经常被问到什么是闭包? 说实话闭包这个概念很难解释.JavaScript权威指南里有这么一段话:“JavaScript函数是将要执行的代码以及执行这些代码作用域构成的一个综合体.在计算机学术语里,这种 ...

  7. JavaScript 闭包深入浅出

    闭包是什么? 闭包是内部函数可以访问外部函数的变量.它可以访问三个作用域:首先可以访问自己的作用域(也就是定义在大括号内的变量),它也能访问外部函数的变量,和它能访问全局变量. 内部函数不仅可以访问外 ...

  8. Javascript闭包和C#匿名函数对比分析

    C#中引入匿名函数,多少都是受到Javascript的闭包语法和面向函数编程语言的影响.人们发现,在表达式中直接编写函数代码是一种普遍存在的需求,这种语法将比那种必须在某个特定地方定义函数的方式灵活和 ...

  9. javascript闭包理解

    //闭包理解一 function superFun(){ var _super_a='a'; function subfuc(){ console.log(_super_a); } return su ...

随机推荐

  1. 一篇干货满满的 NFS 文章

    目录 NFS 1. 安装 2. 配置 3. 启动并添加到开机自启 4. NFS 客户端挂载 5 报错与解决办法 6. Win 系统安装 NFS client NFS 1. 安装 yum install ...

  2. Redis 的底层数据结构(整数集合)

    当一个集合中只包含整数,并且元素的个数不是很多的话,redis 会用整数集合作为底层存储,它的一个优点就是可以节省很多内存,虽然字典结构的效率很高,但是它的实现结构相对复杂并且会分配较多的内存空间. ...

  3. Bran的内核开发教程(bkerndev)-03 内核初步

    目录 内核初步 内核入口 链接脚本 汇编和链接 PS: 下面是我自己写的 64位Linux下的编译脚本 内核初步   在这节教程, 我们将深入研究一些汇编程序, 学习创建链接脚本的基础知识以及使用它的 ...

  4. OpenCV支持Qt用户界面

    在运行opencv程序的时候报下面的错误: ... The library is compiled without QT support in function ... 原因是在使用cmake安装op ...

  5. Java学习笔记之基础语法(顺序,条件,循环语句)

    顺序结构:自上而下 条件分支选择结构: if条件语句   1,一旦某一个分支确定执行以后,其他分支就不会执行.if后面的条件必须是boolean类型   2,if  后面如果不加大括号,默认相邻的下一 ...

  6. Vue核心之数据劫持

    前瞻 当前前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Anglar,Regular,Vue,React等等,它们最大的优点就是可以实现数据绑定,再也不需要手动进行DOM操作了, ...

  7. POST PUT 小解

    POST 主要是用来提交数据让服务器进行处理的,PUT主要是请求数据的. POST 提交的数据放在HTTP正文里面,而PUTT提交的数据放在url里面.

  8. ESP8266开发之旅 基础篇① 走进ESP8266的世界

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  9. jhipster入门

    环境: 阿里云linux /////////////////////////////////////////////////////////////////////yum install java-1 ...

  10. centos7将python默认版本升级

    想用centos7来写python,但是默认安装的是python2.7(python -v命令可以查看版本信息) 准备升级到python3.5.2 首先安装编译环境 yum -y install gc ...