JS执行

245 阅读7分钟

1)、宏观和微观任务

  • 宏观任务—宿主发起的任务;
  • 微观任务—JavaScript 引擎发起的任务;
  • 在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列;
  • 有了宏观任务和微观任务机制,就可以实现 JavaScript 引擎级和宿主级的任务了,例如:Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。

2)、Promise

  • Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。
    function sleep(duration) {
      return new Promise(function (resolve) {
        console.log("start");
        setTimeout(resolve, duration);
      });
    }
    await sleep(1000).then(() => console.log("finished"));
    
  • Promise 里的代码比 setTimeout 先执行,d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。
    var r = new Promise(function (resolve, reject) {
      console.log("a");
      resolve();
    });
    setTimeout(() => console.log("d"), 0);
    r.then(() => console.log("c"));
    console.log("b");
    
  • 为了理解微任务始终先于宏任务,我们设计一个实验:执行一个耗时 1 秒的 Promise。我们可以看到,即使耗时一秒的 c1 执行完毕,再 enque 的 c2,仍然先于 d 执行了,这很好地解释了微任务优先的原理。
    setTimeout(() => console.log("d"), 0);
    var r = new Promise(function (resolve, reject) {
      resolve();
    });
    r.then(() => {
      var begin = Date.now();
      while (Date.now() - begin < 1000);
      console.log("c1");
      new Promise(function (resolve, reject) {
        resolve();
      }).then(() => console.log("c2"));
    });
    
  • 通过一系列的实验,我们可以总结一下如何分析异步执行的顺序:
    • 首先我们分析有多少个宏任务;
    • 在每个宏任务中,分析有多少个微任务;
    • 根据调用次序,确定宏任务中的微任务执行次序;
    • 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
    • 确定整个顺序。

3)、async/await

  • async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数;
  • async 函数是一种特殊语法,特征是在 function 关键字之前加上 async 关键字,这样,就定义了一个 async 函数,我们可以在其中使用 await 来等待一个 Promise。

4)、闭包

  • 闭包其实只是一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境。这个古典的闭包定义中,闭包包含两个部分。

    • 环境部分
      • 环境:函数的词法环境(执行上下文的一部分)
      • 标识符列表:函数中用到的未声明的变量
    • 表达式部分:函数体

5)、执行上下文

  • 在 ES3 中,包含三个部分。
    • scope:作用域,也常常被叫做作用域链;
    • variable object:变量对象,用于存储变量的对象;
    • this value:this 值。
  • 在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。
    • lexical environment:词法环境,当获取变量时使用;
    • variable environment:变量环境,当声明变量时使用;
    • this value:this 值。
  • 在 ES2018 中,执行上下文又变成了这个样子,
    • this 值被归入 lexical environment,但是增加了不少内容;
    • lexical environment:词法环境,当获取变量或者 this 值时使用;
    • variable environment:变量环境,当声明变量时使用;
    • code evaluation state:用于恢复代码执行位置;
    • Function:执行的任务是函数时使用,表示正在被执行的函数;
    • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码;
    • Realm:使用的基础库和内置对象实例;
    • Generator:仅生成器上下文有这个属性,表示当前生成器。

6)、变量声明

 ```js
 var b = {}
 let c = 1
 this.a = 2;
 ```
  • 要想正确执行它,我们需要知道以下信息:

    • var 把 b 声明到哪里;
    • b 表示哪个变量;
    • b 的原型是哪个对象;
    • let 把 c 声明到哪里;
    • this 指向哪个对象。
  • 这些信息就需要执行上下文来给出了,这段代码出现在不同的位置,甚至在每次执行中,会关联到不同的执行上下文,所以,同样的代码会产生不一样的行为。

  • var 声明与赋值,只有 var,没有 let 的旧 JavaScript 时代,诞生了一个技巧,叫做:立即执行的函数表达式(IIFE),通过创建一个函数,并且立即执行,来构造一个新的域,从而控制 var 的范围。

  • let 是 ES6 开始引入的新的变量声明模式,比起 var 的诸多弊病,let 做了非常明确的梳理和规定。为了实现 let,JavaScript 在运行时引入了块级作用域。也就是说,在 let 出现之前,JavaScript 的 if for 等语句皆不产生作用域。我简单统计了下,以下语句会产生 let 使用的作用域:

    • for;
    • if;
    • switch;
    • try/catch/finally。
  • Realm: Realm 中包含一组完整的内置对象,而且是复制关系。对不同 Realm 中的对象操作,会有一些需要格外注意的问题,比如 instanceOf 几乎是失效的。

    var iframe = document.createElement("iframe");
    document.documentElement.appendChild(iframe);
    iframe.src = "javascript:var b = {};";
    
    var b1 = iframe.contentWindow.b;
    var b2 = {};
    
    console.log(typeof b1, typeof b2); //object object
    
    console.log(b1 instanceof Object, b2 instanceof Object); //false true
    

