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