likes
comments
collection
share

闭包 - 理解不了闭包不是你的问题,而是闭包的问题。我们无法以干练的语言去解释一个抽象的概念

作者站长头像
站长
· 阅读数 4

前言

下面这句话是 MDN 上对闭包的解释,它足够简洁、也足够准确:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

我相信大家都看过这句话,但是很多人依旧没有理解闭包到底是什么。

很多人理解不了闭包是什么不是人的问题,而是闭包的问题。

请先别说大言不惭,下面将带大家一步步理解为什么说是闭包的问题。

闭包并不是一个定义学的概念,而是归纳学的产物。 闭包并不是一个定义学的概念,而是归纳学的产物。 闭包并不是一个定义学的概念,而是归纳学的产物。

重要的话说三遍,我们总尝试使用一个定义型的句子来总结一个归纳学的产物,自然而言的会产生某种误解。

理解闭包

我不想直接和大家聊闭包是什么,因为这对你理解闭包没有任何好处。

下面我将从:

  1. 什么是一等公民
  2. JS 的一等公民
  3. 函数作为一等公民与闭包的关系
  4. 从语言设计者角度理解闭包
  5. 闭包的错误理解

来带大家理解闭包的到底是什么。

什么是一等公民

一等公民(First-Class Citizen)是指在编程语言中,某种实体(通常是数据类型、对象或函数)拥有与其他实体相同的权利和特权。这意味着这种实体可以像其他实体一样自由地在代码中使用,包括作为参数传递给函数、作为函数的返回值、赋值给变量以及存储在数据结构中。

一等公民的特点:

  • 可以赋值给变量
  • 可以作为方法的参数传递
  • 可以作为方法的返回值
  • 可以存储在数据结构中
  • 可以通过构造函数、对象字面量直接创建

JS 的一等公民

显而易见的是, JS 的函数确实是 一等公民 ,且具有所有一等公民的特点。

  • 函数可以被赋值给变量
  • 函数可以作为其他函数的参数传递
  • 函数可以作为其他函数的返回值
  • 函数可以存储在数组、对象等数据结构中
  • 函数可以通过字面量直接创建

看到这里大家可能会疑惑,以上这些特点 object 也同样满足,为什么 JSobject 不是一等公民?

object 不是一等公民的原因是:与函数相比, object 不具备自由的捕获和操作外部作用域的变量的能力。

函数作为一等公民与闭包的关系

所有的一等公民是函数的语言中,都躲不开闭包这个概念。(这句话虽然不完全准确,但就目前的发展来看,在现阶段是准确的)

就现在来看,除了早期的 C 语言不具备闭包的特性,其他所有的函数作为一等公民的语言都具有 闭包 这个特性。

JS 中的函数作为一等公民意味着函数具有与其他数据类型相同的权利和特权,可以像其他数据类型一样自由地在代码中使用,包括作为参数传递给函数、作为函数的返回值、赋值给变量以及存储在数据结构中。

那么就意味着函数在定义时,可以捕获(应用)外部作用域中的变量,并在函数被调用时仍然可以访问操作这些变量,其实这种能力就是闭包。闭包延长了变量的生命周期,在函数执行的过程中仍然可以访问它们。

什么意思呢?看下面代码:

const name = 'sincenir';

function run() {
    console.log(name)
};

run();

这里 run 函数本体和 name 变量 就形成了一个闭包,进而在函数调用时可以访问该变量。

如果不使用闭包则是:

const name = 'sincenir';

function run(name) {
    console.log(name)
};

run();

显而易见的,如果我们不在 run 调用时传递 name ,上面的 name 变量就会被垃圾回收释放,进而让我们打印出 undeined

综上可以感觉到 闭包JS 的重要性。

语言设计角度

概念我们讲的差不多了,下面我们从语言设计者的角度聊聊函数与闭包。

条件:我们现在需要一个函数作为一等公民的语言。

如果我们设计一个函数作为一等公民的语言,躲不开的一件事:我们的函数中使用的属性都从哪里来。

const age = '14'
function chiesePeople(name) {
    const country = '中国'
    console.log(`我的名字是${name},我的国家是${country},我的年龄是${age}`)
}

chiesePeople('sincenir')

这是一个比较典型的 JS 函数,函数内的参数通过 内部参数、外部参数、形参 三种传入类型传入。

外部参数引用

这里就有一个问题,函数内部是否可引入外部参数?显而易见的是, JS 允许我们在函数内部使用外部参数。

既然使用外部参数,那就一定存在一个问题:变量应该在什么时候被回收? 如果变量不被回收,那么就成了恐怖的野生变量。

