壹 ❀ 引

最近在看前端进阶的系列专栏,碰巧看到了几篇关于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. 比特平面分层(一些基本的灰度变换函数)基本原理及Python实现

    1. 基本原理 在灰度图中,像素值的范围为[0, 255],即共有256级灰度.在计算机中,我们使用8比特数来表示每一个像素值.因此可以提取出不同比特层面的灰度图.比特层面分层可用于图片压缩:只储存较 ...

  2. redis缓存介绍以及常见问题浅析

    # 没缓存的日子: 对于web来说,是用户量和访问量支持项目技术的更迭和前进.随着服务用户提升.可能会出现一下的一些状况: 页面并发量和访问量并不多,mysql足以支撑自己逻辑业务的发展.那么其实可以 ...

  3. 2.PHP利用PDO连接方式连接mysql数据库

    代码如下 <?php$serverName = "这里填IP地址";$dbName = "这里填数据库名";$userName = "这里填用户 ...

  4. GD32电压不足时烧写程序导致程序运行异常的解决方法

    一直使用的GD32F450前段时间遇到这样一个问题,当使用J-Link供电给板子烧写程序之后,程序运行缓慢,就像运行在FLASH高速部分之外一样,但是如果使用外部供电烧写,就不会出现这个问题,而且一旦 ...

  5. powerdesign进军(二)--oracle数据源配置

    目录 资源下载(oracle客户端) 配置 查看系统的数据源 powerdesign 连接数据库 title: powerdesign进军(二)--oracle数据源配置 date: 2019-05- ...

  6. Vue创建项目配置

    前言 安装VS Code,开始vue的学习及编程,但是总是遇到各种各样的错误,控制台语法错误,格式错误.一股脑的袭来,感觉创建个项目怎么这个麻烦.这里就讲一下vue的安装及创建. 安装环境 当然第一步 ...

  7. 7.26 面向对象_封装_property_接口

    封装 封装 就是隐藏内部实现细节, 将复杂的,丑陋的,隐私的细节隐藏到内部,对外提供简单的访问接口 为什么要封装 1.保证关键数据的安全性 2.对外部隐藏实现细节,隔离复杂度 什么时候应该封装 1.当 ...

  8. SpringCloud微服务小白入门之Eureka注册中心和服务中心搭建示例

    一.注册中心配置文件 代码复制区域: spring: application: name: spring-cloud-server server: port: 7000 eureka: instanc ...

  9. 《统计学习方法》极简笔记P2:感知机数学推导

    感知机模型 输入空间是$\chi\subseteq\mathbb{R}^n$,输出空间是$y={+1,-1}$ 感知机定义为:$f(x)=sign(wx+b)$ 感知机学习策略 输入空间任一点$x_0 ...

  10. 小白学Python(3)——输入和输出,显示你的名字

    任何计算机程序都是为了执行一个特定的任务,有了输入,用户才能告诉计算机程序所需的信息,有了输出,程序运行后才能告诉用户任务的结果. 输入是Input,输出是Output,因此,我们把输入输出统称为In ...