【一文读懂】对闭包的理解及其衍生问题(作用域、垃圾回收、闭包的应用、释放闭包)

👉 个人博客主页 👈
📝 一个努力学习的程序猿


专栏:
HTML和CSS
JavaScript
jQuery
Vue
Vue3
React
TypeScript
uni-app
Linux
个人经历+面经+学习路线【内含免费下载初级前端面试题】
前端学习+方案分享(VitePress、html2canvas+jspdf、vuedraggable、videojs)
前端踩坑日记(ElementUI)
更多前端面试分享


前言

闭包可以说是前端必须要了解的内容,但面试时往往会问更多相关问题,比如我就碰到过这样的“组合技”:

什么是闭包?什么是作用域链?写个闭包的例子吧!为什么形成了闭包?(面试官写了个例子)这样写是不是闭包?闭包涉及的变量,存储的时候是在栈内存吗?闭包有什么应用?防抖和节流的含义?写个防抖/节流函数吧!什么是垃圾回收?闭包为什么不会被回收?JS如何实现自动垃圾回收?如何手动进行垃圾回收?闭包会不会出现内存泄露?怎么避免内存泄露?怎么释放闭包?

本文就将全面整理闭包及其衍生问题。


1、作用域

1.1 作用域和作用域链的概念

要想开始回答所有问题,首先要搞清楚作用域。

JavaScript 中的作用域,指变量和函数的可访问范围,它决定了代码在不同区域中如何和何时可以访问变量和函数。其共有三种作用域:

  • 全局作用域:在代码中任何地方都能访问到的变量或函数。
  • 局部(函数)作用域只在定义它们的函数中访问到的变量或函数。
  • 块级作用域:(该作用域在 ES6 引入 let 和 const 后才出现)只在定义它们的代码块中访问(如:if、for、while)到的变量或函数。

演示如下:

// 全局变量,全局作用域
var globalVar = '全局变量'

function testFunctionScope() {
  // 局部变量,函数/局部作用域
  var functionScopeVar = '函数/局部作用域'

  if (true) {
    var blockScopeVar = 'var在块级作用域内,也在函数/局部作用域内'
    // 块级作用域变量
    let blockScopeLet = 'let在块级作用域内'
    console.log(blockScopeLet) // 可访问
    console.log(blockScopeVar) // 可访问
    console.log(functionScopeVar) // 可访问
  }

  console.log(functionScopeVar) // 可访问
  console.log(blockScopeVar) // 可访问
  // console.log(blockScopeLet) // 错误,因为 blockScopeLet 是块级作用域内的变量
}

testFunctionScope()
console.log(globalVar) // 可访问
// console.log(functionScopeVar) // 错误,functionScopeVar 不在作用域内
// console.log(blockScopeVar) // 错误,blockScopeVar 不在作用域内
// console.log(blockScopeLet) // 错误,blockScopeLet 不在作用域内

在上述例子中,为什么有些能输出,有些会报错的根因就是:

当代码需要访问变量时,JavaScript 解释器会首先在当前作用域中查找是否有该变量的定义。如果没有找到,解释器会继续在上一层作用域中查找,这个查找过程会一直持续到全局作用域。如果在全局作用域中仍然没有找到该变量,则会抛出引用错误(ReferenceError)。这种解析变量的机制(多个作用域层次顺序连接而成的链),就是作用域链

根据声明变量所处的不同位置,也就形成了全局变量、局部变量、块级作用域变量

同时,通过示例也可以简单看出 let(const)、var 的一个区别:

  • var 声明的变量仅能为全局变量或局部变量。在函数外(全局)声明的变量为全局变量,在函数内声明的变量为局部变量。
  • let(const) 声明的变量可以为全局变量、局部变量、块级作用域变量,依赖于它们所处的位置。

由于 var 声明的变量会无视块级作用域,所以现在通常都是用 let 和 const 声明变量


以下为更多延展子问题。

1.2 let、var、const 的区别

