壹 ❀ 引

最近在看前端进阶的系列专栏,碰巧看到了几篇关于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. 浅析java中的语法糖

    概述 编译器是一种计算机程序, 它主要的目的是将便于人编写.阅读.维护的高级计算机语言所写的源代码程序, 翻译为计算机能解读.运行的低阶机器语言的程序, 即可执行文件.而 javac 就是java语言 ...

  2. JMS入门简介

    一.JMS是什么 1.JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中 ...

  3. React Native-安卓环境的搭建

    最近公司做一个项目,项目中使用React Native(简称RN)来写安卓APP,现在我分享下安卓环境搭建的过程. 我参照https://reactnative.cn/docs/getting-sta ...

  4. css3实现loading效果--当页面加载过程中显示Loading的进度条,全部加载完成之后进度条消失

    一个页面等图片资源全部加载完成,会需要很长时间,用户体验会很差,所以我们需要loading来掩盖这个漫长的过程! emmm,定时器?写个定时器还要清除,万一造成内存泄露?定时器之间还会互相影响,呼呼呼 ...

  5. laya2d 与 cad 之间的坐标转换

    坐标系基本概念 直角坐标系可分为左手坐标系与右手坐标系,cad 中用到的是右手坐标系, Laya2D 中用到的是左手坐标系, Laya3D 中使用右手坐标系. 那么如何判断二维直角坐标系是左手还是右手 ...

  6. Vue-Router中History模式

    目录 history路由 官方示例 Express中间件 客户端兜底404 示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在 ...

  7. Element-UI 表单验证规则rules 配置参数说明

    官方文档 : https://github.com/yiminghe/async-validator

  8. Android老司机搬砖小技巧

    作为一名Android世界的搬运工,每天搬砖已经够苦够累了,走在坑坑洼洼的道路一不小心就掉坑里了. SDK常用工具类 Android SDK中本身就拥有很多轮子,熟悉这些轮子,可以提高我们的搬砖效率. ...

  9. 十款强大的IDEA插件-Java开发者的利器

    xl_echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加echo微信(微信号:t2421499075)交流学习. 百战不败,依不自称常胜,百败不颓,依能奋力前行.--这才是真正的堪称强大!! 插 ...

  10. 怒改springMVC项目为springBoot项目

    背景:公司最近在做项目升级,融合所有项目,但是目前使用的一个系统还是最原始的框架 springMVC+spring+mybatis ,前端还是jsp,easyui(技术老的掉牙),终于出手了,结果.. ...