首先,记住同步第一、异步第二、回调最末的口诀

公式表达:同步=>异步=>回调

看一道经典的面试题:

for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('i: ',i);
}, 1000);
} console.log(i); //输出
5
i: 5
i: 5
i: 5
i: 5
i: 5

这道题目大家都遇到过了吧,那么为什么会输出这个呢?记住我们的口诀 同步 => 异步 => 回调

1、for循环和循环体外部的console是同步的,所以先执行for循环,再执行外部的console.log。(同步优先)

2、for循环里面有一个setTimeout回调,他是垫底的存在,只能最后执行。(回调垫底)

那么,为什么我们最先输出的是5呢?

非常好理解,for循环先执行,但是不会给setTimeout传参(回调垫底),等for循环执行完,就会给setTimeout传参,而外部的console打印出5是因为for循环执行完成了。

知乎有大神讲解过 80% 应聘者都不及格的 JS 面试题 ,就是以这个例子为开头的。但是没有说为什么setTimeout是输出5个5。

这里涉及到JavaScript执行栈和消息队列的概念,概念的详细解释可以看阮老师的 JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志,或者看 并发模型与Event Loop

《图片来自于MDN官方》

我拿这个例子做一下讲解,JavaScript单线程如何处理回调呢?JavaScript同步的代码是在堆栈中顺序执行的,而setTimeout回调会先放到消息队列,for循环每执行一次,就会放一个setTimeout到消息队列排队等候,当同步的代码执行完了,再去调用消息队列的回调方法。

在这个经典例子中,也就是说,先执行for循环,按顺序放了5个setTimeout回调到消息队列,然后for循环结束,下面还有一个同步的console,执行完console之后,堆栈中已经没有同步的代码了,就去消息队列找,发现找到了5个setTimeout,注意setTimeout是有顺序的。

那么,setTimeout既然在最后才执行,那么他输出的i又是什么呢?答案就是5。。有人说不是废话吗?

现在告诉大家为什么setTimeout全都是5,JavaScript在把setTimeout放到消息队列的过程中,循环的i是不会及时保存进去的,相当于你写了一个异步的方法,但是ajax的结果还没返回,只能等到返回之后才能传参到异步函数中。
在这里也是一样,for循环结束之后,因为i是用var定义的,所以var是全局变量(这里没有函数,如果有就是函数内部的变量),这个时候的i是5,从外部的console输出结果就可以知道。那么当执行setTimeout的时候,由于全局变量的i已经是5了,所以传入setTimeout中的每个参数都是5。很多人都会以为setTimeout里面的i是for循环过程中的i,这种理解是不对的。

==========================================================================================================================================

我们给第一个例子加一行代码。

for (var i = 0; i < 5; ++i) {
setTimeout(function() {
console.log('2: ',i);
}, 1000);
console.log('1: ', i); //新加一行代码
} console.log(i); //输出
1: 0
1: 1
1: 2
1: 3
1: 4
5
2: 5
2: 5
2: 5
2: 5
2: 5

同步 => 异步 => 回调 (强化记忆)

这个例子可以很清楚的看到先执行for循环,for循环里面的console是同步的,所以先输出,for循环结束后,执行外部的console输出5,最后再执行setTimeout回调 55555。。。

=====================================分割线============================================

那么面试官会问,怎么解决这个问题?

最简单的当然是let语法啦。

for (let i = 0; i < 5; ++i) {
setTimeout(function() {
console.log('2: ',i);
}, 1000);
} console.log(i); //输出
i is not defined
2: 0
2: 1
2: 2
2: 3
2: 4

咦,有同学问,为什么外部的i报错了呢?
又有同学问,你这个口诀在这里好像不适应啊?

let是ES6语法,ES5中的变量作用域是函数,而let语法的作用域是当前块,在这里就是for循环体。在这里,let本质上就是形成了一个闭包。也就是下面这种写法一样的意思。如果面试官对你说用下面的这种方式,还有let的方式,你可以严肃的告诉他:这就是一个意思!这也就是为什么有人说let是语法糖。

var loop = function (_i) {
setTimeout(function() {
console.log('2:', _i);
}, 1000);
}; for (var _i = 0; _i < 5; _i++) {
loop(_i);
} console.log(i);

写成ES5的形式,你是不是发现就适合我说的口诀了?而用let的时候,你发现看不懂?那是因为你没有真正了解ES6的语法原理。