其中一个区别,在上文的示例中已给出。其他示例如下:

console.log(varVariable) // 输出:undefined
// console.log(letVariable) // 抛出 ReferenceError
// console.log(constVariable) // 抛出 ReferenceError

var varVariable = 'var 变量'
varVariable = 'var new' // 可以重新赋值
var varVariable = 'var repeat' // 可以重复声明
let letVariable = 'let 变量'
letVariable = 'let new' // 可以重新赋值
// let letVariable = 'let repeat' // 错误:不能重复声明
const constVariable = 'const 变量'
// constVariable = 'const new' // 错误:不能重新赋值
// const constVariable = 'const repeat' // 错误:不能重复声明

console.log(varVariable) // 输出:var repeat
console.log(letVariable) // 输出:let new
console.log(constVariable) // 输出:const 变量

// 函数声明
console.log(declaredFunction()) // 输出:我是声明的函数
function declaredFunction() {
  return '我是声明的函数'
}

// 函数表达式
console.log(expressionFunction) // 输出:undefined
// console.log(expressionFunction()) // TypeError: expressionFunction is not a function
var expressionFunction = function() {
  return '我是函数表达式'
}

// 使用 let
// console.log(letFunc) // ReferenceError: Cannot access 'letFunc' before initialization
let letFunc = function() {
  return '我是使用 let 声明的函数表达式'
}

// 使用 const
// console.log(constFunc) // ReferenceError: Cannot access 'constFunc' before initialization
const constFunc = function() {
  return '我是使用 const 声明的函数表达式'
}

总结示例中所有内容:

  • var 有变量提升,但如果在声明前调用则为 undefined;可以重复声明、可以重新赋值、可以没有初始值(初始值:undefined)。
  • let 没有变量提升,必须先声明后使用;不可以重复声明、可以重新赋值、可以没有初始值(初始值:undefined)。
  • const 没有变量提升,必须先声明后使用;不可以重复声明、不可以重新赋值、必须有初始值。
  • 函数声明会被提升,可以在声明前调用。
  • 函数表达式的行为取决于使用的是 var、let 还是 const。

1.3 基本数据类型和引用数据类型的区别

基本数据类型包括:String、Number、Boolean、Null、Undefined、Symbol(ES6 新增)、BigInt(ES2020 新增)。

  • 存储位置:基本数据类型的值直接存储在栈(Stack)内存中。
  • 访问方式:访问基本数据类型的值是直接访问其存储在栈内存中的值。
  • 复制行为:复制基本数据类型的变量时,会在栈内存中创建一个新值,复制的是值本身,因此两个变量互不影响

引用类型包括:Object(包括所有的对象,如 Array、Function、Date、RegExp 等)。

  • 存储位置:引用类型的值存储在堆(Heap)内存中,而变量实际上存储的是指向堆内存中该对象的地址(即引用)。
  • 访问方式:访问引用类型的变量时,首先从栈内存中获取该对象的地址引用,然后通过引用找到堆内存中的对象
  • 复制行为:复制引用类型的变量时,复制的是对象的地址引用,而不是对象本身,因此两个变量会指向堆内存中的同一个对象。对其中一个变量所引用的对象的修改会影响到另一个变量。(这也是为什么需要深克隆的原因,详见第 1.4 节)

1.4 深浅拷贝

浅拷贝只复制对象的第一层属性,对于嵌套的对象或数组,浅拷贝只复制引用地址,而不是实际的值。常见的浅拷贝方法包括:

  • Object.assign(target, source)
  • 展开运算符 {…obj}
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
shallowCopy.b.c = 3; // 修改嵌套对象
console.log(obj.b.c); // 输出 3,原对象也被修改

深拷贝会递归地复制对象的所有层级,确保嵌套对象或数组的值完全独立。常见的深拷贝方法包括:

  • 使用 JSON.parse(JSON.stringify(obj))(简单但有局限性,如无法处理函数、循环引用等)
  • 使用第三方库(如 lodash 的 cloneDeep)
  • 手动递归实现深拷贝
const obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.b.c = 3; // 修改嵌套对象
console.log(obj.b.c); // 输出 2,原对象未被修改

一个手写深拷贝:

function deepClone(obj, map = new WeakMap()) {
  // 如果是原始类型,直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理循环引用
  if (map.has(obj)) {
    return map.get(obj);
  }

  // 处理特殊对象
  if (obj instanceof Date) {
    return new Date(obj);
  }
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }
  if (obj instanceof Map) {
    const mapCopy = new Map();
    map.set(obj, mapCopy); // 存储引用关系
    obj.forEach((value, key) => {
      mapCopy.set(deepClone(key, map), deepClone(value, map));
    });
    return mapCopy;
  }
  if (obj instanceof Set) {
    const setCopy = new Set();
    map.set(obj, setCopy); // 存储引用关系
    obj.forEach((value) => {
      setCopy.add(deepClone(value, map));
    });
    return setCopy;
  }

  // 创建拷贝对象(区分数组和普通对象)
  const clone = Array.isArray(obj) ? [] : {};

  // 存储引用关系,防止循环引用
  map.set(obj, clone);

  // 拷贝 Symbol 属性
  const symbolKeys = Object.getOwnPropertySymbols(obj);
  symbolKeys.forEach((symKey) => {
    clone[symKey] = deepClone(obj[symKey], map);
  });

  // 拷贝普通属性
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map);
    }
  }

  return clone;
}

为什么需要深浅拷贝

  1. 数据隔离
    • 浅拷贝会导致多个对象共享同一个引用,修改其中一个会影响其他对象。
    • 深拷贝可以确保数据的独立性,避免意外修改。
  2. 状态管理
    • 在前端框架(如 Vue、React)中,状态管理需要确保数据的独立性,避免因引用共享导致的状态污染。
  3. 避免副作用
    • 浅拷贝可能引发不可预期的副作用,尤其是在处理复杂嵌套数据时。

2、垃圾回收

2.1 垃圾回收机制

垃圾回收是一种自动内存管理的机制,它的目的是帮助程序自动回收不再使用的内存资源,减少内存泄漏的风险,从而避免程序手动管理内存的复杂性和出错的可能性。

那 JS 是怎么判断哪些是不再使用的内存资源呢?

通常是通过跟踪对象的引用来实现的。如果一个对象没有任何引用指向它(即不可达),那么这个对象就被认为是“不再需要的”,垃圾回收器就可以回收这块内存

再具体点说,JavaScript的自动垃圾回收机制主要基于“标记-清除”(Mark-and-Sweep)算法来实现的。这个过程分为两个阶段:标记阶段和清除阶段。

  • 标记阶段:在标记阶段,垃圾回收器会从根(通常是全局对象)开始遍历所有可达的对象。所谓“可达”的对象,是指从根出发,通过引用(比如变量、属性等)可以访问到的对象(有引用的对象)。在遍历过程中,每遇到一个可达对象,就将其 标记为“活动”,即不是垃圾。

  • 清除阶段:在清除阶段,垃圾回收器会检查所有对象,把那些在标记阶段没有被标记为“活动”的对象视为垃圾进行回收。这样,那些不再被应用程序所需要的内存就被释放了,从而避免了内存泄漏。

实际上,现代的 JavaScript 引擎(如V8)采用了更加复杂和高效的算法来优化垃圾回收过程,比如:

  • 分代回收:将对象分为“新生代”(新创建的对象)和“老生代”(经过多次垃圾回收仍然存活的对象),因为大部分对象都是短命的,所以通过只针对“新生代”频繁进行垃圾回收,可以提高回收效率(老生代降低回收频率)。
  • 增量回收:将垃圾回收分成多个小步骤进行,这样可以减少每次垃圾回收对程序执行的影响。
  • 并发回收:在另一个线程中执行垃圾回收,以减少对主线程的影响。

