文章目录
接前两天的文章:
第一题 执行上下文(Execution Context)
先不要急着看答案。
var a = 1
function fn() {
var b = 2
function inner() {
var c = 3
console.log(a)
console.log(b)
console.log(c)
}
inner()
}
fn()
问题1
代码执行过程中,一共创建了几个执行上下文?
A:1个
B:2个
C:3个
D:4个
问题2
执行顺序是什么?
按照进入和退出的顺序写出来。
问题3(思考题)
执行 console.log(a) 时: JS 引擎是如何找到 a 的?
为什么 inner() 里面能直接访问:
a
b
c
而没有报:
a is not defined
第二题:作用域链
还是先思考。
var name = 'global'
function a() {
var name = 'a'
function b() {
var name = 'b'
console.log(name)
}
b()
}
a()
输出什么?为什么?
再改一下:
var name = 'global'
function a() {
var name = 'a'
function b() {
console.log(name)
}
b()
}
a()
输出什么?为什么?
再改:
var name = 'global'
function a() {
function b() {
console.log(name)
}
b()
}
a()
输出什么?为什么?
菜鸟回答 —— 感觉和上一天重复,但是其实有部分新知识
这里菜鸟感觉不是和第二天一样吗?简简单单,没有一点难度,所以很快就完成了!
但是写的过程中发现,我好像完全不清楚执行上下文怎么算个数。
所以回答如下:
第一题:
问题1:不知道,我不清楚执行上下文怎么算个数
问题2:
解析阶段
GO: {
a: undefined,
fn: function fn() {}
}
执行阶段
a = 1
GO: {
a: 1,
fn: function fn() {}
}
解析阶段
AO(fn): {
b:undefined,
inner:function inner() {}
}
执行阶段:
b = 2
AO(fn): {
b:2,
inner:function inner() {}
}
解析阶段
AO(inner): {
c:undefined,
}
执行阶段
c = 3
AO(inner): {
c:3
}
问题3:
AO(inner) --> AO(fn)-->GO 找到的a
第二题:
输出:b
因为:AO(b) --> 可以找到name --> 直接输出
后面也是一样的道理,找不到就往外找
输出:a
输出:global
GPT 批改
很好,这次已经能看出来你昨天的变量提升知识点已经基本掌握了。我给你逐个点评。
第一题
问题1:执行上下文有几个?
代码:
var a = 1
function fn() {
var b = 2
function inner() {
var c = 3
console.log(a)
console.log(b)
console.log(c)
}
inner()
}
fn()
你说:
不知道,我不清楚执行上下文怎么算个数
这是今天最大的知识点。
判断规则(记住这一条)
规则1
全局代码 -> 创建一个全局执行上下文
Global Execution Context
规则2
每调用一次函数,就创建一个新的执行上下文。
注意:
function a() {}
这里只有定义:
全局执行上下文
而:
function a() {}
a()
就是:
全局执行上下文
a执行上下文
回到题目
第一步
进入文件:
var a = 1
function fn() {}
fn()
创建:
Global EC
第二步
执行:
fn()
创建:
fn EC
第三步
执行:
inner()
创建:
inner EC
所以一共:
Global EC
fn EC
inner EC
答案:3个执行上下文。
问题2
你写的是:
解析阶段
GO
执行阶段
a = 1
AO(fn)
执行阶段
b = 2
AO(inner)
执行阶段
c = 3
这个思路已经对了 80% 。
但是今天需要在脑子里再增加一个东西:
调用栈(Call Stack)
真正执行过程:
① 创建 Global EC
Call Stack:
┌──────────┐
│ GlobalEC │
└──────────┘
执行:
fn()
压栈:
┌──────────┐
│ fnEC │
├──────────┤
│ GlobalEC │
└──────────┘
执行:
inner()
再次压栈:
┌──────────┐
│ innerEC │
├──────────┤
│ fnEC │
├──────────┤
│ GlobalEC │
└──────────┘
执行:
console.log(c)
输出:3
inner 执行完:
┌──────────┐
│ fnEC │
├──────────┤
│ GlobalEC │
└──────────┘
fn 执行完:
┌──────────┐
│ GlobalEC │
└──────────┘
全局执行完:
空
你现在脑子里应该有这个模型
执行上下文 = 工作空间
调用栈 = 工作空间叠起来的栈
问题3
你回答:
AO(inner)
→ AO(fn)
→ GO
完全正确。
但是我想再往深一层。
执行:
console.log(a)
查找过程:
AO(inner)
{
c: 3
}
有没有:
a
没有,就向外找:
AO(fn)
{
b:2,
inner: fn
}
有没有:
a
没有,继续向外:
GO
{
a:1,
fn: fn
}
有:
a = 1
输出:1
再看:
console.log(b)
AO(inner) —— 没有
↓
AO(fn)
↓
找到 b = 2
输出:2
再看:
console.log(c)
AO(inner)
↓
直接找到 c = 3
输出:3
第二题
第一段
var name = 'global'
function a() {
var name = 'a'
function b() {
var name = 'b'
console.log(name)
}
b()
}
a()
你答:b
✅ 完全正确。
查找:
AO(b)
{
name:'b'
}
当前作用域直接找到。
输出:b
第二段
function a() {
var name = 'a'
function b() {
console.log(name)
}
b()
}
你答:a
✅ 完全正确。
查找:
AO(b) —— 没有
↓
AO(a)
{
name:'a'
}
↓
找到,输出:a
第三段
function a() {
function b() {
console.log(name)
}
b()
}
你答:global
✅ 完全正确。
查找:
AO(b) —— 没有
↓
AO(a) —— 没有
↓
GO
{
name:'global'
}
↓
找到,输出:global
今天最重要的一句话
你现在不要再把它记成:
AO(inner)
→ AO(fn)
→ GO
而应该升级成:
作用域链:
inner.[[Scope]]
↓
AO(fn)
↓
GO
因为后面学闭包时:
function outer() {
let count = 0
return function () {
count++
console.log(count)
}
}
之所以:
1
2
3
4
能够一直打印下去,就是因为:
返回的函数
↓
[[Scope]]
↓
AO(outer)
↓
count
虽然 outer() 已经执行完,但这个作用域还被引用着,没有被销毁。
第二题 今日验收
var a = 1
function fn() {
var a = 2
function inner() {
console.log(a)
}
return inner
}
const f = fn()
f()
回答三个问题:
- 一共创建了几个执行上下文?
- 输出什么?
- inner 为什么还能访问到 a?
菜鸟回答 —— 知道新知识后,感觉不难,沾沾自喜
1、
Global EC
fn EC
inner EC
2、
输出:2
3、
解析阶段
GO: {
a: undefined,
fn: function fn() {}
f: undefined
}
执行阶段
a = 1
GO: {
a: 1,
fn: function fn() {}
f: undefined
}
f = fn()
GO: {
a: 1,
fn: function fn() {}
f: function inner() {}
}
因为inner里面访问了a,而a在AO(inner)中找不到,会去找AO(fn),所以不能销毁AO(fn),也就能继续访问a了
GPT 批改
非常好,这次我给你打 90 分。
因为你已经自己推导出了闭包最核心的本质:
因为
inner还要访问AO(fn)里的变量,所以AO(fn)不能被销毁。
很多工作 3~5 年的前端,其实只能背:
闭包就是函数引用外部变量。
但说不出来:
为什么外部变量不会被销毁?
而你已经开始能解释原因了。
但是有两个细节要纠正
纠正1:GO 里面没有 f
你写:
GO: {
a: undefined,
fn: function fn() {},
f: undefined
}
这个不对。
因为:
const f = fn()
里面:
const f
不是:
var f
var
会挂到:
GO
例如:
var a = 1
解析:
GO = {
a: undefined
}
let/const
不会成为 GO 的属性。
例如:
const f = 123
let b = 456
更准确的理解是:
Global Lexical Environment
├── a
├── fn
├── f
└── b
所以:
window.f
结果:
undefined
这个知识先有个印象即可。
后面讲:
- 词法环境(Lexical Environment)
- TDZ(暂时性死区)
- let/const 底层实现
会再展开。
纠正2:不是「找不到就不能销毁」
你写:
因为 inner 里面访问了 a,而 a 在 AO(inner)中找不到,会去找 AO(fn),所以不能销毁 AO(fn)
这个描述已经很接近了。
但更准确的说法应该是:
inner 的 [[Scope]] 保存了对 fn 词法环境的引用,所以 fn 的词法环境不能被垃圾回收。
JS 引擎真正做的事情
执行:
function fn() {
var a = 2
function inner() {
console.log(a)
}
return inner
}
创建:
GlobalEC
调用:
fn()
创建:
fnEC
AO(fn) {
a: 2
inner: function inner(){}
}
此时:
inner.[[Scope]]
↓
AO(fn)
注意:
这里已经产生引用关系了。
然后:
return inner
执行:
const f = fn()
实际上:
f === inner
所以:
f
↓
inner函数对象
↓
[[Scope]]
↓
AO(fn)
形成:
f
↓
inner
↓
[[Scope]]
↓
AO(fn)
这时候:
fn()
虽然执行结束。
调用栈:
GlobalEC
fnEC 出栈:
GlobalEC
但是:
fnEC 出栈 ≠ AO(fn) 被销毁
因为:
f
↓
inner
↓
[[Scope]]
↓
AO(fn)
还有人引用它,所以:
AO(fn)
继续活着。
再执行
f()
创建:
innerEC
执行:
console.log(a)
查找:
AO(inner)
↓
[[Scope]]
↓
AO(fn)
找到:
a = 2
输出:2
你现在应该建立这个模型
调用栈(Call Stack)
决定:
谁正在执行
作用域链(Scope Chain)
决定:
变量去哪里找
闭包(Closure)
决定:
作用域是否继续活着
三者是串起来的。
最后给你一道真正的闭包题(Day3 加练)
function outer() {
let count = 0
return function () {
count++
console.log(count)
}
}
const fn1 = outer()
const fn2 = outer()
fn1()
fn1()
fn2()
fn1()
fn2()
回答:
- 输出什么?
- 一共创建了几个执行上下文?
- 内存里面会存在几个:
AO(outer)
为什么?
菜鸟回答 —— 直接全军覆没
输出:
1
2
3
4
5
创建了3个:
Global EC、outer EC、function EC
内存有1个AO(outer),因为有两个变量赋值都是AO(function),并不是AO(outer),但是AO(outer)因为被AO(function)引用了变量,所以不会注销,所以一直存在一个
GPT 批改
问题1:输出
你答:
1
2
3
4
5
❌ 错了。
代码:
function outer() {
let count = 0
return function () {
count++
console.log(count)
}
}
const fn1 = outer()
const fn2 = outer()
fn1()
fn1()
fn2()
fn1()
fn2()
第一步
执行:
const fn1 = outer()
创建:
AO(outer#1)
{
count: 0
}
返回:
fn1
↓
匿名函数
↓
[[Scope]]
↓
AO(outer#1)
第二步
执行:
const fn2 = outer()
创建:
AO(outer#2)
{
count: 0
}
返回:
fn2
↓
匿名函数
↓
[[Scope]]
↓
AO(outer#2)
注意:
AO(outer#1)
和
AO(outer#2)
是两个完全独立的作用域。
执行 fn1()
count: 0 → 1
输出:1
执行 fn1()
count: 1 → 2
输出:2
执行 fn2()
这是另一份:
count: 0 → 1
输出:1
执行 fn1()
还是第一份:
count: 2 → 3
输出:3
执行 fn2()
第二份:
count: 1 → 2
输出:2
正确答案
1
2
1
3
2
问题2:创建几个执行上下文?
你答:
Global EC
outer EC
function EC
❌ 不对。
Global EC
1个
outer()
调用了几次?
const fn1 = outer()
const fn2 = outer()
两次。
所以:
outer EC #1
outer EC #2
共:
2个
返回的匿名函数执行了几次?
fn1()
fn1()
fn2()
fn1()
fn2()
一共:
5次
每调用一次函数:
创建一个新的执行上下文
所以:
匿名函数 EC × 5
总数
Global EC × 1
outer EC × 2
匿名函数 EC × 5
=================
8个执行上下文
问题3:几个 AO(outer)?
你答:
1个
❌ 这是今天最关键的坑。
你说:
因为两个变量赋值都是 AO(function)
这里有个概念混淆。
fn1
执行:
const fn1 = outer()
创建:
AO(outer#1)
{
count: 0
}
返回:
fn1
↓
匿名函数
↓
[[Scope]]
↓
AO(outer#1)
fn2
执行:
const fn2 = outer()
又创建:
AO(outer#2)
{
count: 0
}
返回:
fn2
↓
匿名函数
↓
[[Scope]]
↓
AO(outer#2)
所以内存其实是:
fn1
↓
匿名函数
↓
AO(outer#1)
{
count: 0
}
fn2
↓
匿名函数
↓
AO(outer#2)
{
count: 0
}
最终
存在:
AO(outer#1)
AO(outer#2)
一共:
2个
这就是为什么很多面试题会这样写
function createCounter() {
let count = 0
return () => ++count
}
const c1 = createCounter()
const c2 = createCounter()
很多人以为:
count只有一个
其实:
c1
↓
AO(createCounter#1)
{
count: 0
}
c2
↓
AO(createCounter#2)
{
count: 0
}
是两份独立状态。
看到这里,你应该突然能理解 Vue3 了
function useCounter() {
const count = ref(0)
const inc = () => {
count.value++
}
return {
count,
inc
}
}
第一次:
const a = useCounter()
产生:
AO(useCounter#1)
{
count: Ref(0)
}
第二次:
const b = useCounter()
产生:
AO(useCounter#2)
{
count: Ref(0)
}
所以:
a.inc()
不会影响:
b.count.value
因为:每次调用 Composition Function,本质上都是创建了一套新的闭包环境。
加练题目
菜鸟感觉这样完全没有理解,所以要了几个加练的题目,这里就不写答案了,各位自己做吧!(主要是没有时间了)
欢迎评论区留下你的结果!
第1题
function fn() {
let a = 1
return function () {
console.log(a)
}
}
const f = fn()
f()
f()
问题
- 输出什么?
- 一共创建几个执行上下文?
- 内存里面有几个 AO(fn)?
第2题
function fn() {
let a = 1
return function () {
a++
console.log(a)
}
}
const f = fn()
f()
f()
f()
问题
- 输出什么?
- 有几个 AO(fn)?
- 为什么不是:
2
2
2
第3题
function fn() {
let a = 1
return function () {
a++
console.log(a)
}
}
const f1 = fn()
const f2 = fn()
f1()
f1()
f2()
f1()
问题
- 输出什么?
- 有几个 AO(fn)?
f1和f2的a是不是同一个?
第4题(提高)
function fn() {
let a = 1
return {
add() {
a++
},
get() {
console.log(a)
}
}
}
const obj = fn()
obj.get()
obj.add()
obj.get()
obj.add()
obj.get()
问题
- 输出什么?
add和get为什么都能访问a?- 内存里面几个 AO(fn)?
第5题(Vue源码味道)
function useCounter() {
let count = 0
const inc = () => count++
const get = () => count
return {
inc,
get
}
}
const c1 = useCounter()
const c2 = useCounter()
c1.inc()
c1.inc()
console.log(c1.get())
console.log(c2.get())
问题
- 输出什么?
- 有几个 AO(useCounter)?
- 为什么
c1不影响c2?
2026/7/1 补充
昨天菜鸟没有空继续写了,才留给读者!
今天抽空就自己先回答了,并且大部分都是正确的
菜鸟回答
第一题:
1、输出:1 1
2、4个执行上下文
Global EC、fn EC、匿名函数 EC X 2
3、1个 AO(fn)
第二题:
1、输出: 2 3 4
2、一个AO(fn)
3、因为就创建了1个fn EC,但是匿名函数 EC有3个
第三题:
1、输出:2 3 2 4
2、两个 AO(fn)
3、不是同一个,因为就创建了2个fn EC
第四题:
1、输出:1 2 3
2、
解析阶段
GO:{
fn:function fn() {}
obj:undefined
}
执行阶段
const obj = fn()
GO:{
fn:function fn() {}
obj:{
add() {
a++
},
get() {
console.log(a)
}
}
}
obj中没有a,向上找到AO(fn)中的a,所以都能访问a
3、1个AO(fn)
第5题:
1、输出:2 0
2、2个AO(useCounter)
3、因为是两个不同的调用链
AO(useCounter#1)
|
count
|
[[Scope]]
|
两个匿名函数
AO(useCounter#2)
|
count
|
[[Scope]]
|
两个匿名函数
GPT 批改
第一题
function fn() {
let a = 1
return function () {
console.log(a)
}
}
const f = fn()
f()
f()
输出
1
1
✅ 正确。
执行上下文
你答:
Global EC
fn EC
匿名函数 EC × 2
共:
4个
✅ 完全正确。
AO(fn)
有几个?
1个
因为:
const f = fn()
只调用了一次:
fn()
所以:
AO(fn#1)
{
a:1
}
匿名函数执行两次:
f()
f()
都是:
AO(匿名函数)
↓
[[Scope]]
↓
AO(fn#1)
共享同一个:
AO(fn#1)
第二题
const f = fn()
f()
f()
f()
输出:
2
3
4
✅ 正确。
AO(fn)
你答:
1个
✅ 正确。
第三个问题
执行:
f()
f()
f()
匿名函数执行了:
3次
所以:
匿名函数 EC × 3
你的核心理解已经对了:
fn EC ×1
AO(fn) ×1
所以:
a
一直在:
1 → 2 → 3 → 4
不会重置。
第三题
const f1 = fn()
const f2 = fn()
输出
2
3
2
4
✅ 完全正确。
AO(fn)
2个
✅ 正确。
原因
你答:
因为创建了2个 fn EC
✅ 对。
但是我希望你升级成:
fn()
↓
AO(fn#1)
{
a:1
}
fn()
↓
AO(fn#2)
{
a:1
}
所以:
f1
↓
AO(fn#1)
f2
↓
AO(fn#2)
彼此独立。
第四题
const obj = fn()
输出
1
2
3
✅ 正确。
为什么 add 和 get 都能访问 a?
你的解释:
obj中没有a
向上找到AO(fn)
✅ 核心正确。
但是有一个概念需要纠正。
错误理解
obj
↓
AO(fn)
实际上不存在。
真正是:
obj
{
add: function,
get: function
}
而:
add
↓
[[Scope]]
↓
AO(fn)
get
↓
[[Scope]]
↓
AO(fn)
注意:是函数闭包了 AO(fn)
不是:obj 闭包了 AO(fn)
因为:
const x = obj.add
x()
一样能访问:
a
说明:不是 obj 在保存状态。
而是:
add函数对象
自己保存了:
[[Scope]]
AO(fn)
1个
✅ 正确。
第五题
这是今天最重要的一题。
const c1 = useCounter()
const c2 = useCounter()
输出
2
0
✅ 正确。
AO(useCounter)
2个
✅ 正确。
为什么 c1 不影响 c2?
你的回答:
AO(useCounter#1)
|
count
|
[[Scope]]
|
两个匿名函数
AO(useCounter#2)
|
count
|
[[Scope]]
|
两个匿名函数
✅ 思路已经非常接近源码级理解。
我帮你调整一下图:
c1
├── inc
└── get
inc.[[Scope]]
↓
AO(useCounter#1)
{
count: 2
}
get.[[Scope]]
↓
AO(useCounter#1)
{
count: 2
}
c2
├── inc
└── get
inc.[[Scope]]
↓
AO(useCounter#2)
{
count: 0
}
get.[[Scope]]
↓
AO(useCounter#2)
{
count: 0
}

4288

被折叠的 条评论
为什么被折叠?



