壹 ❀ 引

最近在看前端进阶的系列专栏,碰巧看到了几篇关于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 面试题

从一道看似简单的面试题重新理解JS执行机制与定时器的更多相关文章

  1. 简单而面试中又常见的知识点:JS执行机制

        在开始讲解之前,我们先来看一段代码: console.log('1'); setTimeout(function() { console.log('2'); process.nextTick( ...

  2. 一道看似简单的sql需求却难倒各路高手 - 你也来挑战下吗?

    转自:http://www.cnblogs.com/keguangqiang/p/4535046.html 听说这题难住大批高手,你也来试下吧.ps:博问里的博友提出的. 原始数据 select *  ...

  3. 一道看似简单的sql需求(转)

    听说这题难住大批高手,你也来试下吧.ps:博问里的博友提出的. 原始数据 select * from t_jeff t  简单排序后数据 select * from t_jeff t order by ...

  4. 一道看似简单的go程序的深入分析

    先上代码: func main() { var a [10]int for i := 0; i < 10; i++ { go func(i int) { for { a[i]++ } }(i) ...

  5. 超耐心地毯式分析,来试试这道看似简单但暗藏玄机的Promise顺序执行题

    壹 ❀ 引 就在昨天,与朋友聊到JS基础时,她突然想起之前在面试时,遇到了一道难以理解的Promise执行顺序题.由于我之前专门写过手写promise的文章,对于部分原理也还算了解,出于兴趣我便要了这 ...

  6. 一道简单的面试题,难倒各大 Java 高手!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 最近栈长在我们的<Java技术栈知识星球>上分享的一道 Java 实战面试题,很有意思,现在拿出来和大家分享下, ...

  7. OpenJDK源码研究笔记(五)-缓存Integer等类型的频繁使用的数据和对象,大幅度提升性能(一道经典的Java笔试题)

    摘要 本文先给出一个看似很简单实则有深意的Java笔试面试题,引出JDK内部的缓存. JDK内部的缓存,主要是为了提高Java程序的性能. 你能答对这道"看似简单,实则有深意"的J ...

  8. 一道关于String的面试题,新鲜出炉,刚被坑过,趁热!!

    很多人都会答错的一道关于String的题目,究竟有什么难度? 我们一起来看一道关于String的面试题,准确说是改编的面试题! 准备好啦?在放大招之前先来一个小招式 String s1 = new S ...

  9. 李洪强iOS经典面试题147-WebView与JS交互

    李洪强iOS经典面试题147-WebView与JS交互   WebView与JS交互 iOS中调用HTML 1. 加载网页 NSURL *url = [[NSBundle mainBundle] UR ...

随机推荐

  1. hdu 6406 Taotao Picks Apples (线段树)

    Problem Description There is an apple tree in front of Taotao's house. When autumn comes, n apples o ...

  2. 记录eclipse中文出现空格宽度不一致的bug

    起因 不久前更新了 eclipse(2019-03) 版本:突然发现出现了,使用注释使用中出现的空格的间隔大小不一致的问题,具体可以看下图: 遇到这种问题简直逼不能忍,在网上搜一下解决方式: 谷歌 搜 ...

  3. jQuery插件之路(三)——文件上传(支持拖拽上传)

    好了,这次咱一改往日的作风,就不多说废话了,哈哈.先贴上源代码地址,点击获取.然后直接进入主题啦,当然,如果你觉得我有哪里写的不对或者欠妥的地方,欢迎留言指出.在附上一些代码之前,我们还是先来了解下, ...

  4. 禅道、jenkins部署记录

    禅道部署1.检查你linux系统的位数(uname -a)2.下载对应位数的禅道包3.通过xftp工具将禅道包拷贝到虚拟机的/opt目录4.tar 对禅道包进行解压5.改配置:vi /opt/zbox ...

  5. spring cloud stream 经验总结

    ---恢复内容开始--- 基本概念 spring: cloud: stream: kafka: binder: brokers: cloudTest:19092 zk-nodes: cloudTest ...

  6. Ubuntu 18.04 LTS版本 GoldenDict安装与配置

    为何安装? GoldenDict是一款Linux下很好用的词典软件,其具有的关于词典的裁剪功能使得用户能够方便地对各种词典进行添加或删除,其具有的屏幕取词功能能够帮助用户方便地进行翻译,其具有的网络源 ...

  7. Spark 系列(十一)—— Spark SQL 聚合函数 Aggregations

    一.简单聚合 1.1 数据准备 // 需要导入 spark sql 内置的函数包 import org.apache.spark.sql.functions._ val spark = SparkSe ...

  8. 洛谷 P2152 [SDOI2009]SuperGCD

    题意简述 求两个整数a,b的最大公约数0 < a , b ≤ 10 ^ 10000. 题解思路 如果 a % 2 == 0 && b % 2 == 0 gcd(a,b) = gc ...

  9. 如何在GitHub上上传自己本地的项目?(很适合新手使用哦!)

    这是我看了一些大佬们的博客后,尝试了几次,终于成功了上传项目,所以想做一下总结,以便以后查看,同时想分享给才接触GitHub的新手们,希望能够有所帮助~ 条条大路通罗马,上传的方法肯定不止一种,等我学 ...

  10. Sqlserver 查询分组 记录

    select b.* from (select a.*,row_number() over (partition by 列1 order by 列2 desc) rn from a) b ; --如需 ...