JavaScript闭包浅析

基本概念

闭包是函数和声明该函数的词法环境的组合。这个环境包含了这个闭包创建时所能访问的所有局部变量。并且它无视js的垃圾回收机制,在外层函数执行完毕后并不会被销毁。

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

一般来说,在makeAdder函数执行完毕之后,变量x应当是不能够访问的。但由于无名函数的存在形成了闭包,导致函数变量add5和add10仍然可以访问x。(分别为5和10)

作用

闭包的作用是能将函数与某些数据(环境)联系起来。这显然类似于一个对象用法。

比如我们想将某些HTML DOM树上的元素绑定显示特定的内容。显然绑定的方法都是类似的,只是要绑定的数据有所区别。这里我们就能使用闭包将数据和固定的函数联系起来。

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

这里的闭包我们就将item数组中的内容分别于showHelp()绑定。不过这个方法创建了三个闭包,相当消耗内存。

除此之外,闭包还能够模拟私有成员。考虑以下代码:

function privateCounter() {
    let privateVal = 0;
    function add(num) {
        return privateVal + num;
    }
    return {
        plus: () => {
            add(1);
        },
        minus: () => {
            add(-1);
        },
        getVal: () => {
            return privateVal
        }
    }
}

let Counter = privateCounter()

privateCounter()就相当于构造函数。通过它实例化了一个Counter对象。这个对象不能直接获取privateVal值,因为它没有这个属性;但是可以通过getVal函数获得,因为其形成了闭包,哪怕构造函数执行完毕也依然能够访问privateVal。

循环闭包陷阱

尽管与上面代码同样的思想,下面的代码就不能正常运行:

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

会发现三个都显示的是age的信息。为什么呢?这关系到var的作用域问题。众所周知var声明的变量是函数作用域。这就意味着在整个函数运行过程中它声明的变量都是有效的。而在上述代码中item的作用域显然就是setupHelp()函数的作用域

在for循环中我们定义了函数showHelp的闭包。这个闭包包括的就是showHelp()函数和它的执行环境setupHelp()函数的作用域

我们知道给DOM元素绑定事件是异步操作。只有当条件达到时才会调用绑定事件函数。那么当showHelp()执行的时候,需要给document.getElementById('help').innerHTML = help;找到help的值。既然本函数中没有,那就只能去它的上一级—setupHelp()函数的作用域中寻找了。此时for循环早已完成,item的值就是{'id': 'age', 'help': 'Your age (you must be over 16)'}。而且三个showHelp()的闭包都是包含的setupHelp()函数的作用域,它们都指向内存中的同一位置。于是三个showHelp()函数都得到的是age的数据。
在这里插入图片描述

想要解决这个问题,有这几种办法:

  1. 使用let声明item。let 声明的变量具有块级作用域,这样其作用域就被限制在了for循环内部。那么在寻找item的时候找到for循环块这一级作用域就能找到了。找到了之后就不会深入寻找了。

在这里插入图片描述

  1. 如“作用”部分中第一段代码所示,在showHelp()外再套一层函数。这样makeHelpCallback函数为每个回调函数创造了一个新的闭包环境(因为函数立即执行,作用域进链)。这个环境里的item.help分别是三个不同的值。
    在这里插入图片描述

性能问题

闭包中的数据一直都处于内存之中且不会被垃圾回收机制回收。滥用闭包就可能导致内存消耗过多甚至是内存泄露。因此在使用闭包之前一定要充分考虑其他方法,控制闭包数量。

代码来源及参考

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

版权声明:本文为weixin_43790271原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43790271/article/details/100025718