关于前端:JavaScript系列之执行机制

50次阅读

共计 8021 个字符,预计需要花费 21 分钟才能阅读完成。

学习并总结 JavaScript 执行机制相干常识,内容程序由大到小,蕴含了 运行时概念 事件循环 函数执行 语句执行

运行时概念

执行栈(Stack)

函数调用造成了一个由若干帧组成的栈

function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42
  1. 当调用 bar 时,第一个帧被创立并压入栈中,帧中蕴含了 bar 的参数和局部变量
  2. 当 bar 调用 foo 时,第二个帧被创立并被压入栈中,放在第一个帧之上,帧中蕴含 foo 的参数和局部变量
  3. 当 foo 执行结束而后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧)
  4. 当 bar 也执行结束而后返回时,第一个帧也被弹出,栈就被清空了

堆(Heap)

对象被调配在堆中

工作队列(Queue)

  • 一个 JavaScript 运行时蕴含了一个待处理的工作队列,每个工作都关联着一个回调函数
  • 事件循环 期间时,先进入队列的工作会被优先解决,而后被解决的工作会被移出队列,并作为输出参数来调用与之关联的回调函数
  • 调用一个回调函数总是会为其发明一个新的栈帧,函数的解决会始终进行到执行栈再次为空为止,而后事件循环将会解决队列中的下一个工作(如果还有的话)

可视化形容

– 引自 MDN

事件循环

JavaScript 引擎期待宿主环境分配任务是一个 期待  -> 执行  的过程,周而复始,也就是事件循环

while (queue.waitForTask()) {queue.processNextTask()
}

宏工作(MacroTask)、微工作(MicroTask)

  • 由宿主环境发动的工作称为 宏工作,常见的有:scriptI/OsetTimeoutsetIntervalsetImmediaterequestAnimationFrame
  • JavaScript 引擎本身发动的工作称为 微工作,常见的有:Promise.then/catch/finallyasync/awaitMutationObserverprocess.nextTick
  • 宏工作 从头执行一段程序(比方从一个控制台,或在一个 <script> 元素中运行代码)执行一个事件回调 一个 interval/timeout 被触发 之类的规范机制而被调度的 JavaScript 代码,这些都在 宏工作队列上 被调度
  • 在宏工作中还蕴含了如 Promise 等由 JavaScript 引擎发动的 微工作 ,JavaScript 必须保障这些微工作 在一个宏工作中 实现,所以每一个宏工作中还蕴含了一个 微工作队列

    • 每当一个宏工作存在,事件循环都会查看该工作是否正把控制权交给其余 JavaScript 代码,如果没有,事件循环就会运行微工作队列中的所有微工作

宏工作和微工作的关系应该是这样的

他们的执行程序应该是这样的

  1. 执行栈抉择最先进入队列的宏工作(个别都是 script),执行其同步代码至 执行栈为空 且控制权尚未返还给用来驱动脚本执行环境的事件循环之前
  2. 查看是否存在微工作(如 Promise),有则会执行至微工作,直到队列为空
  3. 开始下一轮,执行下一个宏工作(setTimeout 等回调)
  4. 所以微工作总是会先于宏工作

Promise、async/await

Promise 是 JavaScript 语言提供的一种标准化的异步治理形式,它的总体思维是,须要进行 io、期待或者其它异步操作的函数,不返回实在后果,而返回一个 承诺,函数的调用方能够在适合的机会,抉择期待这个承诺兑现

  • 看一个能够无效证实微工作先于宏工作的例子
setTimeout(() => console.log('d'), 0)

const r = new Promise((resolve, reject) => {resolve()
})

r.then(() => {const begin = Date.now()
  while (Date.now() - begin < 1000);
  
  console.log('c1')
  
  new Promise((resolve, reject) => {resolve()
  }).then(() => console.log('c2'))
})
1. 强制 1 秒的执行耗时,确保微工作 c2 是在宏工作 d 之后被增加到工作队列
2. 耗时 1 秒的 c1 执行结束,再入队的 c2 仍先于 d 执行了 
最终输入:c1 c2 d
  • 再看一个比拟综合的例子
