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

简介: 本文主要讲代码中隐含的基础知识

网络异常,图片无法展示
|

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情


前言


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


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


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


结果是什么?


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


网络异常,图片无法展示
|


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


网络异常,图片无法展示
|


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


函数执行


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


网络异常,图片无法展示
|


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


  1. 函数创建
  1. 一个函数的创建会在 Heap 堆内存中开辟一块空间来存储函数。对象在创建会在堆内存中存储对象的键值对,而函数在堆内存中会存储三部分东西:
  • 作用域:[[scope]](在这里是window)
  • 函数字符串
  • 键值对
  1. 变量提升,fn 提升到最前面
  2. 代码执行(函数执行)
  1. 函数执行
  • 创建自己的私有上下文,函数一旦执行,就会创建一个全新的私有上下文「进栈」,函数每一次执行都是重新形成一个全新的私有上下文,和之前执行产生的上下文没有必然的联系,函数中的代码都是在私有上下文中进行执行的。函数进栈执行会创建一个私有变量对象 AO(Active Object),这里区分开 VO。AO 是 VO 的一个分支。在私用上下文中创建的变量都会存储在 AO 中,例如形参、变量提升和函数中定义的变量。
  1. 函数进行初始化操作
  1. 初始化作用域链<<自己的私有上下文,作用域>>。
  2. 初始化 this。
  3. 初始化 arguments (没有)。
  4. 形参赋值。
  5. 变量提升。
  • for 循环中的 let 不进行提升
  • for 循环内部的 var 进行变量提升,提升到 fn 的函数作用域内,这其实就是在循环体实际只是定义了一个 timer 变量,每一次迭代执行,都是对这个 timer 进行重新赋值。
  1. 函数存储的字符串执行,将堆内存中存在的代码字符串从上往下的顺序进行执行。
  2. 出栈释放

网络异常,图片无法展示
|


函数代码字符串的执行


我们接下来重点看看 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 ]

网络异常,图片无法展示
|

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


异步队列执行


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

网络异常,图片无法展示
|


如何正常输入 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被你忽略的区别):

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

网络异常,图片无法展示
|


总结



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


  • 「变量提升」
  • 「函数执行机制」
  • 「块级作用域」
  • 「异步任务队列」
  • let/const 和 var 的区别


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

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


参考


目录
相关文章
|
28天前
|
JavaScript 前端开发 Docker
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
在使用 Deno 构建项目时,生成的可执行文件体积较大,通常接近 100 MB,而 Node.js 构建的项目体积则要小得多。这是由于 Deno 包含了完整的 V8 引擎和运行时,使其能够在目标设备上独立运行,无需额外安装依赖。尽管体积较大,但 Deno 提供了更好的安全性和部署便利性。通过裁剪功能、使用压缩工具等方法,可以优化可执行文件的体积。
110 3
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
|
7天前
|
Web App开发 缓存 监控
前端性能优化实战:从代码到部署的全面策略
前端性能优化实战:从代码到部署的全面策略
13 1
|
10天前
|
前端开发 JavaScript
前端界的革命:掌握这些新技术,让你的代码简洁到让人惊叹!
前端技术的快速发展带来了许多令人惊叹的新特性。ES6及其后续版本引入了箭头函数、模板字符串等简洁语法,极大减少了代码冗余。React通过虚拟DOM和组件化思想,提高了代码的可维护性和效率。Webpack等构建工具通过模块化和代码分割,优化了应用性能和加载速度。这些新技术正引领前端开发的革命,使代码更加简洁、高效、可维护。
14 2
|
10天前
|
前端开发 JavaScript 测试技术
前端工程师的必修课:如何写出优雅、可维护的代码?
前端工程作为数字世界的门面,编写优雅、可维护的代码至关重要。本文从命名规范、模块化设计、注释与文档、遵循最佳实践四个方面,提供了提升代码质量的方法。通过清晰的命名、合理的模块划分、详细的注释和持续的学习,前端工程师可以写出高效且易于维护的代码,为项目的成功打下坚实基础。
23 2
|
15天前
|
监控 前端开发 JavaScript
前端开发的终极奥义:如何让你的代码既快又美,还不易出错?
【10月更文挑战第31天】前端开发是一个充满挑战与机遇的领域,本文从性能优化、代码美化和错误处理三个方面,探讨了如何提升代码的效率、可读性和健壮性。通过减少DOM操作、懒加载、使用Web Workers等方法提升性能;遵循命名规范、保持一致的缩进与空行、添加注释与文档,让代码更易读;通过输入验证、try-catch捕获异常、日志与监控,增强代码的健壮性。追求代码的“快、美、稳”,是每个前端开发者的目标。
31 3
|
17天前
|
前端开发 JavaScript 开发者
前端开发的终极技巧:如何让你的代码既简洁又高效,还能减少bug?
【10月更文挑战第30天】前端开发充满挑战与创新,如何编写简洁高效且少bug的代码是开发者关注的重点。本文介绍五大技巧:1. 模块化,提高代码复用性;2. 组件化,降低代码耦合度;3. 使用现代框架,提高开发效率;4. 统一代码规范,降低沟通成本;5. 利用工具,优化代码质量。掌握这些技巧,让前端开发更高效。
36 1
|
24天前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
30 1
|
1月前
|
存储 监控 前端开发
掌握微前端架构:构建未来前端应用的基石
【10月更文挑战第12天】随着前端技术的发展,传统的单体应用架构已无法满足现代应用的需求。微前端架构通过将大型应用拆分为独立的小模块,提供了更高的灵活性、可维护性和快速迭代能力。本文介绍了微前端架构的概念、核心优势及实施步骤,并探讨了其在复杂应用中的应用及实战技巧。
|
1月前
|
前端开发 JavaScript 开发者
利用代码分割优化前端性能:高级技巧与实践
【10月更文挑战第2天】在现代Web开发中,代码分割是优化前端性能的关键技术,可显著减少页面加载时间。本文详细探讨了代码分割的基本原理及其实现方法,包括自动与手动分割、预加载与预取、动态导入及按需加载CSS等高级技巧,旨在帮助开发者提升Web应用性能,改善用户体验。
|
1月前
|
前端开发 小程序 JavaScript
信前端里的循环显示如何编写代码?
信前端里的循环显示如何编写代码?
71 5
下一篇
无影云桌面