阅读视图

发现新文章,点击刷新页面。

JS炼化:闭包——未识其名已善用, 深知反被概念拘

扯下“闭包”大衣,发现函数你好装

ZZZ复盘学习JS基础,重新认识知识,总结掌握知识,炼化知识,输出知识。这是一篇学习反思,用自己的逻辑把知识体系搭建,用自己的话总结输出。

首先看闭包定义 :闭包是指一个函数以及其捆绑的周边环境状态(词法环境)的引用的组合。

(定义中既有捆绑也有引用,感觉很复杂拗口。但是,如果我们从函数特性出发,再结合 JavaScript 发展过程中有意思的历史背景与设计演变,看清闭包的意义是如何一步步变化的,从而真正理解:我们今天学习闭包,到底在学什么、它又为何重要。)

一句话戳破闭包:没有那么玄乎,就是函数的特性延展

一、从函数来看闭包

1.1 了解函数概念及特点

在JS里,函数是一段可以被重复执行的代码块,也是实现闭包最基础的单元。想要看懂闭包,必须先搞懂函数这几个核心特点:

<>1. 函数可以独立定义,也可以嵌套定义 我们不仅可以在全局写函数,还可以在一个函数内部再定义另一个函数,形成内外层函数结构。

<>2. 函数有自己的作用域 函数内部声明的变量,外部无法直接访问,形成了一层独立的“变量空间”,保证了数据的独立性。

<>3. 内部函数可以访问外部函数的变量 内层函数能够“向外”读取外层函数的变量和参数,而外层函数不能访问内层函数的变量,这是一条单向可见的规则。

<>4. 函数可以作为值被返回和传递 函数可以被  return  出去、赋值给变量、当作参数传递,让它可以在定义它的作用域之外执行。

有了上面这四个函数特性,我们就初步理解了函数的基本能力。接下来我们试着实现一个简单功能,不用刻意去写闭包,却会在无形中把闭包搭建出来。

1.2 应用函数特长形成闭包

给你一个任务,写一段代码实现最简单的计数功能

// 定义一个用来生成计数功能的函数
function makeCounter() {
  // 在这里定义一个变量,用来记录次数
  let count = 0

  // 返回一个新的函数
  return function () {
    // 每次执行这个函数,count 就加 1
    count++
    // 把最新的 count 返回出去
    return count
  }
}

// 调用 makeCounter,得到里面返回的那个函数
const counter = makeCounter()

// 每调用一次 counter,就会操作之前的 count
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3

通过上面的代码,我们实现了计数器功能。

回头看闭包的定义: 闭包是函数与其周边词法环境(状态)的引用所捆绑形成的组合。

对照这段代码就会发现:

  • 我们有一个内部函数
  • 这个函数引用并绑定了外部函数的  count  变量(也就是它的词法环境/状态)
  • 即使外部  makeCounter  执行完毕,内部函数依然保留着对  count  的引用

所以这段代码虽然我们没有刻意强调闭包,但它已经完全符合闭包的定义,天然形成了标准的闭包结构。通过这个例子,只是看到了我写了一个稍微复杂一点的逻辑,就有了一个闭包的形成,但是对于它的产生和定义还是有疑惑?继续看完后面闭包的优缺点以及历史演变,我想这些问题就会迎刃而解了。

二、为什么要学闭包:三大优点

2.1 优点一

持久保存状态,变量不会丢失

:内部函数会一直记住外部函数的变量,函数执行完后变量依然保留,不会被重置。

还看刚刚写的计数器

function makeCounter() {
  let count = 0
  return function () {
    count++
    return count
  }
}

const counter = makeCounter()
console.log(counter()) // 1
console.log(counter()) // 2

这个功能用到了函数的这几个特性:

  • 用到了 函数可以嵌套,在外面函数里又写了一个函数。
  • 用到了 内部函数可以访问外部函数变量,所以内部函数能拿到并修改  count 。
  • 用到了 函数可以被返回,把内部函数传出去,在外面还能调用。

正是这几个特性一起作用, count  才会被一直记住,实现了状态持久保存。

2.2 优点二

实现数据私有,外部无法随意修改

:外部函数里的变量,外部作用域无法直接访问和修改,只能通过闭包函数操作,保证了数据安全。

看一个简单私有数据

function createPerson() {
  let name = "小明" // 外部无法直接改

  return {
    getName: function () {
      return name
    }
  }
}

const person = createPerson()
console.log(person.getName()) // 小明
// 无法直接修改 person.name,保证了数据私有

这个功能同样是依靠函数特性:

  • 函数有 自己独立的作用域,外部本来就不能直接访问里面的  name 。
  • 再加上 函数嵌套 和 内部函数能访问外部变量,外部只能通过返回的方法去读  name 。
  • 最后把方法 返回出去,外部才能使用。

最终效果就是: 外面可以调用,但不能直接改,只能通过内部函数操作,实现了数据私有。

2.3 优点三

这里我先卖个关子,第三个优点我先不直接写出来。相信看完后面的例子和历史演变,自然就明白了,同时也是希望大家能看下去,虽然是个人的学习分享,但是想让大家一起思考一下技术学习的意义。