我们来分析一下,用了let作为变量i的定义之后,for循环每执行一次,都会先给setTimeout传参,准确的说是给loop传参,loop形成了一个闭包,这样就执行了5个loop,每个loop传的参数分别是0,1,2,3,4,然后loop里面的setTimeout会进入消息队列排队等候。当外部的console执行完毕,因为for循环里的i变成了一个新的变量 _i ,所以在外部的console.log(i)是不存在的。

现在可以解释闭包的概念了:当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。

我知道你又要我解释这句话了,loop(_i)是外部函数,setTimeout是内部函数,当setTimeout被loop的变量访问的时候,就形成了一个闭包。

随便举个新的例子。

function t() {
var a = 10;
var b = function() {
console.log(a);
}
b();
}
t(); //输出 10

跟我一起念口诀:同步 => 异步 => 回调 (强化记忆)

先执行函数t,然后js就进入了t内部,定义了一个变量,然后执行函数b,进入b内部,然后打印a,这里都是同步的代码,没什么异议,那么这里怎么解释闭包:函数t是外部函数,函数b是内部函数,当函数b被函数t的变量访问的时候,就形成了闭包。

=====================================分割线============================================

上面主要讲了同步和回调执行顺序的问题,接着我就举一个包含同步、异步、回调的例子。

let a = new Promise(
function(resolve, reject) {
console.log(1)
setTimeout(() => console.log(2), 0)
console.log(3)
console.log(4)
resolve(true)
}
)
a.then(v => {
console.log(8)
}) let b = new Promise(
function() {
console.log(5)
setTimeout(() => console.log(6), 0)
}
) console.log(7)

1、看同步代码:a变量是一个Promise,我们知道Promise是异步的,是指他的then()和catch()方法,Promise本身还是同步的,所以这里先执行a变量内部的Promise同步代码。(同步优先)

    console.log(1)
setTimeout(() => console.log(2), 0) //回调
console.log(3)
console.log(4)

2、Promise内部有4个console,第二个是一个setTimeout回调(回调垫底)。所以这里先输出1,3,4回调的方法丢到消息队列中排队等着。

3、接着执行resolve(true),进入then(),then是异步,下面还有同步没执行完呢,所以then也滚去消息队列排队等候。(真可怜)(异步靠边)
4、b变量也是一个Promise,和a一样,执行内部的同步代码,输出5,setTimeout滚去消息队列排队等候。

5、最下面同步输出7。

6、同步的代码执行完了,JavaScript就跑去消息队列呼叫异步的代码:异步,出来执行了。这里只有一个异步then,所以输出8。

7、异步也over,轮到回调的孩子们:回调,出来执行了。这里有2个回调在排队,他们的时间都设置为0,所以不受时间影响,只跟排队先后顺序有关。则先输出a里面的回调2,最后输出b里面的回调6。

8、最终输出结果就是:1、3、4、5、7、8、2、6。

我们还可以稍微做一点修改,把a里面Promise的 setTimeout(() => console.log(2), 0)改成 setTimeout(() => console.log(2), 2),对,时间改成了2ms,为什么不改成1试试呢?1ms的话,浏览器都还没有反应过来呢。你改成大于或等于2的数字就能看到2个setTimeout的输出顺序发生了变化。所以回调函数正常情况下是在消息队列顺序执行的,但是使用setTimeout的时候,还需要注意时间的大小也会改变它的顺序。

口诀不一定是万能的,只能作为一个辅助,更重要的还是要理解JavaScript的运行机制,才能对代码执行顺序有清晰的路线。

还有async/await等其他异步的方案,不管是哪种异步,基本都适用这个口诀,对于新手来说,可以快速读懂面试官出的js笔试题目。以后再也不用害怕做笔试题啦。

