JavaScript 核心底层概念:作用域、闭包、var/let/const、词法作用域、作用域链、暂时性死区、闭包实战与陷阱的完整权威讲解。


一、开篇:两个核心问题

为什么有些变量随处可访问,有些却 “消失不见”?为什么函数在父函数执行完毕后,还能 “记住” 父函数里的变量?

function createCounter() {
  let count = 0 // 被封闭的变量
  return function() {
    count++
    return count
  }
}
const counter = createCounter()
counter() // 1
counter() // 2 —— 它记住了!

答案就是:作用域(Scope)闭包(Closure)

  • 作用域:决定变量在哪里可见
  • 闭包:让函数记住诞生时的环境

二、本文你将学到

  • 3 种作用域:全局、函数、块级
  • var / let / const 核心区别
  • 词法作用域、作用域链
  • 闭包原理与用途
  • 数据私有化、工厂函数、记忆化(memoization)
  • 经典闭包陷阱与避坑方法

前置知识:调用栈(call stack)。


三、什么是作用域?

作用域 = 变量的可见范围。它是一套规则,决定变量在哪里能被访问、不能被访问。

作用域可以嵌套:内层能访问外层,外层绝不能访问内层。

办公楼类比(最易懂)

  • 大厅 = 全局作用域
  • 走廊 = 函数作用域
  • 私人办公室 = 块级作用域

你在私人办公室里:能看到自己桌上的文件、走廊、大厅。但大厅里的人看不到你办公室内部。

这就是 JS 作用域的规则:内层向外看,外层不能向内看。


四、作用域的三大意义

  1. 防止命名冲突不同作用域可以用同名变量。

  2. 内存管理作用域结束,变量可被垃圾回收。

  3. 封装与安全隐藏内部实现,防止意外修改。


五、JS 的三种作用域

1. 全局作用域

不在任何函数 / 块里的变量,任何地方都能访问。

const appName = "MyApp" // 全局
function fn() { console.log(appName) }

⚠️ 尽量少用全局变量,避免全局污染。

2. 函数作用域

var 声明的变量只在函数内有效。

function fn() {
  var a = 1 // 函数作用域
}
console.log(a) // 报错

3. 块级作用域

let / const{} 内有效(if、for、while)。

if (true) {
  let a = 1
}
console.log(a) // 报错

六、var /let/const 终极对比

TDZ 是 Temporal Dead Zone(暂时性死区) 的缩写

特性 var let const
作用域 函数级 块级 块级
变量提升 提升并赋值 undefined 提升但 TDZ 提升但 TDZ
重复声明 允许 报错 报错
重新赋值 允许 允许 不允许
必须初始化

现代最佳实践

  • 默认用 const
  • 需要变用 let
  • 永远不用 var

七、变量提升与暂时性死区(TDZ)

变量提升

var 会被提升到顶部,值为 undefined

console.log(a) // undefined
var a = 1

暂时性死区(TDZ)

let/const 提升了,但不能访问,直到声明行。

console.log(name) // 报错!
let name = "Alice"

这段不能访问的区域就叫 暂时性死区


八、词法作用域(Lexical Scope)

词法作用域 = 静态作用域变量的作用域由代码写在哪决定,不是运行时决定。

内层函数永远能访问外层函数的变量。

function outer() {
  let a = 1
  function inner() {
    console.log(a) // 能访问
  }
}

九、作用域链(Scope Chain)

JS 查找变量的顺序:

  1. 当前作用域
  2. 父作用域
  3. 向上一直找…
  4. 全局作用域
  5. 找不到 → 报错

十、变量遮蔽(Variable Shadowing)

内层同名变量会 “遮住” 外层变量。

const name = "global"
function fn() {
  const name = "function"
  console.log(name) // function
}

十一、什么是闭包(Closure)?

闭包 = 函数 + 它诞生时的词法环境

即使外层函数执行完毕,内部函数依然可以访问并保留外层函数的变量

