Skip to content

第三章:作用域、执行上下文、作用域链、闭包

1. 它是什么

这一章先记住 4 个关键词:

  1. 作用域
  2. 执行上下文
  3. 作用域链
  4. 闭包 它们不是四个孤立点,而是一条线:
  • 作用域:变量在哪能访问
  • 执行上下文:函数执行时创建的运行环境
  • 作用域链:当前代码查找变量的路径
  • 闭包:函数和其词法作用域的组合,使变量在函数外部调用时仍可被访问
    你只要把这条线吃透,后面的 this原型链异步 都会顺很多。

2. 作用域是什么

它是什么
作用域就是:
变量、函数、参数可以被访问的范围
JS 里常见 3 种作用域:

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

2.1 全局作用域

在最外层声明的变量,通常属于全局作用域。

js
let a = 1

function fn() {
  console.log(a)
}

这里 a 在全局可访问,函数里也能访问。

2.2 函数作用域

函数内部声明的变量,只能在函数内部访问。

js
function fn() {
  let a = 1
  console.log(a)
}

fn()
// console.log(a) // 报错

2.3 块级作用域

letconst 会形成块级作用域,var 不会。

js
{
  let a = 1
  const b = 2
  var c = 3
}

console.log(c) // 3
// console.log(a) // 报错
// console.log(b) // 报错

3. 为什么这样设计

如果没有作用域,会有什么问题?

js
let a = 1

function test() {
  let a = 2
}

如果没有访问范围的限制,变量名冲突会非常严重,程序会混乱。
所以作用域的意义就是:

  1. 隔离变量,避免污染
  2. 控制访问范围
  3. 支持模块化和封装
  4. 让变量查找有规则 你可以理解成:
    作用域是 JS 管理变量可见性的规则系统

4. 底层怎么运行:词法作用域

这是本章第一个核心。
JS 采用的是:词法作用域,也叫 静态作用域
意思是:
一个变量能不能被访问,不是在函数调用时决定的,而是在函数定义时决定的
看代码:

js
let a = 1

function fn() {
  console.log(a)
}

function test() {
  let a = 2
  fn()
}

test()

输出是:

js
1

为什么不是 2
因为 fn 在定义的时候,外层环境是全局作用域,所以它找 a 时,沿着自己定义时的作用域往外找,而不是按调用位置找。
这就是词法作用域。
你一定要记住这句话:
JS 的作用域在编写代码时基本就确定了,不是调用时临时决定的。

5. 执行上下文是什么

它是什么 执行上下文可以理解成:
代码执行时的运行环境
每当 JS 执行一段代码时,都会进入一个对应的执行上下文。
常见有三种:

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval 执行上下文(基本不用展开) 比如:
js
let a = 1

function fn(x) {
  let b = 2
  console.log(x + b)
}

fn(3)

执行时会有:

  • 全局执行上下文
  • fn 的函数执行上下文
    为什么这样设计
    因为函数执行时,JS 需要知道:
  • 当前 this 是谁
  • 当前参数有哪些
  • 当前局部变量有哪些
  • 当前去哪里找外部变量

所以必须给每次执行创建一个“运行环境”。
底层怎么运行 一个执行上下文里,面试常见可以这样理解:

  1. 变量/函数声明
  2. 作用域链
  3. this 指向
    以前很多资料会讲:
  • 变量对象 VO
  • 活动对象 AO
    现在更接近规范的表述通常是:
  • 词法环境 Lexical Environment
  • 变量环境 Variable Environment
    你面试里不用讲太深规范名词,但要知道:
    函数执行时,会创建自己的局部环境,用来保存参数、局部变量、函数声明,并且记录外层作用域引用。 这句话很重要。

6. 执行上下文和调用栈

函数执行不是平铺的,而是通过 调用栈 管理。
看代码:

js
function a() {
  b()
}

function b() {
  c()
}

function c() {
  console.log('end')
}

a()

执行顺序可以理解为:

  1. 进入全局执行上下文
  2. 调用 a,压栈
  3. 调用 b,压栈
  4. 调用 c,压栈
  5. c 执行完出栈
  6. b 出栈
  7. a 出栈 所以调用栈本质上就是:
    管理函数执行顺序的一种后进先出结构
    后面讲 Event Loop 时会再次用到这个概念。

7. 作用域链是什么

它是什么 作用域链就是:
当前作用域查找变量时,逐层向外查找的路径
例如:

js
let a = 1

function outer() {
  let b = 2

  function inner() {
    let c = 3
    console.log(a, b, c)
  }

  inner()
}

