没有错,这道题就是:javascript
for (var i = 0; i< 10; i++){ setTimeout(() => { console.log(i); }, 1000) } // 10 10 10 10 ...
为何这里会出现10次10,而不是咱们预期的0-9呢?咱们要如何修改达到预期效果呢?java
首先咱们得理解setTimeout
中函数的执行时机,这里就要讲到一个运行时的概念。es6
函数调用造成了一个栈帧。数组
function foo(b) { var a = 10; return a + b + 11; } function bar(x) { var y = 3; return foo(x * y); } console.log(bar(7)); // 返回 42
当调用 bar
时,建立了第一个帧 ,帧中包含了 bar
的参数和局部变量。当 bar
调用 foo
时,第二个帧就被建立,并被压到第一个帧之上,帧中包含了 foo
的参数和局部变量。当 foo
返回时,最上层的帧就被弹出栈(剩下 bar
函数的调用帧 )。当 bar
返回的时候,栈就空了。promise
对象被分配在一个堆中,即用以表示一大块非结构化的内存区域。并发
一个 JavaScript 运行时包含了一个待处理的消息队列。每个消息都关联着一个用以处理这个消息的函数。异步
在事件循环(Event Loop)期间的某个时刻,运行时从最早进入队列的消息开始处理队列中的消息。为此,这个消息会被移出队列,并做为输入参数调用与之关联的函数。正如前面所提到的,调用一个函数老是会为其创造一个新的栈帧。async
函数的处理会一直进行到执行栈再次为空为止;而后事件循环将会处理队列中的下一个消息(若是还有的话)。函数
这里setTimeout
会等到当前队列执行完了以后再执行,即for
循环结束后执行,而这个时候i
的值已是10
了,因此会打印出来10个10这样的结果。oop
要是想获得预期效果,简单的删除setTimeout
也是可行的。固然也能够这样改setTimeout(console.log, 1000, i);
将i
做为参数传入函数。
仔细查阅规范可知,异步任务可分为 task
和 microtask
两类,不一样的API注册的异步任务会依次进入自身对应的队列中,而后等待 Event Loop 将它们依次压入执行栈中执行。
(macro)task主要包含:script(总体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
附上一幅图更清楚的了解一下
每一次Event Loop触发时:
其实promise的then和catch才是microtask,自己的内部代码不是。
new Promise(resolve => { resolve(1); Promise.resolve().then(() => console.log(2)); console.log(4) }).then(t => console.log(t)); console.log(3);
这道题比较基础,答案为4321。先执行同步任务,打印出43,而后分析微任务,2先入任务队列先执行,再打印出1。
这里还有几种变种,结果相似。
let promise1 = new Promise(resolve => { resolve(2); }); new Promise(resolve => { resolve(1); Promise.resolve(2).then(v => console.log(v)); //Promise.resolve(Promise.resolve(2)).then(v => console.log(v)); //Promise.resolve(promise1).then(v => console.log(v)); //new Promise(resolve=>{resolve(2)}).then(v => console.log(v)); console.log(4) }).then(t => console.log(t)); console.log(3);
不过要值得注意的是一下两种状况:
let thenable = { then: function(resolve, reject) { resolve(2); } }; new Promise(resolve => { resolve(1); new Promise(resolve => { resolve(promise1); }).then(v => { console.log(v); }); // Promise.resolve(thenable).then(v => { // console.log(v); // }); console.log(4); }).then(t => console.log(t)); console.log(3);
let promise1 = new Promise(resolve => { resolve(thenable); }); new Promise(resolve => { resolve(1); Promise.resolve(promise1).then(v => { console.log(v); }); // new Promise(resolve => { // resolve(promise1); // }).then(v => { // console.log(v); // }); console.log(4); }).then(t => console.log(t)); console.log(3);
结果为4312。有人可能会说阮老师这篇文章里提过
Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo'))
那为何这两个的结果不同呢?
请注意这里resolve
的前提条件是参数是一个原始值,或者是一个不具备then
方法的对象,而其余状况是怎样的呢,stackoverflow上这个问题分析的比较透彻,我这里简单的总结一下。
这里的RESOLVE('xxx')是new Promise(resolve=>resolve('xxx'))简写
Promise.resolve('nonThenable')
和 RESOLVE('nonThenable')
相似;Promise.resolve(thenable)
和 RESOLVE(thenable)
相似;Promise.resolve(promise)
要根据promise
对象的resolve
来区分,不为thenable
的话状况和Promise.resolve('nonThenable')
类似;RESOLVE(thenable)
和 RESOLVE(promise)
能够理解为 new Promise((resolve, reject) => { Promise.resolve().then(() => { thenable.then(resolve) }) })
也就是说能够理解为Promise.resolve(thenable)
会在这一次的Event Loop
中当即执行thenable
对象的then
方法,而后将外部的then
调入下一次循环中执行。
再形象一点理解,能够理解为RESOLVE(thenable).then
和PROMISE.then.then
的语法相似。
再来一道略微复杂一点的题加深印象
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0) async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end');
答案
/* script start async1 start async2 promise1 script end async1 end promise2 setTimeout */
总结一下阮老师的介绍。
let
命令,用来声明变量。它的用法相似于var
,可是所声明的变量,只在let
命令所在的代码块内有效。而var
全局有效。var
命令会发生“变量提高”现象,即变量能够在声明以前使用,值为undefined
。let
命令改变了语法行为,它所声明的变量必定要在声明后使用,不然报错。let
命令声明变量以前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。let
不容许在相同做用域内,重复声明同一个变量。const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须当即初始化,不能留到之后赋值。const
其余用法和let
相同。上面代码中,变量i
是var
命令声明的,在全局范围内都有效,因此全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。也就是说,全部数组a
的成员里面的i
,指向的都是同一个i
,致使运行时输出的是最后一轮的i
的值,也就是 10。
要是想获得预期效果,能够简单的把var
换成let
。
let
实际上为 JavaScript 新增了块级做用域。
function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 }
上面的函数有两个代码块,都声明了变量n
,运行后输出 5。这表示外层代码块不受内层代码块的影响。若是两次都使用var
定义变量n
,最后输出的值才是 10。
块级做用域的出现,实际上使得得到普遍应用的当即执行函数表达式(IIFE)再也不必要了。
// IIFE 写法 (function () { var tmp = ...; ... }()); // 块级做用域写法 { let tmp = ...; ... }
若是不用let
,咱们可使用iife将setTimeout
包裹,从而达到预期效果。
for (var i = 0; i < 10; i++) { (i => setTimeout(() => { console.log(i); }, 1000))(i); }
for (var i = 0; i < 10; i++) { try { throw i; } catch (i) { setTimeout(() => { console.log(i); }, 1000); } }
What's the difference between resolve(thenable) and resolve('non-thenable-object')?