GPT训练我的第四天,被打惨了!!!😭😭😭

接前几天的文章:

  1. 直接让GPT每日训练我!!!😕😕😕
  2. GPT训练我的第二天,我表示不过如此!!!😕😕😕
  3. GPT训练我的第三天,明白了应该咋说满分回答!😕😕😕

第一题 执行上下文(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()

回答三个问题:

  1. 一共创建了几个执行上下文?
  2. 输出什么?
  3. 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()

回答:

  1. 输出什么?
  2. 一共创建了几个执行上下文?
  3. 内存里面会存在几个:
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()

问题

  1. 输出什么?
  2. 一共创建几个执行上下文?
  3. 内存里面有几个 AO(fn)?

第2题

js 复制代码
function fn() {
  let a = 1

  return function () {
    a++
    console.log(a)
  }
}

const f = fn()

f()
f()
f()

问题

  1. 输出什么?

  2. 有几个 AO(fn)?

  3. 为什么不是:

    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()

问题

  1. 输出什么?
  2. 有几个 AO(fn)?
  3. f1f2a 是不是同一个?

第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()

问题

  1. 输出什么?
  2. addget 为什么都能访问 a
  3. 内存里面几个 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())

问题

  1. 输出什么?
  2. 有几个 AO(useCounter)?
  3. 为什么 c1 不影响 c2
相关推荐
梨子同志1 小时前
CSS
前端
一tiao咸鱼1 小时前
Ai 相关 7月1日学习
前端·agent
DarkLONGLOVE1 小时前
快速上手 Pinia!Vue3 极简状态管理使用教程
javascript·vue.js
mackbob1 小时前
.eslintrc.js详细配置说明
javascript
梨子同志1 小时前
HTML
前端
ZhengEnCi1 小时前
Q06-导航按钮高级拟态玻璃效果构建完全指南
前端·css
Apifox2 小时前
Apifox 6 月更新|Apifox CLI 全面升级、导入导出优化、OAuth 2.0 支持自动刷新令牌
前端·后端·测试
CodingSpace2 小时前
TypeScript 装饰器
前端
宸翰2 小时前
解决 uni-app App 端 vue-i18n 占位符丢失:封装跨端可用的 tf 格式化方法
前端·vue.js·uni-app