前端基石:一段代码隐含了多少基础知识?

前言

今天在网上看见这样这样一段代码,如下:

function fn() {
    for (let i = 0; i < 4; i++) {
        var timer = setTimeout(function(i) {
            console.log(i);
            clearTimeout(timer);
        }, 10, i);
    }
}
fn();
复制代码

请问这段代码执行会输出什么?为什么?

结果是什么?

大家可以先在大脑里过一下这段代码,试着说出结果是什么?

2ae92f4ff557554341d9c0be60954c96.png
image.png

我们在浏览器运行一下这段代码,结果是0、1、2。你答对了吗。

d9cd89ef9a2fbe5c759473f7b90fcf71.png
image.png

为什么这段代码输出的是 0、1、2,而不是0、1、2、3。

函数执行

问题就出在 timer 前面这个 var 上。

3b310bcb2fe5208567e90497f03dc763.pngvar 定义的变量,是不会生成“块级作用域”的。按照之前文章的思路(\# 前端基石:函数的底层执行机制[2]),我们来一步一步执行这段代码。

  1. 函数创建

  • 作用域:[[scope]](在这里是window)

  • 函数字符串

  • 键值对

  1. 一个函数的创建会在 Heap 堆内存中开辟一块空间来存储函数。对象在创建会在堆内存中存储对象的键值对,而函数在堆内存中会存储三部分东西:

变量提升,fn 提升到最前面

代码执行(函数执行)

  • for 循环中的 let 不进行提升

  • for 循环内部的 var 进行变量提升,提升到 fn 的函数作用域内,这其实就是在循环体实际只是定义了一个 timer 变量,每一次迭代执行,都是对这个 timer 进行重新赋值。

  • 创建自己的私有上下文,函数一旦执行,就会创建一个全新的私有上下文「进栈」,函数每一次执行都是重新形成一个全新的私有上下文,和之前执行产生的上下文没有必然的联系,函数中的代码都是在私有上下文中进行执行的。函数进栈执行会创建一个私有变量对象 AO(Active Object),这里区分开 VO。AO 是 VO 的一个分支。在私用上下文中创建的变量都会存储在 AO 中,例如形参、变量提升和函数中定义的变量。

  1. 函数存储的字符串执行,将堆内存中存在的代码字符串从上往下的顺序进行执行。

  2. 出栈释放

  3. 函数进行初始化操作

  4. 初始化作用域链<<自己的私有上下文,作用域>>。

  5. 初始化 this。

  6. 初始化 arguments (没有)。

  7. 形参赋值。

  8. 变量提升。

  9. 函数执行

336efd1ed6470085d5e3afabac6dbd2c.png

函数代码字符串的执行

我们接下来重点看看 fn 函数内部的执行,下面我用一段伪代码演示一下你这个代码执行后发生了什么:

注意点:

  1. for 循环中的 let 不进行提升,形成块级作用域。

  2. for 循环内部的 var 进行变量提升,提升到 fn 的函数作用域内,这其实就是在循环体实际只是定义了一个 timer 变量,每一次迭代执行,都是对这个 timer 进行重新赋值。

  3. setTimeout 是异步,会放入宏任务队列。

  4. setTimeout 的第三个参数会作为 setTimeout 的回调函数的参数传入。

第一次迭代:

fn: {
  var timer = timer0
  for: {
    let i = 0
  }
}

task(宏任务): [ timer0 ]
复制代码

第二次迭代:

fn: {
  var timer = timer1
  for: {
    let i = 1
  }
}

task(宏任务): [ timer0, timer1 ]
复制代码

第三次迭代:

fn: {
  var timer = timer2
  for: {
    let i = 2
  }
}

task(宏任务): [ timer0, timer1, timer2 ]
复制代码

第四次迭代:

fn: {
  var timer = timer3
  for: {
    let i = 3
  }
}

task(宏任务): [ timer0, timer1, timer2, timer3 ]
复制代码

73c759fc4821ae2f99f990d27aa63224.png同步代码执行完成,timer 由于被提升到 fn 的函数作用域,每一次循环执行,都是重置赋值 timer,所以,timer 最终指向的 timer3。这样还没有结束,同步代码执行完成,开始执行异步队列代码。

异步队列执行

异步代码会按照 task 的顺序依次执行。 当执行 timer0 的时候会执行 clearTimeout(timer3) ,把 timer3 从 task 列表里去掉,最终只有 timer0、timer1、timer2 三个得到了执行,因此只会 console 出 0、1、2。

c0b52fc0c7a657e1ce2c9731c332077e.png

如何正常输入 0、1、2、3

那如果我们想要按照0、1、2、3 这样的输出方式进行输出,应该如何调整这段代码了。

问题的本质是出在 timer 前面这个 var 变量定义上。var 定义的变量,是不会生成“块级作用域”,变量被提升。同步代码执行完成之后,timer 最终指向了 timer3,导致在执行 task 的时候被清除。

要解决这个问题就不能让 timer 最后指向 timer3。

需要让每一次循环迭代,timer 都是一个独立的变量。

timer 如果想要是独立的变量,就需要每一次循环迭代都在一个独立的作用域中执行。并且不会进行变量提升

除函数和对象的大括号之外,其他大括号如果出现了let、const、function、class 等关键字声明变量,则当前大括号会产生一个块级上下文。它的上级上下文就是所处的环境。var 声明不会产生块级上下文,也不受块级上下文的影响。所以在当前场景,使用 let/const 声明 timer,形成块级作用域。

这里也很简单会议一下 let/const 和 var 的区别(\# var、let、const被你忽略的区别[3]):

  1. let/const 不存在变量提升,不允许在声明之前使用

  2. let/const 不允许重复声明

  3. let/const 不会污染全局

  4. let/const 会生成块级上下文

  5. let / const 会形成暂时性死区

  6. 形参重新声明(易被忽略)

function fn() {
    for (let i = 0; i < 4; i++) {
        // let、const 都可以
        let timer = setTimeout(function(i) {
            console.log(i);
            clearTimeout(timer);
        }, 10, i);
    }
}
fn();
复制代码
90063a1fbff22505de84b55081a6e94f.png
image.png

总结

看似简单的一段代码,隐藏了很多的知识点:

  • 「变量提升」

  • 「函数执行机制」

  • 「块级作用域」

  • 「异步任务队列」

  • let/const 和 var 的区别

等... 但这些知识点本身不难,只是可能隐藏比较深,让我们一不小心就掉坑了。所以简单的代码,越容易有坑。本文也通过一步一步的分析找到问题的原因,然后找出解决办法。

以上就是本文的全部内容,如果喜欢帮忙点个赞吧,谢谢。

关于本文:

来源:拜小白

https://juejin.cn/post/7088161682027085860

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值