js同步、异步、回调的执行顺序以及闭包的理解的更多相关文章

  1. js同步-异步-回调

    出处:https://blog.csdn.net/u010297791/article/details/71158212(1)上面主要讲了同步和回调执行顺序的问题,接着我就举一个包含同步.异步.回调的 ...

  2. js同步任务和异步任务的执行顺序

    先来道今日头条面试题开开胃 async function async1() { console.log('async1 start'); await async2(); console.log('as ...

  3. js 关于setTimeout和Promise执行顺序问题

    js 关于setTimeout和Promise执行顺序问题 异步 -- Promise和setTimeout 执行顺序   Promise 和 setTimeout 到底谁先执行 定时器的介绍 Jav ...

  4. 前端入门20-JavaScript进阶之异步回调的执行时机

    声明 本系列文章内容全部梳理自以下几个来源: <JavaScript权威指南> MDN web docs Github:smyhvae/web Github:goddyZhao/Trans ...

  5. C# 同步 异步 回调 状态机 async await Demo

    源码 https://gitee.com/s0611163/AsyncAwaitDemo 为什么会研究这个? 我们项目的客户端和服务端通信用的是WCF,我就想,能不能用异步的方式调用WCF服务呢?或者 ...

  6. uni-app 中实现 onLaunch 异步回调后执行 onLoad 最佳实践

    前言 好久没写博客了,由于公司业务需要,最近接触uiapp比较多,一直想着输出一些相关的文章.正好最近时间富余,有机会来一波输出了. 问题描述 在使用 uni-app 开发项目时,会遇到需要在 onL ...

  7. wex5 实战 wex5与js的组件关系与执行顺序(父子与先后)

    初学wex5,先理理让人容易混淆的三个概念: 一 基本概念: 1 wex5组件,顾名思义,在编辑窗口右侧的组件集合里的,都是wex5基于开源自创的组件,并封装了一套自已的方法.目的是为了方便.相关方法 ...

  8. JS的预编译和执行顺序 详析

    原文:JS的预编译和执行顺序 详析 最近在复习javascript的事件处理时发现了一个问题,然后也是我来写javascript的预编译和执行顺序的问题   代码:   复制代码 代码一 <ht ...

  9. 浅谈个人对客户端JavaScript同步、异步、执行顺序等概念的理解

    一.同步和异步的概念. 同步:即按代码的顺序执行任务. 在下列代码中,按照同步概念,则是先打印1后打印2. console.log(1); console.log(2); 异步:即执行一个任务的同时执 ...

随机推荐

  1. openssl内核升级

    由于工作需要,防止安全漏洞需要对openssl升级现在整理出centos6.8和ubuntu14.4升级 centos升级openssl 1.首先去OpenSSL的网站 https://www.ope ...

  2. web开发中xml的内容

    文档声明(注:文档声明前不能有注释) XML中的元素/标签 注:xmlx中解析程序会将其中的空格与换行当做内容来解析,区分大小写 CDATA区域中的内容不解析

  3. 已知长度为n的线性表采用顺序结构,写一算法删除该线性表中所有值为item的元素

    /** * @author:(LiberHome) * @date:Created in 2019/2/27 23:34 * @description: * @version:$ */ /*已知长度为 ...

  4. python全栈开发 * 31知识点汇总 * 180716

    31 模块和包一.模块(一)模块的种类:内置模块,自定义模块,扩展模块第三方模块(二)自定义模块 1.模块的创建 : 新建一个py文件. 2.模块名 : 模块名需要符合变量的命名规范. 3.模块的导入 ...

  5. Prometheus监控学习记录

    官方文档 Prometheus基础文档 从零开始:Prometheus 进阶之路:Prometheus —— 技巧篇 进阶之路:Prometheus —— 理解篇 prometheus的数据类型介绍 ...

  6. (备忘)打开office2010总是在配置进度

    1.同时按上键盘上面的windows键和R键,出现“运行” 2.输入“regedit”,回车进入注册表 3.点击“HKEY_CURRENT_USER”展开,依次“Software”--“Microso ...

  7. 15.3-uC/OS-III资源管理(多值信号量)

    多值信号量是 uC/OS 操作系统的一个内核对象, 主要用于标志事件的发生和共享资源管理. 1.如果想要使用多值信号量,就必须事先使能多值信号量. 多值信号量的使能位于“os_cfg.h”. 2.OS ...

  8. 15.0-uC/OS-III资源管理

    1.共享资源可以是:变量(静态的或全局的).结构体.内存空间. I/O等. 多个任务可能会同时要求占用资源: 内存空间. 全局变量. 指针.缓冲区. 列表. 环形缓冲区等. 通过共享资源, 任务间通信 ...

  9. Redis入门到高可用(十四)—— bitmap

    一.初步认识 二.API 1.setbit 2.getbit 3.bitcount 4.bitop 5.bitpos 三.位图使用 四. 使用经验

  10. 利用python脚本(re)抓取美空mm图片

    很久没有写博客了,这段时间一直在搞风控的东西,过段时间我把风控的内容整理整理发出来大家一起研究研究. 这两天抽空写了两个python爬虫脚本,一个使用re,一个使用xpath. 直接上代码——基于re ...