调用栈是解析器(如浏览器中的的javascript
解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)LIFO(后进先出)
- 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
- 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
- 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
- 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误。
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
“堆栈溢出“,当你达到调用栈最大的大小的时候就会发生这种情况,而且这相当容易发生,特别是在你写递归的时候却没有全方位的测试它。
并发与事件循环
当调用栈有函数要执行,浏览器就不能做任何事,它会被堵塞住。这意味着浏览器不能渲染,不能运行其他的代码,它被卡住了。如果你想在应用里让 UI 很流畅的话,这就会产生问题。
而且这不是唯一的问题,一旦你的浏览器开始处理调用栈中的众多任务,它可能会停止响应相当长一段时间。大多数浏览器都会这么做,报一个错误,询问你是否想终止 web 页面。
什么是执行上下文?
简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript
代码在运行的时候,它都是在执行上下文中运行。执行上下文的类型
三种执行上下文类型
- 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置
this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 - 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
- Eval 函数执行上下文 — 执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于JavaScript
开发者并不经常使用 eval,所以在这里我不会讨论它。
怎么创建执行上下文?
- 创建阶段
- 执行阶段。
在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事: - this 值的决定,即我们所熟知的
This
绑定。 - 创建词法环境组件。
- 创建变量环境组件。
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
//this绑定
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被
// 对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为
// 没有指定引用对象
词法环境
词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。
- 环境记录器是存储变量和函数声明的实际位置。
- 外部环境的引用意味着它可以访问其父级词法环境(作用域)。
词法环境有两种类型:
- 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是
null
。它拥有内建的Object/Array/
等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。 - 在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
环境记录器也有两种类型:
- 声明式环境记录器存储变量、函数和参数。
- 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。
简而言之,
- 在全局环境中,环境记录器是对象环境记录器。
- 在函数环境中,环境记录器是声明式环境记录器。
注意 — 对于函数环境,声明式环境记录器还包含了一个传递给函数的arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的length
。
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
}
outer: <null>
}
}
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
}
outer: <Global or outer function environment reference>
}
}
变量环境:
它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。
在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let
和const
)绑定,而后者只用来存储var
变量绑定。
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
执行阶段
完成对所有这些变量的分配,最后执行代码。
注意 — 在执行阶段,如果 JavaScript
引擎不能在源码中声明的实际位置找到let
变量的值,它会被赋值为 undefined
javascript事件循环
- 同步任务
- 异步任务
- 同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束’);
1. ajax进入Event Table,注册回调函数success。
2. 执行console.log('代码执行结束')。
3. ajax事件完成,回调函数success进入Event Queue。
4. 主线程从Event Queue读取回调函数success并执行
除了广义的同步任务和异步任务,对任务有更精细的定义:
- macro-task(宏任务):包括整体代码
script,setTimeout,setInterval
- micro-task(微任务):
Promise,process.nextTick
不同类型的任务会进入对应的Event Queue
,比如setTimeout
和setInterval
会进入相同的Event Queue
。
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console’);
1. 这段代码作为宏任务,进入主线程。
2. 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
3. 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
4. 遇到console.log(),立即执行。
5. 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
6. ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
7. 结束。