执行上下文栈
js代码执行顺序是什么,大部分人都知道同步执行
var foo = function(){
console.log('foo1');
}
foo(); // foo1
var foo = function(){
console.log('foo2');
}
foo() // foo2
然后看以下下面代码
function foo(){
console.log('foo1');0
}
foo();// foo2
function foo(){
console.log('foo2');
}
foo() // foo2
js 引擎 执行代码是 一段一段分析执行;不是一行一行执行的
当一段代码执行完毕后, 会进行一个准备工作,
比如:第一个例子,变量提升
,第二个例子中, 函数声明提升
准备工作:变量提升,函数提升
Q1:段 是如何划分的?
准备工作是什么时候进行的呢?
可执行代码
这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了?
其实很简单,就三种,全局代码、函数代码、eval代码。
举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(00context)"。
执行上下文栈
接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文
呢?
所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)
来管理执行上下文
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组
ECStack = [];+
假设当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈添加一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
// 模拟将全局执行上下文添加到执行上下文栈中
ECStack = [
globalContext
];
现在 js 引擎 遇到下面的这段代码了:
function foo3() {
console.log('foo3')
}
function foo2() {
foo3();
}
function foo1() {
foo2();
}
foo1();
当执行一个函数的时候,就会创建一个执行上下文,并且添加执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看js引擎如何处理上面这段代码:
// 模拟js引擎执行代码
// foo1()
ECStack.push(<foo1> functionContext);
// foo1中竟然调用了foo2,还要创建foo2的执行上下文
ECStack.push(<foo2> functionContext);
// foo2还调用了foo3!创建 foo3执行上下文
ECStack.push(<foo3> functionContext);
// ECStack= [globalContext,foo1<functionCountext>,foo2<functionCountext>,foo3<funcrtionCountext>]
// foo3执行完毕 foo3执行上下文销毁
ECStack.pop();
// ECStack= [globalContext,foo1<functionCountext>,foo2<functionCountext>]
// foo2执行完毕 foo2执行上下文销毁
ECStack.pop();
// ECStack= [globalContext,foo1<functionCountext>]
// foo1执行完毕,foo1执行上下文销毁
ECStack.pop();
// ECStack= [globalContext]
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
// 关闭浏览器 关闭应用程序
// ECStack.pop()
// ECStack = []
执行上下文
就是代码运行的环境的抽象概念。
1: 作用域链 scope
2: 变量对象 GO AO (预编译)
3: this
执行上下文一旦销毁,以上3个内容都销毁
执行上下文的类型
全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置
this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
Eval 函数执行上下文 — 执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval
,所以在这里我不会讨论它。
执行上下文栈
执行上下文栈就是执行栈,一种具有FILO
first input last output
的数据结构的栈,也可以理解为数组。管理执行上下文
当第一次遇到js代码时候,就会创建一个全局执行上下文
global Context
并压入执行栈底。当每次调用函数时,也会创建一个执行上下文并压入栈中的顶部。
当函数执行完毕之后,当前的执行上下文就会从执行栈中弹出并销毁。
当页面被关闭或程序被结束时候,栈底的全局执行上下文
global Context
被销毁创建执行上下文
两个阶段: 创建阶段和执行阶段
创建阶段:
- 创建变量对象 (创建词法环境,变量环境)
- 建立作用域链 (包括所有的父作用域的变量对象,实则就是多个执行上下文的变量对象形成的链表)
- 确定this指向
执行阶段:
- 完成对所有这些变量的分配,最后执行代码,
注意:在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到
let
变量的值,它会被赋值为undefined
。
确定this指向:
在全局执行上下文中: this 指向 window全局对象
在函数执行上下文中:this指向调用它的引用对象。
特殊情况:
- 在使用
new
关键字创建构造函数并调用时,this指向new
的创建对象- 当call() / apply() / bind() API 调用函数时, this指向参数中的第一个参数对象
- 箭头函数可以指向保留this指向。
变量环境:
变量环境和词法环境差不多。同样是一个词法环境。
不同点: 词法环境被用来存储函数声明和变量(
let
,const
)绑定, 而变量环境只用来存储var
变量绑定。
词法环境 :是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
词法环境中由两个关键: 环境记录器和一个外部环境的引用
环境记录器: 就是存储变量和函数声明的实际位置。
外部环境的引用:它可以访问取父级词法环境 (作用域)
词法环境的类型:
- 全局环境:没有外部环境引用的词法环境,全局环境的外部环境引用是null, 拥有内建的object/Array/等,还有任何用户的全局变量,并且
this
的值指向全局对象window.- 函数环境:函数内部用户定义的变量存储在环境记录器。
环境记录器也有两种类型:
- 声明式环境记录器存储变量、函数和参数
- 对象环境记录器用于定义出现在全局上下文的变量和函数的关系。
注意: 对于函数环境,声明式环境记录器中还包括一个传递给函数的
arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的length
.