资讯专栏INFORMATION COLUMN

由一道题引申出的事件循环、letvar用法、iife、块级作用域

animabear / 2420人阅读

摘要:和块级作用域实际上为新增了块级作用域。这表示外层代码块不受内层代码块的影响。块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式不再必要了。其他骚气方法参考阮老师并发模型与事件循环

没有错,这道题就是:

for (var i = 0; i< 10; i++){
    setTimeout(() => {
        console.log(i);
    }, 1000)
} // 10 10 10 10 ...

为什么这里会出现10次10,而不是我们预期的0-9呢?我们要如何修改达到预期效果呢?

运行时&&事件循环

首先我们得理解setTimeout中函数的执行时机,这里就要讲到一个运行时的概念。

函数调用形成了一个栈帧。

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 返回的时候,栈就空了。

对象被分配在一个堆中,即用以表示一大块非结构化的内存区域。

队列

一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。

在事件循环(Event Loop)期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个消息会被移出队列,并作为输入参数调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

与这题的关联

这里setTimeout会等到当前队列执行完了之后再执行,即for循环结束后执行,而这个时候i的值已经是10了,所以会打印出来10个10这样的结果。

要是想得到预期效果,简单的删除setTimeout也是可行的。当然也可以这样改setTimeout(console.log, 1000, i);i作为参数传入函数。

引申出其他

仔细查阅规范可知,异步任务可分为 taskmicrotask 两类,不同的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触发时:

执行完主执行线程中的任务。

取出micro-task中任务执行直到清空。

取出macro-task中一个任务执行。

取出micro-task中任务执行直到清空。

重复3和4。

其实promise的then和catch才是microtask,本身的内部代码不是。

ps: 再额外附上一道题
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).thenPROMISE.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和const用法和区别

总结一下阮老师的介绍。

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。而var全局有效。

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefinedlet命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

let不允许在相同作用域内,重复声明同一个变量。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。const其他用法和let相同。

与这题关联

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

要是想得到预期效果,可以简单的把var换成let

iife和块级作用域

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);
  }
}
参考

阮老师es6

What"s the difference between resolve(thenable) and resolve("non-thenable-object")?

Daily-Interview-Question

并发模型与事件循环

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103503.html

相关文章

  • 深入理解JavaScript(二):一道来思考闭包

    摘要:中所有的事件绑定都是异步编程当前这件事件没有彻底完成,不再等待,继续执行下面的任务当绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束即是最后。 概念 闭包就是指有权访问另一个函数作用域中的变量的函数 点击li标签弹出对应数字 0 1...

    曹金海 评论0 收藏0
  • 《深入理解ES6》笔记——块级作用绑定(1)

    摘要:没有声明的情况和都能够声明块级作用域,用法和是类似的,的特点是不会变量提升,而是被锁在当前块中。声明常量,一旦声明,不可更改,而且常量必须初始化赋值。临时死区的意思是在当前作用域的块内,在声明变量前的区域叫做临时死区。 本章涉及3个知识点,var、let、const,现在让我们了解3个关键字的特性和使用方法。 var JavaScript中,我们通常说的作用域是函数作用域,使用var声...

    2bdenny 评论0 收藏0
  • javascript中为什么我们不能直接使用export?

    摘要:我们可以认为,宏任务中还有微任务这里不再多做解释可能会执行的代码包括脚本模块和函数体。声明声明永远作用于脚本模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量。 相信很多人最开始时都有过这样的疑问假如我的项目目录下有一个 index.html, index.js 于是我像这样写 在浏览器之间打开index.html,发现showImg(https://...

    URLOS 评论0 收藏0
  • 简单理解JavaScript中的闭包

    摘要:闭包在我理解是一种比较抽象的东西。所以我写了一篇博文来方便自己理解闭包。那么现在我们可以解释一下闭包的第一个定义在计算机科学中,闭包是引用了自由变量的函数。循环中创建闭包在我们使用的关键字之前,闭包的一个常见问题就出现在循环中创建闭包。 零. 前言 从我开始接触前端时就听说过闭包,但是一直不理解闭包究竟是什么。上网看了各种博客,大家对闭包的说法不一。闭包在我理解是一种比较抽象的东西。所...

    sihai 评论0 收藏0

发表评论

0条评论

animabear

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<