目录
[1.1 一句话定义](#1.1 一句话定义)
[1.2 核心三点(必背)](#1.2 核心三点(必背))
[1.3 代码示例](#1.3 代码示例)
[1. 私有变量(模块化)](#1. 私有变量(模块化))
[2. 防抖函数](#2. 防抖函数)
[3. 节流函数](#3. 节流函数)
[4. 循环中的经典问题(面试常考)](#4. 循环中的经典问题(面试常考))
[四、作用域链(Scope Chain)](#四、作用域链(Scope Chain))
[4.1 定义](#4.1 定义)
[4.2 图解作用域链](#4.2 图解作用域链)
[4.3 作用域的类型](#4.3 作用域的类型)
[1. 只能由内向外查找](#1. 只能由内向外查找)
[2. 词法作用域(静态作用域)](#2. 词法作用域(静态作用域))
[3. 内层共享外层变量](#3. 内层共享外层变量)
一、闭包(Closure)
1.1 一句话定义
函数嵌套函数,内部函数引用了外部函数的变量,就形成了闭包。这些变量不会被垃圾回收,会一直保留在内存中。
1.2 核心三点(必背)
1. 函数嵌套函数
2. 内部函数引用外部函数的变量/参数
3. 外部函数执行完后,变量依然保存在内存中
1.3 代码示例
javascript
function outer() {
let a = 1 // 外部函数的变量
function inner() { // 1. 函数嵌套函数
console.log(a) // 2. 内部函数引用外部变量 → 形成闭包
}
return inner // 把内部函数返回出去
}
const fn = outer() // outer 执行完了
fn() // 3. 依然能访问到 a → 输出 1
执行过程图解:
outer() 执行:
┌─────────────────────────────────────────┐
│ outer 作用域 │
│ a = 1 │
│ inner = function │
└─────────────────────────────────────────┘
│
│ 返回 inner
↓
outer 执行完毕,按理说 a 应该被回收
│
│ 但 inner 还在引用 a
↓
┌─────────────────────────────────────────┐
│ 闭包:a 被保留在内存中 │
│ fn 可以继续访问 a │
└─────────────────────────────────────────┘
二、闭包的常见应用场景
1. 私有变量(模块化)
javascript
function createCounter() {
let count = 0 // 私有变量,外部无法直接访问
return {
increment() {
count++
return count
},
decrement() {
count--
return count
},
getCount() {
return count
}
}
}
const counter = createCounter()
console.log(counter.getCount()) // 0
counter.increment() // 1
counter.increment() // 2
console.log(counter.count) // undefined(无法直接访问)
2. 防抖函数
javascript
function debounce(fn, delay) {
let timer = null // 闭包保存 timer
return function(...args) {
clearTimeout(timer) // 每次调用都能访问到同一个 timer
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
const debouncedSearch = debounce(search, 500)
3. 节流函数
javascript
function throttle(fn, interval) {
let lastTime = 0 // 闭包保存 lastTime
return function(...args) {
const now = Date.now()
if (now - lastTime >= interval) {
fn.apply(this, args)
lastTime = now
}
}
}
4. 循环中的经典问题(面试常考)
javascript
// ❌ 错误:var 没有块级作用域
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i) // 输出 3, 3, 3(不是 0,1,2)
}, 100)
}
// ✅ 解决方案1:闭包
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => {
console.log(j) // 输出 0,1,2
}, 100)
})(i)
}
// ✅ 解决方案2:let(推荐)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i) // 输出 0,1,2
}, 100)
}
块级作用域让每次循环都能"新建"一个独立的变量,而不是共用一个变量。
用 var |
用 let |
|---|---|
只有一个 i,循环结束后变成 3 |
每次循环都有一个独立的 i |
所有定时器都读取同一个 i(3) |
每个定时器读取自己的 i(0,1,2) |
输出 3 3 3 |
输出 0 1 2 |
三、闭包的优缺点
优点
| 优点 | 说明 |
|---|---|
| 私有化变量 | 避免污染全局作用域 |
| 变量长期驻留 | 实现缓存、累加器等 |
| 模块化 | 暴露方法,隐藏内部数据 |
| 函数工厂 | 动态生成函数 |
缺点
| 缺点 | 说明 | 解决方案 |
|---|---|---|
| 内存泄漏 | 变量不被回收 | 不用时手动赋值为 null |
| 性能开销 | 比普通函数多占用内存 | 谨慎使用,及时释放 |
javascript
// 手动释放闭包引用
function createBigData() {
let bigData = new Array(1000000).fill('data')
return function() {
return bigData.length
}
}
const fn = createBigData()
console.log(fn()) // 使用
// 不再需要时,解除引用
fn = null // bigData 才会被垃圾回收
四、作用域链(Scope Chain)
4.1 定义
当你在代码中使用一个变量时,JS 会先在当前作用域找,找不到就去外层作用域找,再找不到继续往外,直到全局作用域。这条从内到外的查找路径,就叫作用域链。
4.2 图解作用域链
javascript
// 全局作用域
let globalVar = 'global'
function outer() {
// outer 函数作用域
let outerVar = 'outer'
function inner() {
// inner 函数作用域
let innerVar = 'inner'
console.log(innerVar) // 找自己 → 找到 ✅
console.log(outerVar) // 自己找不到 → 去 outer 找 → 找到 ✅
console.log(globalVar) // outer 找不到 → 去全局找 → 找到 ✅
console.log(xxx) // 全局也找不到 → 报错 ❌
}
inner()
}
outer()
作用域链图示:
inner 作用域
│
│ 找不到就去
↓
outer 作用域
│
│ 找不到就去
↓
全局作用域
│
│ 找不到就报错
↓
undefined / 报错
4.3 作用域的类型
| 作用域类型 | 说明 | 示例 |
|---|---|---|
| 全局作用域 | 在任何地方都能访问 | window、globalThis |
| 函数作用域 | 函数内部声明的变量,外部访问不到 | function fn() { var a = 1 } |
| 块级作用域 | {} 内部声明的变量,只在块内有效 |
if () { let a = 1 } |
五、作用域链的特点
1. 只能由内向外查找
javascript
let a = 'global'
function outer() {
let a = 'outer' // 同名变量会遮蔽外层的
function inner() {
let a = 'inner'
console.log(a) // 'inner'(找到就停,不会继续向外)
}
inner()
}
2. 词法作用域(静态作用域)
作用域在函数定义时就确定了,不是调用时!
javascript
let a = 'global'
function fn1() {
console.log(a) // 定义时 a 指向全局
}
function fn2() {
let a = 'fn2'
fn1() // 调用 fn1,但 fn1 的作用域在定义时就确定了
}
fn2() // 输出 'global',不是 'fn2'!
图解:
fn1 定义时:
┌─────────────────────────────────────────┐
│ fn1 的作用域链: │
│ 1. 自己的作用域(没有 a) │
│ 2. 全局作用域(a = 'global') │
└─────────────────────────────────────────┘
不管在哪里调用 fn1,都按这个链查找!
3. 内层共享外层变量
javascript
function createCounter() {
let count = 0
return {
increment() { count++ },
decrement() { count-- },
getCount() { return count }
}
}
const counter = createCounter()
counter.increment()
counter.increment()
console.log(counter.getCount()) // 2
// 多个方法共享同一个 count 变量
六、闭包和作用域链的关系(面试必问)
| 概念 | 定义 | 关系 |
|---|---|---|
| 作用域链 | 变量查找的机制 | 底层原理 |
| 闭包 | 函数+外部变量的现象 | 作用域链的具体表现 |
一句话总结:
作用域链是机制,闭包是现象。闭包能实现,就是因为内部函数被返回后,依然保留着原来的作用域链。
七、面试高频问题
Q1:什么是闭包?有什么作用?
答:
闭包是函数嵌套函数,内部函数引用外部函数的变量形成的特性。即使外部函数执行完毕,这些变量也不会被回收。
作用:
私有化变量,避免全局污染
让变量长期驻留内存(缓存、累加器)
实现防抖、节流
模块化封装
Q2:闭包会导致内存泄漏吗?
答:
会。因为闭包引用的变量不会被垃圾回收,如果不及时释放,会导致内存泄漏。解决方法是:不用时将变量赋值为
null。
Q3:什么是作用域链?
答:
当访问变量时,JS 会从当前作用域开始,逐级向外层作用域查找,直到全局作用域。这条查找路径就是作用域链。它是词法作用域的体现,在函数定义时就已确定。
Q4:闭包和作用域链的关系?
答:
作用域链是底层机制,闭包是这个机制的具体表现。闭包之所以能访问外部函数的变量,就是因为内部函数保留了原来的作用域链。