上下文
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。 每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。
全局上下文是最外层的上下文,(在浏览器中,全局上下文就是我们常说的 window
对象,因此所有通过 var
定义的全局变量和函数都会成为 window
对象的属性和方法)上下文在其所有代码都执行完毕后会被销毁,包括它上面的变量和函数。
每个函数调用都有自己的上下文。当代码执行流入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数的上下文,将控制权返回给之前的执行上下文。
执行上下文
简单的来说,执行上下文是一种对JavaScript代码执行环境的抽象概念,也就是说,只要有JavaScript代码运行,那么它就一定是运行在执行上下文中,变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。
执行上下文的类型分为三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是
window
对象,this
指向这个全局对象 - 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
Eval
函数执行上下文:指的是运行在eval
函数中的代码,很少用而且不建议使用
🌰 简单举例如下:
紫色框住的部分为全局上下文,蓝色和橘色框起来的是不同的函数上下文。只有全局上下文(的变量)能被其他任何上下文访问
可以有任意多个函数上下文,每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问
生命周期
执行上下文的生命周期包括三个阶段:创建阶段->执行阶段->回收阶段
创建阶段
创建阶段即当函数被调用,但未执行任何内部代码之前
- 确定
this
的值,也被称为This Binding
- 词法环境(LexicalEnvironment)组件被创建
- 变量环境(VariableEnvironment)组件被创建
// 伪代码
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
This Binding
this
的值是在执行时候才能确认,定义时不能确认
词法环境
词法环境有两个组成部分:
- 全局环境:是一个没有外部环境的词法环境,其外部环境引用为
null
,有一个全局对象,this
的值指向这个全局对象 - 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了
arguments
对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境
// 伪代码
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
outer: <null> // 对外部环境的引用
}
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
// 标识符绑定在这里
outer: <Global or outer function environment reference> // 对外部环境的引用
}
}
}
变量环境
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性
在ES6中,词法环境和变量环境的区别在于前者用于存储函数声明和变量(let
和 const
)绑定,而后者仅用于存储变量(var
)绑定
执行阶段
在这阶段,执行变量赋值、代码执行
如果JavaScript引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配undefined
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
执行栈
执行栈,也叫调用栈,后进先出,用于存储在代码执行期间创建的所有执行上下文
当Javascript引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中
每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中
引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文
🌰 举个例子:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
🫱 输出结果:
Inside first function
Inside second function
Again inside first function
Inside Global Execution Context
👇 执行栈中内容用图表示:
作用域链
上下文中的代码在执行时,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。
从执行上下文的角度来说: 作用域链是由当前执行上下文和所有外层执行上下文的变量对象组成的链式结构。当JavaScript代码在一个执行上下文中查找变量时,会先在当前执行上下文的变量对象中查找,如果没有找到,就会继续在外层执行上下文的变量对象中查找,直到找到该变量或者到达全局执行上下文。
如果上下文是函数,则其活动对象用作变量对象,活动对象最初只有一个定义变量:arguments
。
代码正在执行的上下文的变量对象始终位于作用域链的最前端,全局上下文的变量对象始终是作用域链的最后一个变量对象。
var color = "blue";
function changeColor() {
if(color === "blue"){
color = "red";
} else {
color = "blue";
}
}
👆 如上例子,函数 changeColor()
的作用域链包含两个对象:一个是它自己的变量(定义了 arguments
对象),另一个是全局上下文的变量对象。
上图矩形表示不同的上下文。内部上下文可以通过作用域链访问外部上下文中的一切。
上下文之间的连接是线性的,有序的。每个上下文都可以到上一级上下文中去搜索变量和函数。
函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。
作用域
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。——《你不知道的JavaScript》
词法作用域(静态作用域)
作用域共有两种工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,意味着作用域是由书写代码时函数声明的位置来决定的。编译时就基本能够知道全部标识符在哪里以及是如何声明的,从而能够在执行过程中对它们进行查找。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
// 在函数定义的时候确定的作用value等于1 所以调用的时候也等于1
另一种叫作动态作用域,最常见的就是Bash脚本。
#test.sh
name='lily';
function getName() {
echo name: $name;
}
function getMyName() {
local name='lucy';
getName;
}
getMyName;
如上代码,如果是静态作用域,函数的作用域在定义时就确定了,输出应为“lily”,而Bash是动态作用域,所以其实输出的是“lucy”。