继续
接前两天的文章:
回顾
昨天你已经理解了:
- 函数提升优先于
var变量提升 GO/AO(全局对象/执行上下文对象)- 创建阶段和执行阶段
今天继续推进两个核心知识点:
- 执行上下文(Execution Context)完整流程
- 作用域链(Scope Chain)到底是怎么找变量的
今日训练
第一题:执行上下文到底经历了什么?
看代码:
js
var a = 1
function fn() {
var b = 2
console.log(a)
console.log(b)
}
fn()
请不要运行,按照下面顺序回答:
第一步:进入全局执行上下文(GO)
GO 里面有什么?
写出:
js
GO = {
}
第二步:执行到 fn() 时
创建函数执行上下文(AO)。
写出:
js
AO = {
}
第三步:执行:
js
console.log(a)
JS 引擎是:
- 去 AO 找?
- 去 GO 找?
- 还是直接全局找?
请写出完整查找过程。
第二题:作用域链
再看:
js
var a = 1
function fn() {
var b = 2
function test() {
var c = 3
console.log(a)
console.log(b)
console.log(c)
}
test()
}
fn()
不要运行,请回答:
问题1
执行:
js
console.log(c)
变量在哪里找到?
问题2
执行:
js
console.log(b)
查找路径是什么?
例如:
js
AO(test)
↓
AO(fn)
↓
GO
问题3
执行:
js
console.log(a)
查找路径是什么?
小任务(非常重要)
从现在开始,每看一个函数,都先在脑子里画:
js
GO
↓
AO(fn)
↓
AO(test)
以后学:
- 闭包
- this
- call/apply/bind
- Vue 响应式源码
- Pinia
- React Hooks
全部建立在这套执行上下文和作用域链模型之上。
菜鸟回答 ------ 这里菜鸟还是没有掌握满分回答的精髓
js
第一题:
GO:{
a:1,
fn:function fn() {}
}
AO:{
a:1,
b:2
}
a 会现在 AO 找,发现找不到所以去 GO 找到了a:1,所以console.log(a) 会输出1
第二题:
console.log(c) 直接在test的AO中找,并找到了,输出3
console.log(b) 先在test的AO中找,找不到,就去fn的AO找,找到了,输出2
console.log(a) 先在test的AO中找,找不到,就去fn的AO找,也找不到,去GO找,找到了,输出1
GPT 批改
第一题批改
代码:
js
var a = 1
function fn() {
var b = 2
console.log(a)
console.log(b)
}
fn()
GO
你写的是:
js
GO = {
a: 1,
fn: function fn() {}
}
✅ 最终执行完成后是对的。
但如果按照执行上下文的完整流程:
创建阶段
js
GO = {
a: undefined,
fn: function fn() {}
}
执行阶段
执行:
js
a = 1
变成:
js
GO = {
a: 1,
fn: function fn() {}
}
AO
你写的是:
js
AO = {
a: 1,
b: 2
}
这里错了一点。
创建阶段
进入 fn:
js
AO = {
b: undefined
}
因为:
js
function fn() {
var b = 2
}
函数里面只有:
js
var b
并没有:
js
var a
所以 a 不会出现在 AO 中。
执行阶段
执行:
js
b = 2
变成:
js
AO = {
b: 2
}
查找 a
你写的:
AO 找不到 → GO 找到
完全正确。
实际上 JS 引擎干的事情类似:
js
AO(fn)
↓ 没有 a
GO
↓ 找到 a = 1
返回 1
第二题批改
代码:
js
var a = 1
function fn() {
var b = 2
function test() {
var c = 3
console.log(a)
console.log(b)
console.log(c)
}
test()
}
fn()
console.log(c)
你:
直接在 test 的 AO 找到,输出 3
✅ 正确。
js
AO(test)
↓
找到 c = 3
console.log(b)
你:
test 的 AO 没有 → fn 的 AO 找到
✅ 正确。
js
AO(test)
↓
AO(fn)
↓
找到 b = 2
console.log(a)
你:
test 的 AO 没有 → fn 的 AO 没有 → GO 找到
✅ 完全正确。
js
AO(test)
↓
AO(fn)
↓
GO
↓
找到 a = 1
今日加练(闭包预热)
不要运行:
js
var a = 1
function fn() {
var b = 2
function test() {
console.log(a)
console.log(b)
}
return test
}
var f = fn()
f()
问题1
执行完:
js
var f = fn()
以后:
js
fn 的 AO
会不会销毁?
问题2
执行:
js
f()
时:
js
console.log(b)
是怎么找到 b 的?
菜鸟回答 ------ 这里已经大致知道GPT想要什么答案了
js
问题1:不会
问题2:
解析阶段:
GO: {
a:undefined,
f:function fn() {}
}
执行:
GO: {
a:1,
f:function fn() {}
}
解析阶段:
AO(fn) {
b:undefined,
test:function test() {}
}
执行阶段:
AO(fn) {
b:2,
test:function test() {}
}
解析阶段:
AO(test) {}
执行阶段:
b找不到,去AO(fn)中找,找到了,所以是2
a找不到,去AO(fn)中找,还是找不到,去GO中找,所以是1
GPT 解析
问题1:fn 的 AO 会不会销毁?
你回答:
不会
✅ 正确。
但是原因你还没有说出来。
因为:
js
var f = fn()
实际上:
js
f === test
也就是:
js
var f = test
而 test 函数里面:
js
console.log(b)
需要访问:
js
AO(fn)
JS 引擎发现:
虽然 fn 执行结束了,但是还有一个函数(test)在引用它里面的变量。
所以:
js
AO(fn)
↑
test 引用了它
因此:
js
AO(fn) 不会被垃圾回收(GC)
这就是闭包产生的根本原因。
问题2:你的 GO 写错了
代码:
js
var a = 1
function fn() {
var b = 2
function test() {
console.log(a)
console.log(b)
}
return test
}
var f = fn()
f()
你写:
js
GO = {
a: undefined,
f: function fn() {}
}
这里错了。
创建阶段
js
GO = {
a: undefined,
fn: function fn() {},
f: undefined
}
因为:
js
var a
function fn(){}
var f
都会提升。
执行阶段
执行:
js
a = 1
js
GO = {
a: 1,
fn: function fn() {},
f: undefined
}
执行:
js
f = fn()
此时:
js
fn()
返回:
js
test
所以:
js
GO = {
a: 1,
fn: function fn() {},
f: function test() {}
}
AO(fn)
创建阶段:
js
AO(fn) = {
b: undefined,
test: function test() {}
}
执行阶段:
js
AO(fn) = {
b: 2,
test: function test() {}
}
AO(test)
创建阶段:
js
AO(test) = {}
执行阶段:
js
AO(test) = {}
因为:
js
function test() {
console.log(a)
console.log(b)
}
里面没有:
- var
- function
- 参数
所以 AO 为空。
查找 b
执行:
js
console.log(b)
查找过程:
js
AO(test)
↓ 没有 b
AO(fn)
↓ 找到 b = 2
返回 2
查找 a
执行:
js
console.log(a)
查找过程:
js
AO(test)
↓ 没有 a
AO(fn)
↓ 没有 a
GO
↓ 找到 a = 1
返回 1
但是最关键的是这一句
很多人会以为:
js
f()
执行时:
js
AO(test)
↓
GO
因为:
js
f()
是在全局调用的,这是错误的!
真正的作用域链
作用域链由:
函数定义的位置决定(词法作用域)
不是:
函数调用的位置决定
所以:
js
function fn() {
var b = 2
function test() {
console.log(b)
}
return test
}
test 出生的时候:
js
GO
↓
AO(fn)
↓
AO(test)
这个链条已经固定了。
即使:
js
var f = fn()
f()
甚至:
js
setTimeout(f)
obj.xxx = f
window.abc = f
调用位置变了无数次。
但是:
js
AO(test)
↓
AO(fn)
↓
GO
永远不会变。
验收题(非常重要)
不要运行:
js
var a = 100
function fn() {
var a = 200
function test() {
console.log(a)
}
return test
}
var f = fn()
a = 300
f()
请你推导:
- 输出多少?
- 查找路径是什么?
菜鸟回答 ------ 接近满分(所以就不给GPT批改了)
js
解析阶段:
GO: {
a: undefined,
fn: function fn() {},
f: undefined
}
执行阶段:
a = 100
GO: {
a: 100,
fn: function fn() {},
f: undefined
}
f = fn()
GO: {
a: 100,
fn: function fn() {},
f: function test() {}
}
a = 300
GO: {
a: 300,
fn: function fn() {},
f: function test() {}
}
解析阶段
AO(fn): {
a:undefined,
test: function test() {}
}
执行阶段:
a = 200
AO(fn): {
a:200,
test: function test() {}
}
解析阶段:
AO(test): {}
执行阶段:
AO(test)找不到a,去AO(fn)找到a,所以是200