简单说:函数记着它 “老家” 的变量,永远忘不了。

闭包执行步骤

  1. 调用外部函数
  2. 创建内部函数
  3. 内部函数记住词法环境
  4. 外部函数结束
  5. 内部函数依旧能访问外部变量

十二、闭包四大实战用途

1. 数据私有化(封装)

真正的私有变量,无法从外部篡改。

function createCounter() {
  let count = 0 // 私有
  return {
    inc: () => count++,
    get: () => count
  }
}

2. 函数工厂

批量生成带不同配置的函数。

function createMultiplier(m) {
  return n => n * m
}
const double = createMultiplier(2)
double(5) // 10

3. 回调与事件保存状态

function setupBtn() {
  let count = 0
  btn.onclick = () => {
    count++
  }
}

4. 记忆化(缓存计算结果)

function memoize(fn) {
  const cache = {}
  return arg => {
    if (cache[arg]) return cache[arg]
    cache[arg] = fn(arg)
    return cache[arg]
  }
}

十三、经典闭包面试题(必看)

问题

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// 输出:3、3、3

原因

var 是函数级作用域,共用同一个 i。等定时器执行时,i 已经变成 3。

三种正确解法

  1. let(最简单)
  2. IIFE
  3. forEach
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// 输出 0、1、2

十四、闭包可能造成内存泄漏

闭包会保留对外层变量的引用,导致无法被垃圾回收。解决方法:

  • 不需要时清空引用
  • 及时 removeEventListener

十五、核心总结

  • 作用域:变量可见范围
  • 3 种作用域:全局、函数、块级
  • let/const:块级、有 TDZ、不污染
  • var:函数级、易出错、别用
  • 词法作用域:代码位置决定访问权
  • 闭包:函数记住诞生环境
  • 闭包用途:私有化、工厂、缓存、状态保存
  • 经典坑:for 循环用 var → 换成 let

常见误区与陷阱(完整原文翻译)

理解作用域与闭包,也意味着要知道哪里最容易出错。这些错误甚至会难倒经验丰富的开发者。

闭包界 #1 面试题

这是最经典的闭包陷阱,几乎所有人第一次都会答错。

问题:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

大部分人以为输出:0, 1, 2真实输出:3, 3, 3

为什么会这样?

  • var i函数作用域,整个循环只有 一个 i
  • 循环瞬间执行完,i 已经变成 3
  • 三个定时器回调共享同一个 i
  • 1 秒后执行时,它们读到的都是最终值 3

三种解决方案

方案 1:使用 let(现代最佳)let 是块级作用域,每次循环都会创建一个新 i

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

方案 2:使用 IIFE(老式方法)

for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 1000);
  })(i);
}

方案 3:使用 forEach

[0,1,2].forEach(i => {
  setTimeout(() => console.log(i), 1000);
});

闭包导致的内存泄漏

闭包非常强大,但也要负责任地使用。因为闭包会持有外部作用域变量的引用,这些变量无法被垃圾回收

例子:

function createHeavyClosure() {
  const hugeData = new Array(1000000);

  return function() {
    console.log(hugeData.length);
  };
}

const leakyFn = createHeavyClosure();

hugeData 会一直留在内存中,因为闭包一直引用它。

如何避免内存泄漏

  • 不再使用时,解除引用
  • 及时移除事件监听 removeEventListener
  • 不要在闭包中捕获不必要的大型对象
cleanup(); // 手动解除引用,让GC可以回收

最佳实践

  • 默认使用 const,变化使用 let永远不要用 var
  • 最小化作用域,把变量写在离使用最近的地方
  • 避免不必要的全局变量
  • 避免变量遮蔽(容易引发 BUG)
  • 闭包中只捕获需要的变量
  • 及时清理事件监听与定时器
  • 循环异步优先使用 let,绝对不要用 var