当然,上述内容表述的很简单,再细致的底层内容请各位自行搜索查阅。


需要注意:

垃圾回收通常不是立即执行的,它是由 JavaScript 引擎自动管理的一个过程,它的执行时机是由引擎内部的算法决定的,这个算法会考虑到多种因素,比如内存使用情况、CPU 时间等,以尽量减少对程序执行的影响(或者你手动触发 => 但这主要用于测试和调试,不推荐在生产环境中使用)。

垃圾回收的不确定性意味着,即使某个对象已经没有任何引用,我们也无法知道它究竟何时会被回收。这也是为什么在开发中,我们更应该关注避免内存泄漏和优化内存使用,而不是依赖垃圾回收的行为。

而我原本并不觉得垃圾回收很重要,不就是 JS 自动帮我们处理嘛!但当我面试提到垃圾回收后,直接就被以下问题震住了… 接下来延展子问题。


2.2 查看垃圾回收的过程

在进行垃圾回收时,我们可以通过很多方式来查看到这一过程。

(1)你可以选择在 Chrome 开发者工具的内存分析器中查看。具体位置如下(Performance – 录制按钮,录制结束后,开启 Memory,可看到内存使用情况):

在这里插入图片描述
此时你会看到,在过了一段时间后,内存使用率才会下降(垃圾回收不是立即执行)。代码中你可以随意写些什么,在点击按钮前,开启录制即可。比如:

<template>
  <div class="test-page">
    <Button @click="testFunc">
      测试
    </Button>
    {{ arr }}
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api'

export default defineComponent({
  name: 'TestPage',
  setup() {
    const arr = ref([]) as any
    function testFunc() {
      for (let i = 0; i < 1000; i++) {
        arr.value.push(i)
      }
    }

    return {
      arr,
      testFunc
    }
  },
})
</script>

(2)你还可以选择用 node 来查看这一现象,手动触发垃圾回收,并查看内存使用情况。

示例:test.js

function testFunc() {
  let testLet = '测试内容'
  console.log(testLet)
}

console.log('Before:', process.memoryUsage().heapUsed)
testFunc()
global.gc()
console.log('After:', process.memoryUsage().heapUsed)
  • 调用该 js 文件:
    node --expose-gc test.js
  • 输出结果:
    Before: 2609400
    测试内容
    After: 2088032

手动触发垃圾回收方式:gc()
需要注意,它不是 JavaScript 标准的一部分,而是在某些特定环境中提供的扩展功能(node.js)。换句话说,在 Vue 中无法直接使用该函数。同时它主要用于开发和调试,不应在生产代码中使用。

process.memoryUsage()
它是 Node.js 中的一个方法,用于返回一个对象,该对象包含了 Node.js 进程的内存使用情况的信息。这个方法提供了一种简单的方式来监控和分析 Node.js 应用程序的内存使用情况,对于诊断内存泄漏或进行性能优化非常有用。返回的对象包含以下几个属性:

  • rss(Resident Set Size):进程的常驻内存部分。这是分配给进程的内存中已经被占用的部分,包括所有的 C++ 和 JavaScript 对象和代码。
  • heapTotal:堆总计。为动态分配的 JavaScript 对象和相关信息预留的总内存量。
  • heapUsed:堆已用。当前已经被使用的堆内存量,表示当前活跃的 JavaScript 对象(包括所有的可达对象)所占用的内存。
  • external:V8 引擎内部的 C++ 对象占用的内存。

只不过该例子下,虽然 After 的值比 Before 小,证明确实有内存被回收,不过并不只是 testLet。global.gc() 会触发一次完整的垃圾回收,可能回收了其他不再使用的内存,即不能完全精确地反映单个变量的回收情况


(3)如果想看单个变量的回收情况,可以使用 FinalizationRegistry。

在这里插入图片描述
FinalizationRegistry 是 JavaScript 中的一个高级功能,它允许你在对象被垃圾回收时注册一个回调函数。这个功能主要用于管理资源清理,比如取消订阅事件、清理外部资源等,旨在为开发者提供一种方式来执行清理操作,而不必显式地追踪对象的生命周期。