const sleep = duration => {return new Promise((resolve, reject) => {console.log('b')
    setTimeout(resolve, duration)
    // resolve()})
}
console.log('a')

sleep(1000).then(() => console.log('c'))

foo()

setTimeout(() => {console.log('e')
  }, 3000)

setTimeout(() => {console.log('d')
}, 2000)

async function foo(){await sleep(5000)
    console.log('f')
}
首先剖析宏工作
1. console.log('a') 和 Promise 中的 console.log('b')
2. await 返回的 Promise 中的 console.log('b')
3. 1000ms setTimeout(resolve, duration) 
4. 2000ms setTimeout console.log('d')
5. 3000ms setTimeout console.log('e')
6. await 返回的 Promise 中的 5000ms setTimeout(resolve, duration) 
微工作
1. 第 3 个宏工作中调用的 resolve console.log('c')
2. 第 6 个宏工作中 await 前面的 console.log('c') 
最终输入程序为:a, b, b, c, d, e, f

剖析 异步执行 的程序:

  1. 有多少个宏工作
  2. 在每个宏工作中,有多少个微工作
  3. 依据调用秩序,确定宏工作中的微工作执行秩序
  4. 依据宏工作的触发规定和调用秩序,确定宏工作的执行秩序
  5. 确定整个程序

函数执行

执行上下文与作用域

JavaScript 规范把一段代码(包含函数),执行所需的所有信息定义为 执行上下文,任何变量都存在于某个执行上下文中(也称为作用域),这个上下文(作用域)决定了变量的生命周期,以及它们能够拜访代码的哪些局部

var color = "blue";

function changeColor() {
  let anotherColor = "red";

  function swapColors() {
    let tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;

    // 这里能够拜访 color、anotherColor 和 tempColor
  }

  // 这里能够拜访 color 和 anotherColor,但拜访不到 tempColor
  swapColors();}

// 这里只能拜访 color
changeColor();

以上代码剖析如下:

  1. 波及 3 个上下文,全局上下文、changeColor()的部分上下文和 swapColors() 的部分上下文
  2. 全局上下文中有一个变量 color 和一个函数chageColor()
  3. changeColor()的部分上下文中有一个变量 anotherColor 和一个函数swapColors(),但在这里能够拜访全局上下文中的变量color
  4. swapColors()的部分上下文中有一个变量tempColor,只能在这个上下文中拜访到。
  5. 全局上下文和 changeColor() 的部分上下文都无法访问到tempColor
  6. 而在 swapColors() 中则能够拜访另外两个上下文中的变量,因为它们都是父上下文

总结几点:

  • 代码执行流每进入一个新上下文,都会创立一个 作用域链,用于搜寻变量和函数
  • 函数或块的部分上下文不仅能够拜访本人作用域内的变量,而且也能够拜访任何蕴含上下文乃至全局上下文中的变量
  • 全局上下文只能拜访全局上下文中的变量和函数,不能间接拜访部分上下文中的任何数据

几种函数

JavaScript 中的函数有下列几种模式:

  • 一般函数
function foo(){// code}

  • 箭头函数
const foo = () => {// code}

  • 用 class 定义的类也是函数
class Foo {constructor(){//code}
}

  • 办法,例如在 class 中定义的函数
class C {foo(){//code}
}

  • 生成器函数
function* foo(){// code}

  • 异步函数
async function foo(){// code}
const foo = async () => {// code}
async function foo*(){// code}

this

JavaScript 中的一个关键字,以后执行上下文(global、function 或 eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下能够是任意值

行为

同一个函数调用形式不同,失去的 this 值也会不同,看几个例子:

  • 应用一般函数
function showThis(){console.log(this);
}

var o = {showThis: showThis}

showThis(); // global
o.showThis(); // o

调用函数时应用的援用,决定了函数执行时刻的 this

  • 应用箭头函数
const showThis = () => {console.log(this);
}

var o = {showThis: showThis}

showThis(); // global
o.showThis(); // global

不管用什么援用来调用它,都不影响它的 this 值,因为箭头函数体内的 this 对象,就是定义时所在的对象,而不是调用时所在的对象

  • 应用办法
class C {showThis() {console.log(this);
    }
}
var o = new C();
var showThis = o.showThis;

showThis(); // undefined
o.showThis(); // o

class 外部会默认依照严格模式执行

  • 非严格模式和严格模式比照
function fun() { return this;}
console.log(fun()); // window
console.log(fun.call(2)); // Number
console.log(fun.apply(null)); // window
console.log(fun.call(undefined)); // window
console.log(fun.bind(true)()); // Boolean
"use strict";
function fun() { return this;}
console.log(fun()); // undefined
console.log(fun.call(2)); // 2
console.log(fun.apply(null)); // null
console.log(fun.call(undefined)); // undefined
console.log(fun.bind(true)()); // true

在严格模式下,指定的 this 不再被封装为对象,而且如果没有指定 this 的话它值是 undefined

绑定模式

由绑定优先级从高到低的程序列一下常见的几种绑定模式

new 的绑定与实现

new 都做了哪些事件:

  1. 创立新的空对象,指定原型
  2. 执行构造函数,并且绑定 this
  3. 判断构造函数是否返回对象,有就返回此对象
  4. 构造函数无返回值返回创立的新对象
function _new() {const [constructor, ...args] = [...arguments]
  // 创立一个空对象,指定原型为 constructor.prototype
  const obj = Object.create(constructor.prototype)
  // 执行构造函数,绑定 this
  const result = constructor.apply(obj, args)
  // 如果构造函数返回一个对象,那么返回该对象
  if (result && (typeof result === 'object' || typeof result === 'function'))
    return result
  // 如果没有就返回新对象
  return obj
}

function Person(name, age) {
  this.name = name
  this.age = age
}

_new(Person, 'mxin', 18)
// Person {name: "mxin", age: "18"}
//     age: "18"
//     name: "mxin"
//     __proto__:
//         constructor: ƒ Person(name, age)
//         __proto__: Object    

const mxin = _new(Person, 'mxin', 18)
console.log(mxin.name, mxin.age)
// mxin,18

显式绑定

应用 callapplybind 三种绑定 this 的形式为显式绑定

  • callapply 两个办法参数不同,成果雷同,且都会执行传入的函数
  • bind 不会执行函数

隐式绑定

函数的调用是在某个对象上触发的,即调用地位上存在上下文对象或被某个对象蕴含

const getName = function(){console.log(`Hello, ${this.name}`);
}
const person = {
    name: 'mxin',
    getName: getName
}
person.getName();

默认绑定

在没有以上几种绑定模式下,此种为默认绑定模式,非严格模式下,浏览器中 this 默认指向 window,严格模式下默认为 undefined

var a = 2;
function foo(){console.log(this.a);
}
foo(); //2

函数与 new

new 仅仅能与一般函数及类搭配应用

函数类型 new
一般函数 新对象
箭头函数 报错
办法 报错
生成器 报错
新对象
异步一般函数 报错
异步箭头函数 报错
生成器函数 报错

语句执行

Completion 类型

依据 try catch finally 语句执行程序能够看到一种景象,try 中有返回值的状况下仍然会在 finally 执行结束后才返回,测试一下

function foo(){
  try{return 0} catch(err) { } finally {console.log("a")
  }
}

console.log(foo());
// a
// 0

tryfinally 都有返回值的状况,会先返回 try 中的值,而后被 finally 中的返回值笼罩,也就是执行了两次 return 操作

function foo(){
  try{return 0} catch(err) { } finally {return 1}
}

console.log(foo());

这一机制的根底正是 JavaScript 语句执行的实现状态,咱们用一个规范类型来示意:Completion Record,它示意一个语句执行完之后的后果,有三个字段:

  • [[type]] 示意实现的类型,有 break continue return thrownormal 几种类型
  • [[value]] 示意语句的返回值,如果语句没有,则是 empty
  • [[target]] 示意语句的指标,通常是一个 JavaScript 标签(标签在后文会有介绍

JavaScript 依附语句的 Completion Record 类型,在语句的简单嵌套构造中,实现了各种管制

语句大略分为以下几种:

一般语句

在 JavaScript 中,咱们把不带控制能力的语句称为一般语句

  • 疏忽 var 和函数申明的预处理机制,一般语句在执行时,从前到后依次执行,没有任何分支或者反复执行逻辑
  • 一般语句执行后,会失去 [[type]] 为 normal 的 Completion Record,JavaScript 引擎遇到这样的 Completion Record,会继续执行下一条语句
  • 只有表达式语句会产生 [[value]],从引擎管制的角度,这个 value 并没有什么用途

Chrome 控制台显示的正是语句的 Completion Record 的 [[value]]

语句块

  • 语句块就是拿大括号括起来的一组语句,它是一种语句的复合构造,能够嵌套
  • 语句块外部的语句的 Completion Record 的 [[type]] 如果不为 normal,会打断语句块后续的语句执行

先看一个一般语句块,对应了给出了每行的 Completion Record,在这个 block 中,每一个语句都是 normal 类型,那么它会依次执行

{
  var i = 1; // normal, empty, empty
  i ++; // normal, 1, empty
  console.log(i) //normal, undefined, empty
} // normal, undefined, empty

退出 return

{
  var i = 1; // normal, empty, empty
  return i; // return, 1, empty
  i ++; 
  console.log(i)
} // return, 1, empty

在 block 中插入了一条 return 语句,产生了一个非 normal 记录,整个 block 会成为非 normal;这个构造就保障了非 normal 的实现类型能够穿透简单的语句嵌套构造,产生管制成果

管制型语句

管制型语句带有 ifswitch 关键字,它们会对不同类型的 Completion Record 产生反馈

  • 管制类语句分成两局部,一类是对其外部造成影响,如 ifswitchwhile/fortry/catch/finally
  • 另一类是对外部造成影响如 breakcontinuereturnthrow,这两类语句的配合,会产生控制代码执行程序和执行逻辑的成果

先来看一下管制语句跟 breakcontinuereturnthrow 四种类型与管制语句两两组合产生的成果

break continue return throw
if 穿透 穿透 穿透 穿透
switch 无效执行 穿透 穿透 穿透
for/while 无效执行 无效执行 穿透 穿透
function 报错 报错 无效执行 穿透
try 非凡解决 非凡解决 非凡解决 无效执行
catch 非凡解决 非凡解决 非凡解决 穿透
finally 非凡解决 非凡解决 非凡解决 穿透

回来看之前例子中的 tryreturn 的组合,依据语句的特点去剖析:

  • finally 中的内容必须保障执行,try/catch 执行结束,失去的后果是非 normal 型的实现记录,也必须要执行 finally
  • finally 执行也失去了非 normal 记录,使 finally 中的记录作为整个 try 构造的后果
function foo(){
  try{return 0} catch(err) { } finally {return 1}
}

console.log(foo());

带标签的语句

语句是能够加标签的,在语句前加冒号即可

firstStatement: var i = 1;

实用场景:与实现记录类型中的 target 相配合,用于跳出多层循环

for(let i=0; i<3; i++){for(let j=0; j<10; j++){console.log(i)
  }
}
// 10 次 0
// 10 次 1
// 10 次 2
outer: for (let i = 0; i < 3; i++) {inner: for (let j = 0; j < 10; j++) {console.log(i)
    if (i === 1) break outer
  }
}
// 10 次 0
// 1

breakcontinue 语句如果后跟了关键字,会产生带 target 的实现记录;一旦实现记录带了 target,那么只有领有对应 label 的循环语句会无效的执行它

参考资料

  • 并发模型与事件循环
  • 嗨,你真的懂 this 吗?
  • 重学前端

正文完
 0