学习并总结 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
- 当调用
bar
时,第一个帧被创立并压入栈中,帧中蕴含了bar
的参数和局部变量 - 当
bar
调用foo
时,第二个帧被创立并被压入栈中,放在第一个帧之上,帧中蕴含foo
的参数和局部变量 - 当
foo
执行结束而后返回时,第二个帧就被弹出栈(剩下bar
函数的调用帧 ) - 当
bar
也执行结束而后返回时,第一个帧也被弹出,栈就被清空了
堆(Heap)
对象被调配在堆中
工作队列(Queue)
- 一个 JavaScript 运行时蕴含了一个待处理的工作队列,每个工作都关联着一个回调函数
- 在事件循环期间时,先进入队列的工作会被优先解决,而后被解决的工作会被移出队列,并作为输出参数来调用与之关联的回调函数
- 调用一个回调函数总是会为其发明一个新的栈帧,函数的解决会始终进行到执行栈再次为空为止,而后事件循环将会解决队列中的下一个工作(如果还有的话)
可视化形容
--引自MDN
事件循环
JavaScript 引擎期待宿主环境分配任务是一个 期待
-> 执行
的过程,周而复始,也就是事件循环
while (queue.waitForTask()) { queue.processNextTask()}
宏工作(MacroTask)、微工作(MicroTask)
- 由宿主环境发动的工作称为宏工作,常见的有:
script
、I/O
、setTimeout
、setInterval
、setImmediate
、requestAnimationFrame
等 - JavaScript 引擎本身发动的工作称为微工作,常见的有:
Promise.then/catch/finally
、async/await
、MutationObserver
、process.nextTick
等 - 宏工作是从头执行一段程序(比方从一个控制台,或在一个
<script>
元素中运行代码)、执行一个事件回调或一个interval/timeout
被触发之类的规范机制而被调度的 JavaScript 代码,这些都在宏工作队列上被调度 在宏工作中还蕴含了如 Promise 等由 JavaScript 引擎发动的微工作,JavaScript 必须保障这些微工作在一个宏工作中实现,所以每一个宏工作中还蕴含了一个微工作队列
- 每当一个宏工作存在,事件循环都会查看该工作是否正把控制权交给其余 JavaScript 代码,如果没有,事件循环就会运行微工作队列中的所有微工作
宏工作和微工作的关系应该是这样的
他们的执行程序应该是这样的
- 执行栈抉择最先进入队列的宏工作(个别都是
script
),执行其同步代码至执行栈为空且控制权尚未返还给用来驱动脚本执行环境的事件循环之前 - 查看是否存在微工作(如
Promise
),有则会执行至微工作,直到队列为空 - 开始下一轮,执行下一个宏工作(
setTimeout
等回调) - 所以微工作总是会先于宏工作
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
剖析异步执行的程序:
- 有多少个宏工作
- 在每个宏工作中,有多少个微工作
- 依据调用秩序,确定宏工作中的微工作执行秩序
- 依据宏工作的触发规定和调用秩序,确定宏工作的执行秩序
- 确定整个程序
函数执行
执行上下文与作用域
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();}// 这里只能拜访colorchangeColor();
以上代码剖析如下:
- 波及3个上下文,全局上下文、
changeColor()
的部分上下文和swapColors()
的部分上下文 - 全局上下文中有一个变量
color
和一个函数chageColor()
changeColor()
的部分上下文中有一个变量anotherColor
和一个函数swapColors()
,但在这里能够拜访全局上下文中的变量color
swapColors()
的部分上下文中有一个变量tempColor
,只能在这个上下文中拜访到。- 全局上下文和
changeColor()
的部分上下文都无法访问到tempColor
- 而在
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(); // globalo.showThis(); // o
调用函数时应用的援用,决定了函数执行时刻的 this
值
- 应用箭头函数
const showThis = () => { console.log(this);}var o = { showThis: showThis}showThis(); // globalo.showThis(); // global
不管用什么援用来调用它,都不影响它的this
值,因为箭头函数体内的this
对象,就是定义时所在的对象,而不是调用时所在的对象
- 应用办法
class C { showThis() { console.log(this); }}var o = new C();var showThis = o.showThis;showThis(); // undefinedo.showThis(); // o
class
外部会默认依照严格模式执行
- 非严格模式和严格模式比照
function fun() { return this; }console.log(fun()); // windowconsole.log(fun.call(2)); // Numberconsole.log(fun.apply(null)); // windowconsole.log(fun.call(undefined)); // windowconsole.log(fun.bind(true)()); // Boolean
"use strict";function fun() { return this; }console.log(fun()); // undefinedconsole.log(fun.call(2)); // 2console.log(fun.apply(null)); // nullconsole.log(fun.call(undefined)); // undefinedconsole.log(fun.bind(true)()); // true
在严格模式下,指定的this
不再被封装为对象,而且如果没有指定this
的话它值是undefined
绑定模式
由绑定优先级从高到低的程序列一下常见的几种绑定模式
new的绑定与实现
new
都做了哪些事件:
- 创立新的空对象,指定原型
- 执行构造函数,并且绑定
this
- 判断构造函数是否返回对象,有就返回此对象
- 构造函数无返回值返回创立的新对象
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
显式绑定
应用 call
、apply
、bind
三种绑定 this
的形式为显式绑定
call
、apply
两个办法参数不同,成果雷同,且都会执行传入的函数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
try
和 finally
都有返回值的状况,会先返回 try
中的值,而后被 finally
中的返回值笼罩,也就是执行了两次 return
操作
function foo(){ try{ return 0 } catch(err) { } finally { return 1 }}console.log(foo());
这一机制的根底正是 JavaScript 语句执行的实现状态,咱们用一个规范类型来示意:Completion Record,它示意一个语句执行完之后的后果,有三个字段:
- [[type]] 示意实现的类型,有
break
continue
return
throw
和normal
几种类型 - [[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 的实现类型能够穿透简单的语句嵌套构造,产生管制成果
管制型语句
管制型语句带有 if
、switch
关键字,它们会对不同类型的 Completion Record 产生反馈
- 管制类语句分成两局部,一类是对其外部造成影响,如
if
、switch
、while/for
、try/catch/finally
等 - 另一类是对外部造成影响如
break
、continue
、return
、throw
,这两类语句的配合,会产生控制代码执行程序和执行逻辑的成果
先来看一下管制语句跟 break
、continue
、return
、throw
四种类型与管制语句两两组合产生的成果
break | continue | return | throw | |
---|---|---|---|---|
if | 穿透 | 穿透 | 穿透 | 穿透 |
switch | 无效执行 | 穿透 | 穿透 | 穿透 |
for/while | 无效执行 | 无效执行 | 穿透 | 穿透 |
function | 报错 | 报错 | 无效执行 | 穿透 |
try | 非凡解决 | 非凡解决 | 非凡解决 | 无效执行 |
catch | 非凡解决 | 非凡解决 | 非凡解决 | 穿透 |
finally | 非凡解决 | 非凡解决 | 非凡解决 | 穿透 |
回来看之前例子中的 try
和 return
的组合,依据语句的特点去剖析:
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
break
、continue
语句如果后跟了关键字,会产生带 target 的实现记录;一旦实现记录带了 target,那么只有领有对应 label 的循环语句会无效的执行它
参考资料
- 并发模型与事件循环
- 嗨,你真的懂this吗?
- 重学前端