3114 字
16 分钟

函数的本质和闭包函数

欢迎来到我的博客!#

使用 Markdown 格式编写。

今天学到了什么#

  • 函数的本质
  • 闭包函数中的变量调用机制

函数的本质#

image-20260219144158291

1. 函数是一种变量吗?#

是的,在 JavaScript 中,函数(Function)本质上就是一种特殊的变量。

  • 头等公民:在此语言中,函数被称为“头等公民”(First-class citizen)。这意味着函数和数字、字符串一样,可以被:
    • 赋值给一个变量(比如 const fun = count())。
    • 作为参数传给另一个函数。
    • 作为另一个函数的返回值(就像你截图里的 return fn)。

2. 函数属于哪种数据类型?#

函数属于复杂数据类型(引用数据类型)。

JavaScript 的数据类型分为两大类,函数归类如下:

  • 基本数据类型(简单类型):String, Number, Boolean, Undefined, Null, Symbol, BigInt。它们存储的是本身。
  • 引用数据类型(复杂类型):Object(对象)。
    • 函数(Function)在 JavaScript 中实际上是一种特殊的对象
    • 数组(Array)也是一种特殊的对象。

验证方法#

你可以使用 typeof 操作符来查看。虽然 typeof 会返回 'function',但这只是 JS 为了方便区分而给的一个特殊标识,本质上它依然是对象(Object)。

// 验证一下
function demo() {}
console.log(typeof demo); // 输出: "function"
// 验证它是不是对象
console.log(demo instanceof Object); // 输出: true (证明函数也是对象)

总结: 你在截图中看到的 const fun = count(),实际上就是把 count 函数内部返回出来的那个**复杂数据类型(函数对象)**的内存地址,赋值给了变量 fun

闭包函数中的变量调用机制#

image-20260219151545885

image-20260219151826557

image-20260219151936435

image-20260219152106013

一般情况下来说。函数内部的变量是局部变量,当函数执行完毕之后,里面的变量就会被回收掉。

但是浏览器进行回收的时候。使用的是标记清除法。只有当这个对象无法到达的时候,才会被回收。然而对于闭包函数。我们仍然可以获取到函数内部的变量。所以不会被清空。

image-20260219151701711

这是一份结合了 作用域作用域链垃圾回收机制(标记清除法)闭包 的完整底层原理解析,能够解答你关于 “为什么 count 不销毁且会累加” 的所有疑惑。


1. 核心概念回顾#

  1. 作用域 (Scope): 变量能够被访问的“地盘”。

    • fn 内部是局部作用域,count 定义在这里。
    • 正常情况下,fn 执行完毕,这个地盘就关闭了。
  2. 作用域链 (Scope Chain): 变量查找的规则链条。

    • JavaScript 采用词法作用域(静态作用域)。这意味着函数在定义时,它的作用域链就已经确定了。
    • 内部函数 fun 定义在 fn 内部,所以 fun 的作用域链永远包含 fn 的作用域(即 fun 能看到 count)。
  3. 垃圾回收 (GC) - 标记清除法: 浏览器的内存管理机制。

    • 原则: “从根对象(全局对象 Window)出发,凡是能被触达(引用)到的变量,都是‘有用的’,标记为活跃;凡是断了联系、触达不到的变量,就是‘垃圾’,会被清除回收。”

2. 详细执行过程拆解#

我们将代码拆解为三个阶段来看内存发生了什么:

阶段一:定义与初始化 (const result = fn())#

  1. 代码执行到 const result = fn()
  2. 调用 fn:
    • 开启了 fn 的函数作用域。
    • 在内存中为局部变量 count 分配空间,赋值为 1
  3. 定义 fun:
    • fn 内部定义了 function fun() {...}
    • 关键点fun 生成了自己的作用域链。因为它生在 fn 里,所以它手里紧紧攥着 fn 的作用域引用(也就是它记住了 count 的位置)。
  4. 返回与赋值:
    • return fun 将内部函数本身(内存地址)返回到了外面。
    • 外部变量 result 接收了这个地址,指向了内存里的那个 fun 函数实体。

阶段二:垃圾回收的审判 (GC Check)#