outer()

inner 里查找变量时:

  1. 先找自己内部有没有 a b c
  2. 没有的继续去 outer
  3. 再没有去全局找

这条查找路径就是作用域链。

为什么这样设计 因为函数内部经常需要访问外层变量,比如:

  • 配置
  • 参数
  • 缓存
  • 状态

如果每个函数都只能访问自己内部变量,写程序会很麻烦。
所以 JS 允许内部函数访问外部函数作用域。
底层怎么运行 本质上是:
每个函数在创建时,都会保存它对外层词法环境的引用 等函数执行时,如果当前作用域找不到某个变量,就顺着这个外层引用往上找。
这个“往上找”的过程,就是作用域链查找。

8. var、let、const 的区别

这是作用域章节必须吃透的点。

8.1 var

  • 函数作用域
  • 没有块级作用域
  • 存在变量提升
  • 可以重复声明
js
if (true) {
  var a = 1
}
console.log(a) // 1

8.2 let

  • 块级作用域
  • 存在提升,但不能在声明前使用
  • 不可重复声明
js
if (true) {
  let a = 1
}
// console.log(a) // 报错

8.3 const

  • 也是块级作用域
  • 声明时必须赋值
  • 不可重复声明
  • 不能修改绑定关系,但如果值是对象,可以修改对象内部属性
js
const obj = { name: 'Tom' }
obj.name = 'Jack' // 可以
// obj = {} // 不可以

9. 什么是变量提升

它是什么 变量提升是指:
声明在代码执行前的准备阶段就已经被处理了 例如:

js
console.log(a)
var a = 1

不会报“a 未定义”,而是输出:

js
undefined

因为 var a 的声明被提升了,但赋值没有提升。
可以粗略理解为:

js
var a
console.log(a)
a = 1

函数提升 函数声明也会提升,而且优先级通常高于 var

js
fn()

function fn() {
  console.log(1)
}

这能正常执行。

10. 暂时性死区 TDZ

这是 letconst 的重点。

js
console.log(a)
let a = 1

