「前端每日一问(28)」说说你对闭包的理解

649 阅读8分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

本题难度:⭐ ⭐ ⭐

回答这个问题之前,先抛出两个问题:

  • 闭包是怎么产生的?
  • 闭包是为了干什么?

闭包是怎么产生的?

要明白闭包是怎么产生的,先要理解两个概念:

比如下面这个例子,add 函数内部返回了一个匿名函数,根据作用域链的规则,匿名函数内部可以访问 add 函数定义的变量 i。

function add () {
  let i = 0
  return function () {
    return ++i
  }
}

const fn = add()

console.log(fn()) // 1
console.log(fn()) // 2

上例的这种调用方式就产生了闭包,那么产生这个闭包究竟是为了干嘛?

闭包是为了干什么?

闭包是为了创建函数的私有变量,且这个变量不会随着函数执行完毕被垃圾回收机制回收,变量的生命周期得到延长

接上例,变量 i 就可以作为 add 函数的私有变量,add 函数执行完了,被垃圾回收机制回收了,但是变量 i 还是可以被继续访问和修改。

如果我们试图从全局作用域直接访问 fn.i ,会得到 undefined,因为 i 是定义在函数 fn 作用域内的变量,它并不是 fn的属性。同样的,如果我们试图访问 i 也会报错,因为 i 并没有在全局作用域中定义。

但是我们可以通过特殊的方式,在 add 函数里返回一个函数,再在外部调用这个返回的函数,来操作这个变量 i。

如何查看闭包中的私有变量?

在内部函数的位置打个断点,就可以在浏览器的 Sources 面板中查看闭包中的私有变量。

为了多看两个私有变量,我随便又定义了一个 name 变量和 age 变量。

image.png

好,我们总结一下,闭包是为了创建一些私有变量,并延长这些私有变量的周期,全局无法访问这些私有变量,可以通过特殊的方式,比如函数嵌套函数来访问和操作这些私有变量

从另一个角度理解闭包

闭包可以被理解为闭塞的,封闭的,或者说把东西包起来(藏起来)。

比如,我有 100 元私房钱,我要把这私房钱藏起来,不让我老婆发现。

我把这 100 元藏到了花盆里,代码差不多是这样子:

function flower () {
  const money = 100 // money 这个变量完全隐藏在 flower 内部
  return function () {
    return money
  }
}

const fn = flower()
console.log(fn()) // 输出100 我知道找到私房钱的方式

// 我老婆找不到
console.log(money) // 报错 Uncaught ReferenceError: money is not defined 

这样 money 这个变量完全隐藏在 flower 函数内部,暴露出来的只有花盆,

而我老婆在外部就只能看到一盆花,她无论如何也读取不到内部的 money 变量。

这就是闭包的应用,像藏私房钱一样地私藏变量,把一堆变量私藏起来,不让外面访问,但是可以通过返回函数的形式来访问(老婆发现不了,但我知道怎么取)。

而私房钱一直藏着,不会被老婆收去,属于我自己,是不是延长了私房钱的使用时间(延长了变量的生命周期)。

这个类比足够生动形象吧,是否加深了你对闭包的理解呢?

加深对函数的本质是对象这句话的理解

上文提到,我们通过返回一个函数来实现闭包,其实要实现闭包,还有其他方式,比如:

定义一个参数,把内部函数赋值给这个参数,然后在外部执行。

let fn
function flower () {
  const money = 100
  fn = function () {
    return money
  }
}

flower()
console.log(fn()) // 100

用一个数组来存储内部函数,把内部函数 push 到数组里,在外部访问数组并执行。

function flower (arr) {
  const money = 100
  function fn () {
    return money
  }
  arr.push(fn)
}

const arr = []
flower(arr)
console.log(arr[0]()) // 100

或者把内部的函数存储在一个对象里,返回这个对象,在外部访问这个对象的方法。

function flower () {
  let money = 100
  function get () {
    return money
  }
  function add (num) {
    money += num
  }
  return {
    get,
    add
  }
}

const obj = flower()
console.log(obj.get()) // 100
obj.add(100) // 存私房钱进花盆
console.log(obj.get()) // 200

这样就可以给我们的私房钱加一个 add 方法,定期把私房钱存进花盆里。

其实,能用这些方式实现闭包的根本原因,就是函数的本质是对象,对象拥有的功能,函数全部都有。

  • 函数可以赋值给变量
  • 函数可以被添加进数组
  • 函数可以被添加成对象的属性
  • 函数可以作为函数的返回值。

更多请参考这篇文章: 「前端每日一问(19)」JS 中函数为什么被称为一等公民?

对闭包不同的定义

我们在上文中说了闭包的现象和应用,但是还没给出一个具体的定义呢。

这确实是很令人脑壳痛的事,事实上,对闭包的定义,我见过很多不同版本,比如:

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

  • 红宝书:闭包是指有权访问另一个函数作用域中的变量的函数。

  • JS 忍者秘籍:闭包允许函数访问并操作函数外部的变量。

  • 你不知道的 JS:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

  • 社区:闭包是指内部函数引用外部函数的变量的集合。

  • 社区:闭包其实只是一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境。

  • 社区: 闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。

  • ...

有人说闭包是一个现象,有人说闭包是一个函数,也有人说闭包是一个变量的集合,到底哪个才是对的嘛?!

image.png

其实他们都是对的,这些定义大同小异,其实都指向的是函数执行过程相关的知识,与执行上下文作用域等知识可以串联起来理解。

说实话,我觉得很多定义都很空,看了都很难理解,还是要结合前文的具体实例来理解好一点。

我个人最喜欢的定义是闭包是指内部函数引用外部函数的变量的集合,毕竟它是看得见摸得着的,可以在浏览器里面表现出来。

image.png

其实我们不必太纠结于概念,因为如何定义闭包不会影响到实际的使用,能说出闭包是如何产生的,以及闭包的应用,就已经把闭包这个概念说清楚了。

至于闭包的应用,可以看我写的下面这几篇文章

结尾

闭包是学习 JS 这门语言无论如何都绕不去的坎儿,因为闭包的应用充斥在我们 JS 的世界里,如果看了一遍本文没看懂,就多看两遍,那些学习前端的经典书籍,比如《红宝书》、《JS忍者秘籍》、《你不知道的JS》,阿林也建议大家都去看一看。

对初学者而言,闭包确实很难,但看多了,闭包的应用见多了,自然也就懂了。

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

你也可以关注《前端每日一问》这个专栏,防止失联哦~

我是阿林,输出洞见技术,再会!

上一篇:

「前端每日一问(27)」JS 中执行上下文是什么?

下一篇:

「前端每日一问(29)」JS 中执行栈是什么?