fn 执行完毕后,浏览器垃圾回收机制(GC)来巡查了:

  • GC 问 fn 的作用域: “你执行完了吗?”
    • fn: “完了。”
  • GC 问 count: “还有人需要你吗?我能把你清除吗?”
    • 关键时刻! 若是普通函数,count 此时断了联系,会被清除
    • 但在闭包中,GC 发现:
      1. 全局变量 global (Window) 引用了 -> 外部变量 result
      2. result 引用了 -> 内存中的内部函数 fun
      3. fun 的作用域链引用了 -> fn 的作用域(包含 count)。
    • 结论: 从根出发,有一条线连着 count(Window -> result -> fun -> count)。
    • GC 判决: 标记为活跃,不能清除! count 必须保留在内存中。

这就是闭包的本质:一个函数(fun)保留了对它被定义时的词法作用域(fn 作用域)的引用,导致那个作用域无法被 GC 回收

阶段三:执行与累加 (result())#

  1. 第一次调用 result():

    • 执行的是内存里那个 fun 函数。
    • 遇到代码 count++
    • 查找作用域链: fun 自己的作用域没有 count -> 沿着链向上找 -> 找到了那个被 GC 保留下来fn 作用域中的 count
    • 此时内存里的 count 是 1。操作后变成 2。
    • 注意: 此时并没有重新运行 let count = 1,因为那是 fn 函数体内的代码,而可以在外面调用的是 fun 函数体,fun 里只有 count++
  2. 第二次调用 result():

    • 再次执行 fun
    • 再次去那个同一个内存地址count
    • 上次它变成了 2,所以这次就是 2++,变成了 3。

3. 终极总结#

  • 函数是变量: result 只是 fun 在外部的一个别名(引用),它们指向同一个内存实体。
  • 作用域链是地图: fun 出生时就带着一张地图,标记着 count 的位置。不管 fun 跑到哪里执行,它都按这张旧地图找变量。
  • GC 是保洁员: 因为你把钥匙(result)留在了外面,保洁员(GC)发现房子(count)还有人能进去,所以**不敢打扫(清除)**这间屋子。
  • 结果: let count = 1 只在盖房子(fn())时执行了一次。之后每次进屋(result()),都是在同一个已存在的变量上进行修改,所以值会累加。

GC进入函数后,是根据[[Environment]]查找吗?很乱啊,怎么一会是根据地址查找,一会根据[[Environment]]查找#

别乱!这其实是同一个东西

一句话总结:GC 永远只认“地址(指针)”。 [[Environment]] 只是函数肚子里存储“上级环境地址”的那个属性的名字。

就像你手机通讯录里的联系人:

  • 查找方式:拨打电话号码(地址)。
  • [[Environment]]:只是你通讯录里存的**“爸爸”**这个备注名。

所以,“根据 [[Environment]] 查找”的意思就是:“翻开通讯录,找到‘爸爸’那一栏,拨打后面记的那个电话号码(地址)。”

为了彻底理清,我们看看 GC 站在一个函数面前,到底会摸哪两条路:

GC 摸函数的“两条路”#

假设我们有个函数 inner

// 全局环境 (GlobalEnv - 地址 0x999)
function outer() {
// 局部环境 (Env_Outer - 地址 0x888)
let a = 1
function inner() {
// inner 函数对象 (地址 0x666)
// ...
}
}

当 GC 摸到了 inner (0x666) 这个函数对象时,它会同时检查两个方向的“电话号码”:

1. 路一:这是什么对象?(自身的属性)#

  • GC 问 inner:“你肚子里有哪些属性?”
  • inner 说:“我有 name(名字叫 ‘inner’),我有 length(参数个数)…”
  • 这些都是 inner 这个对象自己带的数据,存在它自己的内存块里。

2. 路二:你从哪来?(身世之谜 - 重点!)#

  • GC 问 inner:“你的 [[Environment]] 这一栏填的是什么地址?
  • inner 答:“填的是 0x888(就是 outer 运行时的那个环境 Env_Outer)。”
  • 于是,GC 拿着 0x888 这个地址,瞬移到了 Env_Outer 那里。
  • GC 到了 0x888,问:“这里面有啥?”
  • Env_Outer 答:“这里面有变量 a。”
  • 结果:GC 摸到了 a,所以 a 不回收。

回到你刚才那个“乱”的例子#

function outer() { // (0x001)
let a = 100 // (Env_Temp - 0x555)
return outer // 返回的是 0x001
}
const fun = outer()

这里为什么摸不到 a

  1. fun 指向 -> 0x001 (outer 函数本身)。
  2. GC 问 0x001:“你的 [[Environment]] 填的是谁?”
  3. 0x001 说:“我是在全局定义的,所以我填的是 0x999(GlobalEnv,全局环境)。”
  4. GC 拿着 0x999 走了。
  5. 关键点:中间那个由 outer() 运行时产生的临时房子 (Env_Temp - 0x555),它的地址根本不在 0x001 的通讯录里!
  6. 所以 GC 摸不到 0x555,那个房子(连同里面的 a)就被拆了。