JS 的垃圾回收采用了引用计数法,因此在函数内部存储一个变量的引用,确保了引用存在变量不被回收,当引用不在时,则将变量回收。

这也就对应了闭包的一个特点: 函数与函数周边的引用形成了一个闭包(变量生命周期延长)

内部参数

既然内部可以使用外部参数,那么外部是否可以使用内部参数呢?

function people() {
    const name = 'sincenir'
    console.log(`你的名字是${name}`)
} 

name = 'zhangsan'
people()

大家可以感觉一下,我们设计肯定不能这么设计,因为我们的作用域都提升到了最上层,一个不经意的变量修改就会影响大量代码, JS 的设计同理。

既然外部不能使用外部变量,那么第二个特性就显而易见了: 函数内部变量是函数的私有变量

模块化

有了上面的特点,我们就可以得到闭包的最大好处: 模块化

什么是模块化?

我们在开发的过程中,会有很多常用的变量,比如: id 。那么如果我们有很多个 id ,我们的变量定义就会无比复杂。这时候便需要一个能将不同位置的 id 隔离的方案,这个方案就是 模块化

示例

我们有一个计算器,支持加减法和获取答案。

// calc.js
let result = 0;

function add(a) {
    result += a
}

function sub(b) {
    result -= b
}

function getResult() {
    return result
}

这时候我们在另一个文件使用时,由于没有作用域的限制,会出现可以直接修改 result 的情况。

// main.js
result = 100;

add(3);
console.log(getResult()) // 103

你现在使用的 import 是不会出现这种问题的,因为它帮你把模块化的事情做好了。

那么我们就需要通过函数限制变量的作用域,以实现模块化。

// calculator.js
const calculator = (function() {
    let result = 0;

    function add(a) {
        result += a
    }

    function sub(b) {
        result -= b
    }

    function getResult() {
        return result
    }
    
    return {
        add,
        sub,
        getResult
    }
})()

这样我们在外部使用 calculator 时,便可以将 result 封装为了私有变量。

很多人称这是一个闭包,但这是不对的。 这是基于闭包的抽象概念实现的一个集合

最后

我们基于语言设计者考虑会发现,闭包其实并不是先有了概念再进行实现,而是为了一些功能实现了函数中变量引用问题,这些功能的实现被归纳为 闭包

闭包的错误理解

  • 一个花括号就是一个闭包
  • JS 所有的 Function 都是闭包
    • 这是我之前的理解,虽然它是对的,但它对我们理解闭包毫无用处
  • 待大家补充...

使用闭包

讲完闭包这个概念的由来,我下面简单列举几个基于闭包这个抽象的概念实现的功能。

请不要在开发时,想我应该用闭包做什么。你应该做的是记住函数及函数的作用域,基于外部变量、内部变量、形参 的作用域去实现。

柯里化

主函数返回的是子函数,且子函数使用了主函数的形参。

function getPeople(country) {
    return function (name, age) {
        console.log(`我的名字是${name},我的国家是${country},我的年龄是${age}`)
    }
}

const chinesePeople = getPeople('中国')
chinesePeople('sincenir', 18)

典型应用:

  • 基础类型验证
  • 表单验证
  • 权限控制
  • 数据处理/数据转换
  • 路由管理
  • 事件处理

模块化

模块化我在上面的语言设计上讲过了,这里就不再列举了。

在现阶段的开发中,我们已经极少用到模块化封装了,这些事通常由 vue、react、cjs、esm 等帮我们做好了。

其他

我本来列了很多的应用场景,但是我发现我很难将他们归为哪一个真正的业务需求。比如 封装、私有变量 等。这些无非都是用了变量作用域的能力,进行了一个扩展。

其实我们只要记住这些特点即可灵活使用:

  • 参数复用

  • 延长变量生命周期

  • 保持状态

  • 内部函数应用外部作用域

  • 实现私有变量和方法

最后

林林总总讲了这么多,最主要的思想是:我们不应该去理解闭包到底是什么,我们需要理解的是 JS 中函数的运行机制

在现代的面试中,已经很少问这个问题了,核心并不是闭包不存在了,而是前端框架已经帮我们干了大多数需要通过理解闭包实现的功能,很多面试官都忘记了这个问题的答案(或者说不知道怎么讲闭包)。

闭包一万个人有一万个说法,一万本书有一万个描述。别去看这些混乱的描述,这对我们理解闭包本身没有任何好处。

如果你们面试时,面试官硬要问这个问题,就把 MDN 的定义甩他脸上吧。至少它是对的。

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。