其使用方法可以直接参考以下示例:test.js

const registry = new FinalizationRegistry(name => {
  console.log(`${name} 已被回收`)
})

function testFunc() {
  let testLet = { name: '测试数据' }
  registry.register(testLet, 'testLet对象')
  console.log(testLet.name)
}

testFunc()
global.gc()
  • 调用该 js 文件:
    node --expose-gc test.js
  • 输出结果:
    测试数据
    testLet对象 已被回收

注意事项:

  • register 方法的第一个参数必须是一个对象,基本类型(如字符串或数字)是不允许的。
  • 不保证执行:回调函数的执行取决于垃圾回收的时机和策略,因此不保证回调一定会被执行。除非你手动触发垃圾回收。
  • 用途限制:FinalizationRegistry 主要用于资源清理等场景,不应该用于程序的主要逻辑中,因为它的行为具有不确定性不要在生产环境使用)。
  • 内存管理:虽然 FinalizationRegistry 提供了一种在对象被回收时执行代码的能力,但它不应该被用作避免内存泄漏的手段。正确的资源管理和避免不必要的全局引用仍然是防止内存泄漏的关键。

另外,Vue 中使用时,如果出现报错: “TS2304: Cannot find name 'FinalizationRegistry'”,这意味着你的 TypeScript 环境没有包含对 FinalizationRegistry 的类型定义。FinalizationRegistry 是 ES2021 中引入的特性,因此,你需要确保你的 TypeScript 配置支持 ES2021 或更高版本的特性。

解决步骤:

  1. 更新 TypeScript 版本:npm install typescript@latest --save-dev
  2. 更新 tsconfig.json 文件:确保 tsconfig.json 中的 lib 选项包含了 ES2021 或更高版本,或者确保 target 设置为 ES2021 或更高。示例配置:
{
  "compilerOptions": {
    "target": "ES2021",
    "lib": ["ESNext", "DOM"],
    // 其他配置...
  }
}

3、闭包基础

3.1 闭包的概念和特性

在第2.1节已经得知,如果一个对象没有任何引用指向它,那么这个对象就会在某一时刻被回收。而闭包就是特殊的存在。具体看如下示例:

function createCounter() {
  let count = 0
  return {
    increase: function() {
      count++
      return count
    },
    decrease: function() {
      count--
      return count
    },
    getCount: function() {
      return count
    }
  }
}

const counter = createCounter()
console.log(counter.increase()) // 输出:1
console.log(counter.increase()) // 输出:2
console.log(counter.count) // undefined
const newCounter = counter
console.log(newCounter.getCount()) // 输出:2
console.log(newCounter.decrease()) // 输出:1

const counterAgain = createCounter()
console.log(counterAgain.getCount()) // 输出:0
console.log(counterAgain.decrease()) // 输出:-1

仅观察输出结果:

  • 在访问 counter 的过程中,其中的 count 会一直记录下来(访问 newCounter 也可以,因为引用类型的赋值为对象地址,所以会指向同一个函数,详见第1.3节)。
  • 当你再声明 counterAgain,此时相当于新建 function(count 又会从 0 开始)。

分析结果:

或许第一次看会觉得很正常,但这确实已经是不知不觉间利用到了闭包的特性,也就是出现了一个不会被垃圾回收的变量 count(被 increase 等 function 一直引用),所以才能让你每次访问同一个对象 counter,都能返回上一次运行后的结果

此时只要 createCounter 不返回 count,那么它就形成了“私有变量”(当然也可以创建出私有方法)。这就是闭包的应用之一:创建私有变量和方法

在第2.1节垃圾回收中也提到,回收是需要时间的,那有没有可能只是单纯的没到回收时间呢?可以用第2.2节的方式做验证,你会发现经过很长时间都没有被回收(暂时不知道有没有更好的验证方式)。