总结#

  • “地址”(GC 能顺着走通)。
  • “无法回收”结果(因为路通了,所以不能拆)。
  • [[Environment]]路标(函数特有的、指向上级环境的路标)。

GC 的逻辑很简单:不管属性名叫什么(是叫 innerObj 也好,叫 [[Environment]] 也好),只要你存了别人的地址,我就能摸顺着地址过去。

不是两条路吗?那通过路一不就能找到自身属性里面的a吗?#

这里有个巨大的误解!

a 并不是 inner 函数的自身属性!

1. 什么是自身属性?#

自身属性是那些直接挂在对象身上的东西。比如:

function inner() {}
inner.abc = 123; // abc 是 inner 的自身属性
// name, length, prototype 也是自身属性

2. a 到底是什么?#

在闭包里:

function outer() {
let a = 100 // a 是定义在【outer 的环境 Env_Outer】里的变量
function inner() {
// 这里的 console.log(a) 只是引用了 a
}
}

a 既不是 inner 的属性,也不是 inner 内部定义的局部变量。 它是一个“如果不出去(顺着 [[Environment]] 找)找不到”的自由变量

3. GC 为什么不能通过路一找到 a#

GC 走“路一”是去查 inner 肚子里装了什么。

  • GC 问 inner:“你有属性叫 a 吗?”
  • inner 答:“没有。我肚子里只有函数体代码字符串(比如 console.log(a)),但我并没有一个属性存着 a 的值。”

GC 必须走路二([[Environment]],去查 inner长辈(定义时的环境),才能在那个长辈的环境对象里找到 a

总结#

你之所以觉得 a 好像是在 inner 里面,是因为代码写在一起看了眼晕。 但在内存里

  • inner 函数对象是一块地(存着代码逻辑)。
  • Env_Outer 环境对象是另一块地(存着 a=100)。
  • 它们之间只有靠 [[Environment]] 这根很细的指针连着。

路一(查自身属性):不仅找不到 a,连影子都看不见。 路二(查环境链):只有走这条路,才能跨越内存空间找到那个 a

[[Environment]]`里存的是什么?#

[[Environment]] 里存的只有一样东西: 它出生时,那个环境对象的内存地址(引用)。

1. 把它想成一张“出生证明”#

  • 每一个函数在你定义它的那一瞬间,JS 引擎就会给它贴上一张“出生证明”([[Environment]] 属性)。
  • 这张证明上只写了一行字:“我的家乡(父级作用域)在地址 0xXXXX”

2. 具体存了什么?(举例)#

例子 1:定义在全局的#

// 全局环境 (GlobalEnv - 地址: 0x999)
function outer() { ... }
// 1. outer 出生了!
// 2. outer.[[Environment]] = 0x999
  • outer 存的是:0x999
  • 含义:如果我在 outer 里找不到变量,就去 0x999 (全局) 找。

例子 2:定义在函数里的(真正的闭包)#

function outer() {
// 局部环境 (Env_Outer - 地址: 0x888)
let a = 1
function inner() { ... }
// 1. inner 出生了!
// 2. inner.[[Environment]] = 0x888 (注意!不是 0x999 了)
}
outer()
  • inner 存的是:0x888
  • 含义:如果我在 inner 里找不到变量,就去 0x888 (Env_Outer) 找。

3. GC 怎么用它#

当 GC 检查到 inner 函数时:

  1. 它读到了 0x888
  2. 它顺着地址飞到了 0x888 那个地方。
  3. 它发现 0x888 是一块还没被销毁的内存,里面还存着 a=1
  4. GC 说:“哦,原来你还要用家里的东西啊,那这块地不能拆。”
  5. 这就是闭包不被回收的原理

总结#

[[Environment]] 就是一个普通的指针变量,里面存了一个内存地址。 它指向函数定义时所在的那个词法环境对象

赞助支持

如果这篇文章对你有帮助,欢迎赞助支持!

赞助
函数的本质和闭包函数
https://firefly.cuteleaf.cn/posts/study-everyday/2026-02-19今日学习-函数的本质/
作者
LJC
发布于
2026-02-19
许可协议
CC BY-NC-SA 4.0
最后更新于 2026-02-19,距今已过 29 天

部分内容可能已过时

目录