第三章:作用域、执行上下文、作用域链、闭包
1. 它是什么
这一章先记住 4 个关键词:
作用域执行上下文作用域链闭包它们不是四个孤立点,而是一条线:
- 作用域:变量在哪能访问
- 执行上下文:函数执行时创建的运行环境
- 作用域链:当前代码查找变量的路径
- 闭包:函数和其词法作用域的组合,使变量在函数外部调用时仍可被访问
你只要把这条线吃透,后面的this、原型链、异步都会顺很多。
2. 作用域是什么
它是什么
作用域就是:变量、函数、参数可以被访问的范围
JS 里常见 3 种作用域:
- 全局作用域
- 函数作用域
- 块级作用域
2.1 全局作用域
在最外层声明的变量,通常属于全局作用域。
let a = 1
function fn() {
console.log(a)
}这里 a 在全局可访问,函数里也能访问。
2.2 函数作用域
函数内部声明的变量,只能在函数内部访问。
function fn() {
let a = 1
console.log(a)
}
fn()
// console.log(a) // 报错2.3 块级作用域
let 和 const 会形成块级作用域,var 不会。
{
let a = 1
const b = 2
var c = 3
}
console.log(c) // 3
// console.log(a) // 报错
// console.log(b) // 报错3. 为什么这样设计
如果没有作用域,会有什么问题?
let a = 1
function test() {
let a = 2
}如果没有访问范围的限制,变量名冲突会非常严重,程序会混乱。
所以作用域的意义就是:
- 隔离变量,避免污染
- 控制访问范围
- 支持模块化和封装
- 让变量查找有规则 你可以理解成:
作用域是 JS 管理变量可见性的规则系统
4. 底层怎么运行:词法作用域
这是本章第一个核心。
JS 采用的是:词法作用域,也叫 静态作用域。
意思是:一个变量能不能被访问,不是在函数调用时决定的,而是在函数定义时决定的
看代码:
let a = 1
function fn() {
console.log(a)
}
function test() {
let a = 2
fn()
}
test()输出是:
1为什么不是 2?
因为 fn 在定义的时候,外层环境是全局作用域,所以它找 a 时,沿着自己定义时的作用域往外找,而不是按调用位置找。
这就是词法作用域。
你一定要记住这句话:JS 的作用域在编写代码时基本就确定了,不是调用时临时决定的。
5. 执行上下文是什么
它是什么 执行上下文可以理解成:代码执行时的运行环境
每当 JS 执行一段代码时,都会进入一个对应的执行上下文。
常见有三种:
- 全局执行上下文
- 函数执行上下文
- eval 执行上下文(基本不用展开) 比如:
let a = 1
function fn(x) {
let b = 2
console.log(x + b)
}
fn(3)执行时会有:
- 全局执行上下文
fn的函数执行上下文
为什么这样设计
因为函数执行时,JS 需要知道:- 当前 this 是谁
- 当前参数有哪些
- 当前局部变量有哪些
- 当前去哪里找外部变量
所以必须给每次执行创建一个“运行环境”。
底层怎么运行 一个执行上下文里,面试常见可以这样理解:
- 变量/函数声明
- 作用域链
- this 指向
以前很多资料会讲:
- 变量对象 VO
- 活动对象 AO
现在更接近规范的表述通常是: - 词法环境 Lexical Environment
- 变量环境 Variable Environment
你面试里不用讲太深规范名词,但要知道:函数执行时,会创建自己的局部环境,用来保存参数、局部变量、函数声明,并且记录外层作用域引用。这句话很重要。
6. 执行上下文和调用栈
函数执行不是平铺的,而是通过 调用栈 管理。
看代码:
function a() {
b()
}
function b() {
c()
}
function c() {
console.log('end')
}
a()执行顺序可以理解为:
- 进入全局执行上下文
- 调用 a,压栈
- 调用 b,压栈
- 调用 c,压栈
- c 执行完出栈
- b 出栈
- a 出栈 所以调用栈本质上就是:
管理函数执行顺序的一种后进先出结构
后面讲 Event Loop 时会再次用到这个概念。
7. 作用域链是什么
它是什么 作用域链就是:当前作用域查找变量时,逐层向外查找的路径
例如:
let a = 1
function outer() {
let b = 2
function inner() {
let c = 3
console.log(a, b, c)
}
inner()
}
outer()inner 里查找变量时:
- 先找自己内部有没有
a b c - 没有的继续去
outer找 - 再没有去全局找
这条查找路径就是作用域链。
为什么这样设计 因为函数内部经常需要访问外层变量,比如:
- 配置
- 参数
- 缓存
- 状态
如果每个函数都只能访问自己内部变量,写程序会很麻烦。
所以 JS 允许内部函数访问外部函数作用域。
底层怎么运行 本质上是:每个函数在创建时,都会保存它对外层词法环境的引用 等函数执行时,如果当前作用域找不到某个变量,就顺着这个外层引用往上找。
这个“往上找”的过程,就是作用域链查找。
8. var、let、const 的区别
这是作用域章节必须吃透的点。
8.1 var
- 函数作用域
- 没有块级作用域
- 存在变量提升
- 可以重复声明
if (true) {
var a = 1
}
console.log(a) // 18.2 let
- 块级作用域
- 存在提升,但不能在声明前使用
- 不可重复声明
if (true) {
let a = 1
}
// console.log(a) // 报错8.3 const
- 也是块级作用域
- 声明时必须赋值
- 不可重复声明
- 不能修改绑定关系,但如果值是对象,可以修改对象内部属性
const obj = { name: 'Tom' }
obj.name = 'Jack' // 可以
// obj = {} // 不可以9. 什么是变量提升
它是什么 变量提升是指:声明在代码执行前的准备阶段就已经被处理了 例如:
console.log(a)
var a = 1不会报“a 未定义”,而是输出:
undefined因为 var a 的声明被提升了,但赋值没有提升。
可以粗略理解为:
var a
console.log(a)
a = 1函数提升 函数声明也会提升,而且优先级通常高于 var。
fn()
function fn() {
console.log(1)
}这能正常执行。
10. 暂时性死区 TDZ
这是 let 和 const 的重点。
console.log(a)
let a = 1这里会报错,不会像 var 一样得到 undefined。
因为:let 和 const 虽然也会在作用域创建时被处理,但在真正声明之前,不能访问,这段区域叫暂时性死区`
你面试里可以这么答:
let 和 const 也会提升,但不会初始化为可访问状态,在声明之前访问会触发暂时性死区错误。
这句话比“let 不提升”更准确。
11. 闭包是什么
这是本章最核心的内容。 它是什么 闭包可以先记最经典定义: 闭包是函数和其词法作用域的组合
如果说得更白一点:一个函数即使在外部作用域执行,仍然能够访问它定义时所在作用域中的变量,这种现象背后的机制就是闭包
看代码:
function outer() {
let a = 1
function inner() {
console.log(a)
}
return inner
}
const fn = outer()
fn()输出:
1为什么 outer() 已经执行完了,a 还能访问?
因为 inner 形成了闭包,它持有对 outer 作用域的引用。
12. 为什么这样设计闭包
因为很多场景下,我们就是需要:
- 保存状态
- 做封装
- 做函数工厂
- 实现私有变量
例如计数器:
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. 闭包底层怎么运行
闭包不是神秘机制,本质上还是前面那条线:
- 函数定义时记录外层词法环境
- 内部函数被返回或传递到外部
- 外部仍然持有这个内部函数
- 内部函数还需要访问外层变量
- 所以外层变量不能被直接销毁
也就是说:只要还有地方引用这个内部函数,而内部函数又依赖外层变量,那么这些外层变量就会继续留在内存里
这就是为什么闭包既强大,也可能带来内存占用。
14. 闭包的实际开发用途
- 封装私有变量
function createUser() {
let name = 'Tom'
return {
getName() {
return name
},
setName(newName) {
name = newName
}
}
}- 函数工厂
function add(x) {
return function (y) {
return x + y
}
}
const add5 = add(5)
console.log(add5(3)) // 8- 事件处理里保存上下文
- 防抖、节流
- 柯里化 后面手写题会频繁遇到。
15. 闭包的风险
闭包本身不是坏事,问题在于:无意义地长期持有不再需要的大对象或大作用域 例如:
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 句话
作用域决定变量在哪里可以被访问。JavaScript 采用词法作用域,作用域在函数定义时基本确定。执行上下文是代码执行时的运行环境。调用栈用于管理函数执行顺序。作用域链是变量逐层向外查找的路径。闭包的本质是函数持有其定义时外层作用域的引用。闭包可以保存状态和封装变量,但不合理使用会增加内存占用。
20. 给你几个判断题
题 1
let a = 1
function fn() {
console.log(a)
}
function test() {
let a = 2
fn()
}
test()答案:1 因为 fn 的作用域在定义时就确定了。
题 2
if (true) {
var a = 1
let b = 2
}
console.log(a)
console.log(b)答案:
a输出1b报错
因为var没有块级作用域,let有。
题 3
function outer() {
let count = 0
return function () {
count++
return count
}
}
const fn = outer()
console.log(fn())
console.log(fn())答案:
12
因为闭包保存了 count。
21. 这一章你先自己回答这 6 题
你按自己的话答,我再帮你逐题纠偏:
- 什么是作用域?为什么需要作用域?
作用域是指变量、函数、参数的可访问范围。JavaScript 中常见有全局作用域、函数作用域和块级作用域。作用域的意义在于隔离变量、避免命名污染、控制访问范围,并支持封装和模块化开发。
- 什么是词法作用域?为什么说 JS 是静态作用域?
词法作用域指的是作用域关系在代码书写或函数定义时就基本确定,而不是在函数调用时动态决定。因此 JavaScript 是静态作用域语言,函数查找变量时,依据的是它定义时所在的词法环境,而不是调用它的位置。
- 什么是执行上下文?
执行上下文是代码执行时创建的运行环境。常见有全局执行上下文、函数执行上下文和 eval 执行上下文。函数执行时会创建自己的上下文,用来保存 this、参数、局部变量、函数声明,并建立与外层词法环境的关联。
- 什么是作用域链?
作用域链是变量查找的路径。当代码访问某个变量时,会先在当前作用域中查找,找不到就沿着外层词法环境逐级向上查找,直到全局作用域为止。由于 JavaScript 采用词法作用域,所以这条查找链在函数定义时就基本确定了。
- 什么是闭包?为什么 outer 执行完了,里面的变量还没销毁?
闭包本质上是函数和其定义时词法作用域的组合。即使函数在外部执行,它依然可以访问创建它时所在作用域中的变量。outer 执行完成后,如果返回的内部函数仍然引用 outer 中的变量,那么这些变量对内部函数来说仍然是可达的,因此不会被垃圾回收。
- var、let、const 的区别是什么?
var 是函数作用域,没有块级作用域,存在变量提升,并且允许重复声明。let 和 const 是块级作用域,也会提升,但在声明前处于暂时性死区,不能访问,且都不允许重复声明。const 与 let 的区别是 const 声明时必须赋值,且不能修改变量的绑定关系,但如果值是对象,仍然可以修改对象内部属性。