核心要点总结

  • Scope = 变量的可见范围
  • 三种作用域:全局、函数、块级
  • var → 函数作用域;let/const → 块级作用域
  • 词法作用域:由代码位置决定,与调用位置无关
  • 作用域链:由内向外查找变量
  • TDZ(暂时性死区):let/const 声明前不可访问
  • 闭包 = 函数 + 词法环境
  • 闭包让函数永远记得诞生时的作用域
  • 闭包用途:私有变量、工厂函数、状态保存、记忆化缓存
  • 循环陷阱:var 共享变量 → 换成 let 立即解决
  • 闭包可能造成内存泄漏,必须及时清理

知识自测

Q1:JavaScript 中有哪三种作用域?

全局作用域、函数作用域、块级作用域。

Q2:什么是暂时性死区 TDZ?

let/const 从进入作用域到声明行之间,无法被访问的区域。访问会报错。

Q3:什么是词法作用域?

作用域由代码书写位置决定,而非运行时调用位置。内层可以访问外层。

Q4:什么是闭包?

函数与其词法环境的组合。函数即使在外部执行,依然能访问定义时所在的作用域。

Q5:下面代码输出什么?为什么?

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}

输出:3,3,3因为 var 是函数作用域,共用一个 i。

Q6:实际开发中闭包用于哪些场景?

  • 数据私有化
  • 工厂函数
  • 事件回调 / 异步状态保存
  • 记忆化缓存(memoization)

常见问题 FAQ

Q:作用域和闭包的区别?

作用域是规则:变量在哪里可访问。闭包是现象:函数能记住并访问定义时的作用域。

Q:为什么要用 let/const,不要用 var?

  • let/const 是块级作用域
  • 不存在变量污染
  • 有暂时性死区,减少错误
  • const 明确不可变

Q:闭包如何工作?

函数在定义时捕获词法环境,即使外部函数执行完毕,内部函数依然持有该环境引用。

Q:闭包会导致内存泄漏吗?

会,如果闭包长期持有不必要的大对象引用,就会产生内存泄漏。

Q:JS 是词法作用域还是动态作用域?

JS 是词法作用域(静态作用域)。


🧩 概念汇总

1. 核心概念

  • Scope(作用域)同类:变量可见性、命名空间、上下文、访问范围
  • Closure(闭包)同类:词法环境捕获、状态保留、函数携带作用域
  • Lexical Scope(词法作用域)同类:静态作用域、书写位置决定作用域
  • Scope Chain(作用域链)同类:变量查找路径、由内向外查找

2. 作用域类型

  • Global Scope(全局作用域)同类:顶层作用域、全局变量
  • Function Scope(函数作用域)同类:函数级可见性、var 作用域
  • Block Scope(块级作用域)同类:{} 作用域、let/const 作用域
  • Module Scope(模块作用域)同类:ESM 文件级作用域、文件隔离

3. 变量声明相关

  • var同类:老式声明、函数级、提升、可重复声明
  • let同类:块级、可变、不可重复、TDZ
  • const同类:块级、不可重赋值、引用可变、TDZ
  • Hoisting(变量提升)同类:声明提前、预解析
  • Temporal Dead Zone(TDZ 暂时性死区)同类:声明前不可访问、安全检查

4. 闭包相关机制

  • Lexical Environment(词法环境)同类:作用域记录、变量环境
  • Variable Shadowing(变量遮蔽)同类:同名覆盖、内层覆盖外层
  • Encapsulation(封装)同类:数据私有化、信息隐藏
  • Memoization(记忆化)同类:缓存计算结果、优化性能
  • Function Factory(函数工厂)同类:批量生成函数、配置化函数

5. 内存与垃圾回收

  • Garbage Collection(GC 垃圾回收)同类:自动内存管理、释放无用变量
  • Memory Leak(内存泄漏)同类:无用对象残留、闭包引用未释放

6. 经典问题与模式

  • IIFE(立即执行函数)同类:老式模块化、创建私有作用域
  • Loop Closure Bug(循环闭包 BUG)同类:var 共享变量、异步延迟问题

更多推荐