三、闭包的历史与作用域演变

3.1 闭包是不是JS 特有?还是早就被定义

虽然可能大家都知道,但是对于我来说确实一开始以为闭包是 JavaScript 才有的东西,了解后才发现其实不然。 闭包早在 1964 年就被计算机科学家 Peter Landin(彼得·兰丁) 正式定义,最初源于 λ 演算 和函数式编程,是一个非常古老的通用概念。

最早的官方定义(原文直译):

闭包是由 环境部分 + 控制部分 组成的整体,用于记录表达式的执行上下文。

简单大白话翻译(你文章直接用):

闭包就是 一个函数 + 它所绑定的外部变量环境 的组合,把“开放”的函数变成“闭合”的完整整体。

它从一开始就不是 JS 特有,Python、Java、Kotlin、Rust 等语言全都支持。 我们今天在 JS 里学闭包,本质是在学一门通用的编程基础能力,而不只是某一门语言的小技巧。

3.2 var 的问题与闭包“被迫”使用形成的第三个优点

前面我卖了个关子,说闭包还有第三个关键作用。 其实它和早期 JavaScript 里  var  的设计问题紧密相关——正是因为 var 没有块级作用域,才让我们不得不靠闭包来解决问题,这也成了闭包一个很重要的历史价值。

首先看例子1:循环绑定事件(var 的经典坑)

// 用 var 会出问题
for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i) // 输出:3 3 3
  }, 1000)
}

因为  var  只有函数作用域,没有块级作用域, 循环里的  i  是同一个变量,等到定时器执行时, i  已经变成 3 了。

“被迫”用闭包来解决

当年没有  let ,大家只能被迫用闭包来锁住每一次的  i :

  
for (var i = 0; i < 3; i++) {
  // 用立即执行函数制造一个函数作用域,形成闭包
  (function (currentI) {
    setTimeout(function () {
      console.log(currentI) // 输出:0 1 2
    }, 1000)
  })(i)
}

这里就是闭包的第三个优点:

  • 在没有块级作用域的年代,闭包帮我们模拟出独立的作用域
  • 让每个循环都能保存自己的变量,互不干扰

被迫这个词用的不是很恰当,只能说闭包很厉害能够成功解决这个痛点。但也正是因为这些痛点,后来 ES6 才推出了  let  /  const ,让我们不用再被迫写复杂的闭包。但也正因为这段历史,我们才更明白:闭包的意义,不只是技巧,更是语言发展过程中解决问题的关键方案。

3.3 let / const 带来的技术解放

后来 ES6 带来了  let  和  const ,可以说给前端开发者带来了一次技术解放。

它们拥有真正的块级作用域,在  for  循环、 if 、 {}  里都会生成独立的变量, 不再像  var  那样全局共用一个变量。

我们再看刚才的循环问题:直接用  let ,代码变简单了,再也不需要靠闭包来强行造作用域。

for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i) // 输出:0 1 2
  }, 1000)
}

这就意味着:

  • 以前闭包第三条优点是被迫使用,用来填  var  的坑
  • 现在  let  /  const  把坑填上了,闭包终于回归它本来的样子: 用来做状态保存、数据私有这些真正有价值的场景

所以我们回过头再看闭包,它的意义也变了: 不再是“不得不写的 workaround”, 而是主动选择、用来写出更优雅、更安全代码的强大特性。

3.4补充闭包使用带来的缺点

闭包虽然好用,但也有一个问题:它会让变量一直留在内存里,不容易被释放,用多了可能让程序变卡。

原因也很简单: 内部函数一直引用着外部函数的变量,所以就算外部函数执行完了,这些变量也不会被垃圾回收,会一直占着内存。如果不注意,就可能造成内存占用过高,甚至内存泄漏。

比如

function outer() {
  let data = new Array(10000).fill('我占内存')

  return function inner() {
    console.log(data.length)
  }
}

const fn = outer()
// 只要 fn 还在,data 就一直占内存

怎么解决?

不用闭包的时候,把引用设为 null,浏览器就会回收内存:

fn = null

四、总结:闭包是什么?我理解的闭包

4.1再看闭包是什么?

依我来看,闭包其实并不是 JavaScript 里刻意创造的复杂语法,而是我们用函数嵌套、实现功能时自然而然就会产生的一种形式。本质上,就是通过函数嵌套,让内部函数可以访问并使用外部函数的变量和参数,从而延长了这些变量的生命周期,同时让不同调用之间的数据相互独立、互不干扰,也实现了变量的私有化。

只要我们用到了「函数能访问外部作用域变量」这个特性,并且用它来实现对应的功能,闭包就已经在默默工作了。理解到这一层就会发现,闭包并没有那么神秘难懂,它只是 JavaScript 函数特性的自然延伸,是写好代码、做复杂逻辑时绕不开、也必然会用到的东西。

以上就是我对闭包的学习心路历程,逻辑梳理和炼化后的输出

如有理解不当,欢迎大家指正,一起学习进步

❌