这里会报错,不会像 var 一样得到 undefined
因为:
letconst 虽然也会在作用域创建时被处理,但在真正声明之前,不能访问,这段区域叫暂时性死区`
你面试里可以这么答:
let 和 const 也会提升,但不会初始化为可访问状态,在声明之前访问会触发暂时性死区错误。
这句话比“let 不提升”更准确。

11. 闭包是什么

这是本章最核心的内容。 它是什么 闭包可以先记最经典定义: 闭包是函数和其词法作用域的组合
如果说得更白一点:
一个函数即使在外部作用域执行,仍然能够访问它定义时所在作用域中的变量,这种现象背后的机制就是闭包
看代码:

js
function outer() {
  let a = 1

  function inner() {
    console.log(a)
  }

  return inner
}

const fn = outer()
fn()

输出:

js
1

为什么 outer() 已经执行完了,a 还能访问?
因为 inner 形成了闭包,它持有对 outer 作用域的引用。

12. 为什么这样设计闭包

因为很多场景下,我们就是需要:

  1. 保存状态
  2. 做封装
  3. 做函数工厂
  4. 实现私有变量

例如计数器:

js
function createCounter() {
  let count = 0

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

const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3

如果没有闭包,这种“私有状态持续存在”的能力就很难实现。

13. 闭包底层怎么运行

闭包不是神秘机制,本质上还是前面那条线:

  1. 函数定义时记录外层词法环境
  2. 内部函数被返回或传递到外部
  3. 外部仍然持有这个内部函数
  4. 内部函数还需要访问外层变量
  5. 所以外层变量不能被直接销毁

也就是说:
只要还有地方引用这个内部函数,而内部函数又依赖外层变量,那么这些外层变量就会继续留在内存里
这就是为什么闭包既强大,也可能带来内存占用。

14. 闭包的实际开发用途

  1. 封装私有变量
js
function createUser() {
  let name = 'Tom'

  return {
    getName() {
      return name
    },
    setName(newName) {
      name = newName
    }
  }
}
  1. 函数工厂
js
function add(x) {
  return function (y) {
    return x + y
  }
}

const add5 = add(5)
console.log(add5(3)) // 8
  1. 事件处理里保存上下文
  2. 防抖、节流
  3. 柯里化 后面手写题会频繁遇到。

15. 闭包的风险

闭包本身不是坏事,问题在于:
无意义地长期持有不再需要的大对象或大作用域 例如:

js
function outer() {
  const bigData = new Array(1000000).fill('x')

  return function () {
    console.log(bigData.length)
  }
}

如果返回的函数一直存在,bigData 就一直不能被回收。
所以你要记住:
闭包可能导致内存占用增加,但不是所有闭包都会造成内存泄漏。只有不必要的长期引用才会成为问题。

16. 面试高频题:什么是作用域链

标准回答作用域链是变量查找的路径。当代码在当前作用域中访问某个变量时,会先在当前作用域查找,找不到就沿着外层词法环境逐级向上查找,直到全局作用域为止。因为 JavaScript 采用词法作用域,所以这个查找链条在函数定义时基本就确定了。

17. 面试高频题:什么是闭包

标准回答闭包本质上是函数和其定义时词法作用域的组合。即使函数在外部作用域执行,它依然可以访问创建它时所在作用域中的变量。闭包常用于封装私有变量、保存状态和实现函数工厂,但如果不合理使用,也可能导致本该释放的数据长期留在内存中。

18. 面试高频题:var、let、const 区别

标准回答var 是函数作用域,存在变量提升,没有块级作用域;let 和 const 是块级作用域,也会提升,但在声明前存在暂时性死区,不能访问。const 与 let 的区别在于 const 必须在声明时赋值,且不能修改变量的绑定关系,但如果值是对象,仍然可以修改对象内部属性。

19. 这一章你必须真正吃透的 7 句话

  1. 作用域决定变量在哪里可以被访问。
  2. JavaScript 采用词法作用域,作用域在函数定义时基本确定。
  3. 执行上下文是代码执行时的运行环境。
  4. 调用栈用于管理函数执行顺序。
  5. 作用域链是变量逐层向外查找的路径。
  6. 闭包的本质是函数持有其定义时外层作用域的引用。
  7. 闭包可以保存状态和封装变量,但不合理使用会增加内存占用。

20. 给你几个判断题

题 1

js
let a = 1

function fn() {
  console.log(a)
}

function test() {
  let a = 2
  fn()
}

test()

答案:1 因为 fn 的作用域在定义时就确定了。

题 2

js
if (true) {
  var a = 1
  let b = 2
}
console.log(a)
console.log(b)

答案:

  • a 输出 1
  • b 报错
    因为 var 没有块级作用域,let 有。

题 3

js
function outer() {
  let count = 0
  return function () {
    count++
    return count
  }
}

const fn = outer()
console.log(fn())
console.log(fn())

答案:

  • 1
  • 2

因为闭包保存了 count

21. 这一章你先自己回答这 6 题

你按自己的话答,我再帮你逐题纠偏:

  1. 什么是作用域?为什么需要作用域?

作用域是指变量、函数、参数的可访问范围。JavaScript 中常见有全局作用域、函数作用域和块级作用域。作用域的意义在于隔离变量、避免命名污染、控制访问范围,并支持封装和模块化开发。

  1. 什么是词法作用域?为什么说 JS 是静态作用域?

词法作用域指的是作用域关系在代码书写或函数定义时就基本确定,而不是在函数调用时动态决定。因此 JavaScript 是静态作用域语言,函数查找变量时,依据的是它定义时所在的词法环境,而不是调用它的位置。

  1. 什么是执行上下文?

执行上下文是代码执行时创建的运行环境。常见有全局执行上下文、函数执行上下文和 eval 执行上下文。函数执行时会创建自己的上下文,用来保存 this、参数、局部变量、函数声明,并建立与外层词法环境的关联。

  1. 什么是作用域链?

作用域链是变量查找的路径。当代码访问某个变量时,会先在当前作用域中查找,找不到就沿着外层词法环境逐级向上查找,直到全局作用域为止。由于 JavaScript 采用词法作用域,所以这条查找链在函数定义时就基本确定了。

  1. 什么是闭包?为什么 outer 执行完了,里面的变量还没销毁?

闭包本质上是函数和其定义时词法作用域的组合。即使函数在外部执行,它依然可以访问创建它时所在作用域中的变量。outer 执行完成后,如果返回的内部函数仍然引用 outer 中的变量,那么这些变量对内部函数来说仍然是可达的,因此不会被垃圾回收。

  1. var、let、const 的区别是什么?

var 是函数作用域,没有块级作用域,存在变量提升,并且允许重复声明。let 和 const 是块级作用域,也会提升,但在声明前处于暂时性死区,不能访问,且都不允许重复声明。const 与 let 的区别是 const 声明时必须赋值,且不能修改变量的绑定关系,但如果值是对象,仍然可以修改对象内部属性。