通过示例,总结一下闭包的概念

闭包是那些能够访问“自由变量”的函数。所谓的自由变量,是指在函数中使用的变量,既不是函数参数,也不是函数的局部变量的变量也不是全局变量)。

再用上述示例复述下,即:increase、decrease 和 getCount 这三个函数都能访问到createCounter 函数作用域中的 count 变量但 count 变量既不是这些函数的参数,也不是这些函数的局部变量(也不是全局变量),所以符合了“自由变量”的概念。此时在 createCounter 函数执行完毕后,这些函数仍然能够访问 count 变量。而这三个函数与它们共同引用的 count 变量的组合,就构成了闭包


综上,闭包的特性可以总结如下:

  1. 访问外部变量:闭包可以访问定义它们的外部函数中的变量(在上文中就是 count)。
  2. 封装性:闭包可以帮助封装变量,提供类似私有变量的效果(在上文中还是 count)。
  3. 持久性:通常,当函数执行完毕后,其内部局部变量会被销毁。但闭包的存在使得外部函数执行完毕后,闭包仍然可以访问外部函数的变量,这些变量的生命周期被延长了(在上文中依然是 count)。
  4. 记忆性:闭包可以用于创建具有记忆功能的函数(getCount 方法总是能返回最新的 count 值)。

延展的更多子问题:

3.2 全局变量和闭包

其实你会发现,闭包和全局变量实现的效果是一致的。不过切记全局变量 + 函数的组合并不是闭包(全局变量自身就带有这些七七八八的特性,但闭包必须要满足相关条件)。如下:

let count = 0; // 全局变量

function increase() {
  count++;
  return count;
}

function decrease() {
  count--;
  return count;
}

function getCount() {
  return count;
}

console.log(increase()); // 输出:1
console.log(increase()); // 输出:2
console.log(getCount()); // 输出:2
console.log(decrease()); // 输出:1

此时对比一下闭包的特性,你就会发现区别:

  1. 没有封装性:count 变量是全局可访问的,任何代码都可以直接修改它,不能保证数据的安全性和一致性
  2. 缺乏模块化:所有相关的函数都暴露在全局作用域中,可能会导致命名冲突
  3. 状态共享:如果需要多个独立的变量(自由变量),全局变量方法就无法满足需求(闭包示例中,你再次创建 createCounter() ,count 就会重新生成,彼此之间不会冲突)。

3.3 自由变量存放在堆内存

在第1.3节中,已经提到过:基本数据类型会存放在栈内存中。但闭包依然是个例外,比如闭包示例中的 count,虽然是基础类型,但是会存储在堆内存中。原因如下:

当 createCounter 函数执行完毕后,正常情况下其局部变量(包括 count)应该被销毁。但由于闭包的存在(JavaScript 引擎检测到对象依然被引用),count 变量需要继续存在。此时,为了使 count 在 createCounter 函数执行完毕后仍然可以被访问,JavaScript 引擎会将其存储在堆内存中

存放在堆内存中时,引擎会创建一个特殊的对象,这个对象包含了闭包引用的所有外部变量(且作用域链中会包含对这个特殊对象的引用,使得闭包函数可以访问 count 变量)。这个特殊的对象通常被称为“词法环境”或“闭包对象”。

在这里插入图片描述
拿这个图简单说明:当函数被创建时,JavaScript 引擎显然要记录函数能够访问的所有局部变量、函数的参数信息等(内部会记录更多其他信息),并存放在内存中,这样的一个数据结构就是词法环境。(它能指向函数被创建时,其外部代码的词法环境,这个引用是实现闭包能力访问外部变量的关键

所以广义上讲,闭包的概念也可以总结成

当一个函数引用了不是函数参数,也不是函数局部变量的变量(自由变量)时,这个函数以及它的词法环境(包含了这些外部变量(自由变量)的引用)共同构成了一个闭包


4、闭包的应用

4.1 防抖(Debounce)

防抖用于限制函数的调用频率,常用于处理频繁触发的事件,只有最后一次调用会触发

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  }
}

