一文彻底搞懂:js-闭包

1,083 阅读3分钟

什么是闭包

官方解释:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

我们通俗的理解一下就是:

函数引用了它的外面的变量,就形成了闭包。

下面代码:

function block() {
  let name = 'RiverLi'
  function displayName() {
    document.getElementById('root').innerText = name
  }
  displayName()
}
block()

执行之后界面上能正确显示结果。上面的代码并不难理解,因为它和下面的代码很相似,区别只是displayName所在的上下文环境不一样,上面的代码中它在 block 函数的作用域内, 下面的代码它在全局作用域内。

let name = 'RiverLi'
function displayName() {
  document.getElementById('root').innerText = name
}
displayName()

但下面的代码就令人迷惑了,block 方法中没有执行displayName 而是将displayName作为函数的返回值return了。在调用Block之后,接收到返回值之后执行fun。

function block() {
  let name = 'RiverLi'
  function displayName() {
    document.getElementById('root').innerText = name
  }
  return displayName
}
let fun = block()
fun()

那,它能执行成功吗?按道理来讲,block执行完毕之后 name 就销毁了,因为name的作用域在block方法内,但 答案是肯定的,界面依然正确显示。这里就涉及到了闭包的概念:闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。 displayName创建的作用域是block函数内部,所以displayName执行的时候能访问到block函数作用域内的任何变量。

下面代码中,dispayName 虽然在同一作用域内声明,但fun1和fun2的上下文环境是不一样的,因为是两个block实例。

function block(name) {
  function dispayName() {
    console.log(name)
  }
  return dispayName
}
let fun1 = block('1111')
fun1()
let fun2 = block('2222')
fun2()

闭包的用途

模拟私有方法

下面代码中,Counter 内部的 changeValue 方法在外部是无法访问到的,只能通过 increment、decrement 方法进行数据的操作,通过value方法获取值,这就使得 changeValue 是私用的方法。

function Counter() {
  let _value = 0;
  function changeValue(num) {
    _value += num
  }
  return {
    increment: function() {
      changeValue(1)
    },
    decrement: function() {
      changeValue(-1)
    },
    value: function() {
      return _value
    }
  }
}

let counter1 = Counter()
counter1.increment()
counter1.increment()
console.log(counter1.value())
counter1.decrement()
console.log(counter1.value())

避免闭包陷阱

下面代码中,我们希望实现点击输入框的时候提示对应的提示文案,但效果是,每次的提示文案都是Your age (you must be over 16 。这是因为闭包的行为发生在for循环内部,也就是说闭包能访问到for循环内部的所有变量,值得我们注意的是 item 是使用var声明的,我们知道这会导致变量提升,每次for循环都更新了item的值,最后访问的是item最后一次被赋值的值。解法方法有:

  1. 使用let 修饰item变量,避免提升。
  2. 使用更多的闭包。
  3. 使用匿名闭包。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>闭包</title>
  </head>
  <body>
    <p id="help">Helpful notes will appear here</p>
    <p>E-mail: <input type="text" id="email" name="email" /></p>
    <p>Name: <input type="text" id="name" name="name" /></p>
    <p>Age: <input type="text" id="age" name="age" /></p>
    <script>
      function showHelp(help) {
        document.getElementById("root").innerHTML = help;
      }
      
      function makeHelper(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 = function () {
            showHelp(item.help);
          };
        }
        /*
        //使用更多的闭包
        for (var i = 0; i < helpText.length; i++) {
          var item = helpText[i];
          document.getElementById(item.id).onfocus = makeHelper(item.help)
        }
        */

        /*
        //是用let 解决闭包
        for (var i = 0; i < helpText.length; i++) {
          let item = helpText[i];
          document.getElementById(item.id).onfocus = function () {
            showHelp(item.help);
          };
        }
        */

        /*
        //使用匿名函数解决闭包
        for (var i = 0; i < helpText.length; i++) {
          (function () {
            var item = helpText[i];
            document.getElementById(item.id).onfocus = function () {
              showHelp(item.help);
            };
          })();
        }
        */
      }
      setupHelp()
    </script>
  </body>
</html>

闭包的性能问题

如果没有特定需求,建议避免使用闭包,因为在它使得上下文作用域不释放或者延迟是否,会增加内存的销毁,降低执行效率。

更多优质内容,请扫码关注公众号:RiverLi。在公众号回复关键字「大前端进阶」,可领取独家资料。