• 从一道看似简单的面试题重新理解JS执行机制与定时器


     壹 ❀ 引

    最近在看前端进阶的系列专栏,碰巧看到了几篇关于JS事件执行机制的面试文章,因为我在之前一篇 JS执行机制详解,定时器时间间隔的真正含义 博文中也有记录JS执行机制,所以正好用于作为测试自己的理解情况,那么本文顺着题目来重新理一理思路,说说我对于题目的理解,扩充知识点。

    本文站在你对于JS执行机制与定时器已经有所了解的前提下展开,若非如此,建议先了解相关概念会更好,那么本文开始。

     贰 ❀ 一道变化的面试题

     题目一:

    说说以上代码输出什么?

    没错,这只是一个非常简单的for循环,依次输出0 - 4;我想大家对于for循环一定都非常熟悉,这里通过步骤拆解简单展示下for循环的执行步骤:

    变量 i 从头到尾就只声明了一次,然后开始第一次条件判断,满足条件执行代码体,之后 i 自增,继续条件判断,如果条件不满足则跳出循环。

    好了,题目升级,我们将for循环内部改成一个定时器,现在会输出什么呢?

     题目二:

    我想稍微有看过类似笔试题的同学,应该都知道,大约在等待一秒钟后,同时输出五个5。原因是定时器是异步任务,for循环的每次循环虽然都会创建一个定时器,但并没有同步执行,而是等到for循环执行完毕后,统一执行了五个定时器,而此时变量 i 早已自增为5。

    我们用步骤拆分模拟执行,如下:

    由于定时器是异步任务,我们可以理解为最后执行,所以真正的样子应该是这样:

    此时 i 已经自增为5。那么有同学又要问了,为什么是等待一秒后同时输出五个5,而不是每隔一秒输出一个5呢?这就得明白定时器时间真正表示的含义,我们看下面这个例子:

    请问上述代码是每隔三秒输出一个1呢,还是等待三秒后同时输出两个1呢?

    答案是后者,如果你觉得答案是前者,那是因为你误会了定时器的时间含义,3000ms并不是定时器执行前的等待时间,而是将定时器中的回调函数加入任务队列前的等待时间。

    我们这个世界只有一条时间线,也没发现平时世界,程序也是如此,3秒倒计时后,两个定时器的回调函数接近同时被加入到了任务队列,因为回调执行耗时可以忽略不计,所以就像同时输出了。

    我们通过一个例子来验证这一点,如下:

    请问谁先执行?时间间隔又是怎么样?

    答案是大约等待一秒后先输出2,再等待大约2秒输出1;原因是1秒过后,第二个定时器回调先加入任务队列,再过2秒将第一个定时器回调加入任务队列,然后开始执行。而任务队列具有FIFO(先进先出)的特性,我们忽略回调执行耗时,也就是1S=>1=>2S=>2这个结果了。

    若你对于JS执行机制或者定时器执行这块有疑虑,可以阅读博主关于JS执行机制的博客,一定会对你有所帮助:JS执行机制详解,定时器时间间隔的真正含义

    好了,第二题拓展说了稍微有点多,我们将题目二再变形,如下,请说说题目三输出结果是什么,时间间隔是多少?

     题目三:

    结合前面对于for循环的拆分,以及对定时器时间含义重新了解,答案是几乎无等待的先输出一个5,之后每隔一秒再输出四个5。

    有同学肯定又要问了,前面你不是说定时器运行时变量 i 已经是5了吗,照理说不是应该等待五秒之后同时输出五个5吗?如果你是这么认为的,那是因为你没理解定时器的执行规则。

    准确来说,定时器异步执行的只是定时器中的回调函数,定时器的时间可不会异步,这里只是将固定的时间换成了一个简单乘法计算而已,所以时间计算在运行到定时器时就已经同步计算完毕了,我们改写代码应该是这样:

    OK,我们对于定时器的理解又更进了一步,那么问题来了,请以题目三为原型做出修改,让for循环先输出0之后每隔一秒依次输出1,2,3,4。做法其实有多种,先自己想想再看答案:

     1.我们可以利用闭包:

    上述代码中我们利用一个自执行函数包裹了定时器,而定时器中的回调函数引用了外层自调函数的形参 i ,所以此时定时器的回调函数是一个闭包。

    有趣的事情来了,当创建每个自执行函数时变量 i 都会作为参数立刻传入到自执行函数体内,此时 i 已经和函数作用域绑定到了一起,而定时器回调函数引用了外层函数的 i ,这样就达到我们想要的目的了,我们改写代码:

    没错,定时器是异步,它应该最后执行,但JS采用的是静态作用域,函数在定义时,它的作用域就已经被确定了,不管它在何处被调用,它能访问的外层作用域就是被创建时所在的作用域,我们再次改写:

    虽然定时器的回调看着是最后执行,但它在执行时由于自己没有 i ,所以只能从父级作用域找,而它的父级就是创建它的自执行函数。

     2.我们可以利用按值传递特性

    我们知道JS中基本类型的数值作为函数参数时都是按值传递的,什么意思呢,通过一个例子来解释:

    上述代码中,我声明了一个基本类型的变量 x 与一个引用类型的数组 y,作为参数传入到了函数中,分别对x y进行了修改,函数执行完毕之后,x y会发生变化吗?

    直觉告诉我们,x不会变化,而 y 被修改了,这有点类似于深浅拷贝,当基本类型的数据作为函数参数时总是按值传递,就像额外拷贝了一份进去,而引用类型的数据传递的其实是一个引用地址,任何操作都会修改原有的数据。

    懂了这个就好办了,我们直接声明一个函数,在for循环中将变量 i 作为调用函数的参数就好了,像这样:

     

    是不是有点把闭包自执行函数移到外面的感觉,原理类似,按值传递居然这么好用。

     3.我们可以利用ES6的let

    当for循环中使用let去声明变量 i 时,利用块级作用域的特性,让每次循环的 i 成为独立的一份,直接上代码:

    这里我不太好详细解释为何let可以到达目的,如果你对for循环中使用var 和 let声明变量 i 究竟有何不同,以及为何每次循环 i 都是独立的一份有兴趣,欢迎阅读博主 for循环中let与var的区别,块级作用域如何产生与迭代中变量i如何记忆上一步的猜想 这篇文章,顺着我的思路,一定给你整的明明白白。

    其实当我们使用let 声明变量 i 时,此时for循环用递归来模拟应该是这样,如果你看不懂以下改写,还是建议阅读我上面推荐的文章。

     4.我们可以使用定时器第三参数

    不知道有多少人知道定时器其实还有第三参数,如果我们想给定时器回调函数传递参数,就可以借助第三参数,直接上代码:

    在创建定时器时,i 同时还作为回调函数的形参传入了回调,根据按值传递的特性,不管定时器何时执行,i 早与创建时的 i 已经绑定在了一起。是不是很棒,原来定时器第三参数还可以这么使用,又学到了一点。

    那么到这里,我们居然掌握了四种做法,利用自执行函数创建闭包,利用按值传值的特性,利用ES6的let,以及利用定时器的第三参数。

    好了,既然说到了代码改写,那么问题再次升级:

     题目四

    定时器的第一参数由一个普通的回调函数变成了一个自执行函数,其它没什么改变,说说会怎么执行?

    答案是无等待的同时输出0,1,2,3,4。说到这可能有同学就有疑问了,输出0-4也就算了,为何输出之间还没间隔了。难道不是把五个自执行函数压入任务队列,然后先输出0后每隔一秒一次输出吗。

    我们都知道定时器有两种写法,以setTimeout为例:

    我们常用的是写法一,写法二之所以能正常运行,其实是类似于eval让字符串运行了,所以并不推荐第二种写法。而题目四的代码类似于这样:

    这段代码的意思是,运行到定时器时,直接将第一参数的函数给执行了,定时器的时间直接不会起作用了。我们可以通过下面的例子来证明这一点:

    上述代码几乎无等待的输出1,尽管这是一个周期性定时器,但之后都不会再执行了,因为第一参数不是一个合格的回调函数。

    怎么样,对定时器是不是又加深了一点印象,好了,面试题就到此为止了,说了很多,拓展了很多,我们来做个总结。

     叁 ❀ 总结

    通过本文的阅读,我们知道了以下知识点

    1.定时器是一个异步任务,准确来说,最终异步执行的是定时器的回调函数,倒计时以及定时器本身并非异步。

    2.我们理解了定时器时间的真正含义,它并非表示过多久之后执行,而是过多久之后将回调函数加入到任务队列

    3.我们知道了任务队列具有先进先出(FIFO)的特性,不管两个定时器定义先后如何,先加入队列的始终先执行(哪个时间设置的小先执行)。

    4.我们了解了定时器其实还有第三参数,它可以为回调函数传递参数。

    5.我们知道了定时器回调函数常用的两种写法,以及当回调函数带括号时会造成什么问题。

    6.我们知道了函数参数如果是简单类型数据时具有按值传递的特性,以及JS具备静态作用域的概念。

    7.我们知道了四种让上方面试题依次输出0-4的改写方法。

    最后我还知道,如果你在以后的面试中偶遇了类似的题目,你大概能秀的面试官头皮发麻,那么到这里本文结束。

     肆 ❀ 参考

    Excuse me?这个前端面试在搞事!

    80% 应聘者都不及格的 JS 面试题

  • 相关阅读:
    redis连接客户端
    map中使用的细节问题(不同key循坏中被后面的值替换)
    使用异步开启新的线程Spring异步方法注解@Async
    npm init 出现一堆提问(npm init -y)
    小程序的时间日期选择器
    小程序--分类项目跳转页面(同样也适用轮播图跳转)
    小程序样式不管用,解决方法button:not([size='mini']) { width: 184px; margin-left: auto; margin-right: auto; }
    vue-elementui的时间日期选择器( value-format="yyyy-MM-dd HH:mm:ss"),以及时间解析{y}-{m}-{d} {h}:{i}:{s}
    vue.config.js配置详细说明(逐条解释)
    element在el-table-column中如何使用过滤器
  • 原文地址:https://www.cnblogs.com/echolun/p/11481991.html
Copyright © 2020-2023  润新知