const debouncedSearch = debounce((query) => {
  console.log('Searching for:', query);
}, 300);

// 使用示例
input.addEventListener('input', (e) => debouncedSearch(e.target.value));

(Vue 可以用第三方依赖 lodash 的 debounce,快速实现功能)


4.2 节流(Throttle)

确保函数在一定时间间隔内最多执行一次

function throttle(fn, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  }
}

const throttledResize = throttle(() => {
  console.log('Window resized');
}, 1000);

// 使用示例
window.addEventListener('resize', throttledResize);

(Vue 可以用第三方依赖 lodash 的 throttle,快速实现功能)


4.3 创建私有变量和方法

闭包可以用来创建私有变量和方法,这在第3.1节已经说明过。


4.4 函数柯里化

柯里化是一种将接受多个参数的函数转换成一系列使用一个参数的函数的技术。通过闭包,柯里化的函数可以记住每次传递给它们的参数。如下所示:

function multiply(a) {
  return function(b) {
    return a * b;
  }
}

// 使用柯里化函数
const multiplyByTwo = multiply(2);
console.log(multiplyByTwo(5)); // 输出:10

const multiplyByThree = multiply(3);
console.log(multiplyByThree(5)); // 输出:15

在这个例子中:

  1. multiply 函数接受一个参数 a,并返回一个新的函数,这个新的函数接受另一个参数 b。
  2. 当调用 multiply(a) 时,它返回一个闭包,这个闭包记住了 a 的值(因为 a 既不是它的参数,也不是它的局部变量)。
  3. 返回的函数(闭包)接受 b 作为参数,并返回 a 和 b 的乘积。
  4. 通过 multiply(2) 和 multiply(3) 创建了两个新的函数 multiplyByTwo 和 multiplyByThree,它们分别固定了乘法的第一个参数为 2 和 3。
  5. 当调用这些固定了一个参数的函数时,只需要提供第二个参数。

5、闭包-内存泄漏(如何释放闭包)

内存泄漏是指程序中已分配的内存由于某些原因未能被正确释放导致这些内存无法被再次使用,从而造成系统内存占用不断增加的现象

显然,闭包很符合这个说法:闭包会保持对其外部作用域中变量(自由变量)的引用,如果这个闭包引用了大量数据,又长期存在,那么这些被引用的变量就无法被垃圾回收,占用大量内存

关于解决的方法:

如果出现内存泄漏,且你不希望形成闭包,那就检查代码,优化相关代码即可;

如果你希望利用闭包 + 避免出现内存泄漏,此时可考虑手动释放。

  1. 移除事件监听器如果闭包被用作事件监听器,确保在不需要时移除事件监听器(Vue中,可以写在 beforeDestroy / beforeUnmount 里)。
function handleClick() {
  // 这里形成闭包…
}

button.addEventListener('click', handleClick);
// 当不再需要监听点击事件时,移除事件监听器
button.removeEventListener('click', handleClick);
  1. 解除外部引用:将声明的变量设置为 null 或 undefined 可以帮助释放闭包。
<template>
  <div class="test-page">
    {{ result && result.getValue().name }}
    <button @click="handleBtn">
      触发
    </button>
    <button @click="releaseBtn">
      释放
    </button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api'
export default defineComponent({
  name: 'TestPage',
  setup() {
    const registry = new FinalizationRegistry(name => {
      console.log(`${name} 已被回收`)
    })

    let result = ref(null) as any
    const testFunction = function() {
      let testObj = { name: '测试数据' }
      registry.register(testObj, 'testObj')
      return {
        getValue: () => testObj
      }
    }
    function handleBtn() {
      result.value = testFunction()
    }
    function releaseBtn() {
      if (result.value) {
        result.value.getValue = null
      }
      result.value = null
    }

    return {
      result,
      handleBtn,
      releaseBtn,
    }
  },
})
</script>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端Jerry_Zheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值