7)、 函数

  • 普通函数:用 function 关键字定义的函数。

    function foo() {
      // code
    }
    
  • 箭头函数:用 => 运算符定义的函数。

    const foo = () => {
      // code
    };
    
  • 方法:在 class 中定义的函数。。

    class C {
      foo() {
        //code
      }
    }
    
  • 生成器函数:用 function * 定义的函数。

    function* foo() {
      // code
    }
    
  • 类:用 class 定义的类,实际上也是函数。

    class Foo {
      constructor() {
        //code
      }
    }
    
  • 异步函数:普通函数、箭头函数和生成器函数加上 async 关键字。

      async function foo(){
          // code
      }
      const foo = async () => {
          // code
      }
      async function foo*(){
          // code
      }
    
  • this 关键字的行为

    • 对普通变量而言,这些函数并没有本质区别,都是遵循了“继承定义时环境”的规则,它们的一个行为差异在于 this 关键字。

    • this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不同,得到的 this 值也不同,我们看一个例子:

      function showThis() {
        console.log(this);
      }
      
      var o = {
        showThis: showThis,
      };
      
      showThis(); // global
      o.showThis(); // o
      
    • 调用函数时使用的引用,决定了函数执行时刻的 this 值,实际上从运行时的角度来看,this 跟面向对象毫无关联,它是与函数调用时使用的表达式相关。

    • 为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。

    • 嵌套的箭头函数中的代码都指向外层 this。

    • 操作 this 的内置函数,Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值,Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的 this 值固定了参数。

      function foo(a, b, c) {
        console.log(this);
        console.log(a, b, c);
      }
      foo.call({}, 1, 2, 3);
      foo.apply({}, [1, 2, 3]);
      
      function foo(a, b, c) {
        console.log(this);
        console.log(a, b, c);
      }
      foo.bind({}, 1, 2, 3)();
      // 有趣的是,call、bind 和 apply 用于不接受 this 的函数类型如箭头、class 都不会报错。这时候,它们无法实现改变 this 的能力,但是可以实现传参。
      
  • new 与 this,通过 new 调用函数,跟直接调用的 this 取值有明显区别,仅普通函数和类能够跟 new 搭配使用.

8)、try 里面放 return,finally 还会执行吗

  • 我们来看一个例子。在函数 foo 中,使用了一组 try 语句。我们可以先来做一个小实验,在 try 中有 return 语句,finally 中的内容还会执行吗?我们来看一段代码。
function foo() {
  try {
    return 0;
  } catch (err) {
  } finally {
    console.log("a");
  }
}
console.log(foo());
  • 通过实际试验,我们可以看到,finally 确实执行了,而且 return 语句也生效了,foo() 返回了结果 0。虽然 return 执行了,但是函数并没有立即返回,又执行了 finally 里面的内容,这样的行为违背了很多人的直觉。
  • 如果在这个例子中,我们在 finally 中加入 return 语句,会发生什么呢?
function foo() {
  try {
    return 0;
  } catch (err) {
  } finally {
    return 1;
  }
}
console.log(foo());

通过实际执行,我们看到,finally 中的 return “覆盖”了 try 中的 return。在一个函数中执行了两次 return,这已经超出了很多人的常识,也是其它语言中不会出现的一种行为。

语句分类