前言:
在阅读本文前,我们需要了解:
- 全局环境:整个脚本的最外层环境,包含全局变量和函数。
- 函数环境:每当函数被调用时创建的新环境,包含该函数的局部变量和参数。
1 执行上下文
1.1 什么是 执行上下文?
当 JS 代码开始执行时,JS 引擎首先会创建全局执行上下文,而全局执行上下文中包含全局对象(如浏览器中的window对象),所有的全局变量和函数都会作为这个对象的属性存在。
而当执行时需要调用函数时,就会为其创建函数执行上下文。而这个新的执行上下文会被推入到执行栈(Execution Stack)的顶部,成为当前正在执行的上下文。在函数执行完毕后,其对应的执行上下文会被从执行栈中弹出,控制权返回到之前的上下文。
而执行上下文就可以理解为代码在执行过程中的环境信息。
在 JS 中,运行环境主要包括全局环境和函数环境。 在 JS 代码运行过程中,最先进入的是全局环境,而在函数被调用时再进入相应的函数环境。
在此我们将全局环境和函数环境所对应的执行上下文分别称为全局(执行)上下文和函数(执行)上下文。
执行上下文分为三类,即: 全局上下文、函数上下文、eval上下文。(由于eval(..)不推荐使用,所以就不深入探究了)
1.2 执行上下文 的生命周期:
1.2.1 生命周期的三个阶段:
- 全局上下文的 创建阶段(Creation Phase):
- 包含全局对象:例如在浏览器中是
window,而在 Node.js 中是global。 - 初始化变量:所有声明的全局变量被初始化为
undefined。 - 声明函数:所有函数声明会提升并初始化为对应的函数对象。
this绑定:在全局执行上下文中,this指向全局对象。
- 包含全局对象:例如在浏览器中是
- 全局上下文的 执行阶段(Execution Phase):
- 逐行执行代码,处理变量赋值函数调用。
- 创建 函数上下文:
- 创建阶段:
- 初始化变量对象:参数初始化、函数声明提升、变量声明提升。
- 构建作用域链:包含当前执行上下文的变量对象及其父级执行上下文的变量对象。
- 绑定
this:根据函数的调用来确定this的值。
- 执行阶段:逐行执行代码,处理变量赋值和函数调用等。
- 销毁阶段:在函数执行完毕后进行销毁函数执行上下文。
- 创建阶段:
- 创建 函数上下文:
- 逐行执行代码,处理变量赋值函数调用。
- 全局上下文的 销毁阶段(Destruction Phase):
- 函数执行完毕后销毁执行上下文。
1.2.2 示例代码:
// 全局环境
var a = 1;
function q() {};
function foo(b){
// 函数环境
console.log(a);// 访问全局变量 a
console.log(b);// 访问局部变量 b
let c = 3;
function fun(d){
// 内层函数环境
}
}
foo(2);
根据上述代码我们可以详细解释一下生命周期:
- 全局上下文的创建阶段:
- 创建全局对象
window - 初始化
a为undefined - 提升
foo(..)和q(..)为function(直接console.log(q)可以得到[Function: q]的反馈)
- 创建全局对象
- 全局上下文的执行阶段:
a被赋值为1- 调用
foo(2),进入函数上下文的创建阶段。
- 函数上下文的创建阶段:
- 创建变量对象
VO(foo),包括:- 参数
b初始化为2。 - 局部变量
c初始化为undefined。 - 函数声明
fun(..)提升为函数对象。
- 参数
- 构建作用域链,包含
VO(foo)和全局变量对象。 - 确定
this值(假设普通函数调用,this指向全局对象window)
- 创建变量对象
- 函数上下文的执行阶段:
- 访问
a并打印。 - 访问
b并打印。 - 将
c赋值为3。
- 访问
- 函数上下文的销毁阶段:
- 执行栈弹出
foo的执行上下文,控制权返回到全局执行上下文。
- 执行栈弹出
- 全局上下文的销毁阶段:
- 执行栈弹出全局的执行上下文,程序结束。
2 调用栈(Call Stack)
2.1 调用栈的基本概念
调用栈(也叫执行栈),其工作方式遵循后进先出(LIFO, Last In First Out)的原则,用于存储在执行代码时所创建的所有执行上下文。
首次运行 JS 代码时,会创建一个 全局上下文 并且将其 push(压入) 进当前的 执行上下栈,而每当一个函数被调用时,一个新的函数上下文会被创建并 push 进调用栈的顶部,当函数执行完毕后,相应的执行上下文会被从调用栈中pop(弹出),上下文控制权将移到当前执行栈的下一个执行上下文。
2.2 调用栈的工作原理:
由于我们并不能实际的显示出其如何工作的,所有在此我利用文字+代码+图片来为大家模型执行上下文栈的行为。
假设我们有以下JavaScript代码:
function math() {
console.log(1); //输出 1
function add(a,b){
console.log(a + b);
}
console.log(add(1,2));// 输出 3
}
math();
为了模拟执行上下栈的行为,我们创建一个数组代表为执行上下栈:
var callStack = [];
我们知道在 JS 要开始执行代码时,最开始我们会遇到全局代码,所以我们初始化时,会先向执行上下栈压入一个全局上下文,我们不妨用global表示它,并且由于其后进先出原理,在我们结束代码之前全局上下文都会在栈底。
callStack.push('global');
在压入全局上下文后,我们就会遇到math(..)函数,当我们要执行其时,就会创建一个函数上下文,并且将其push进我们的执行上下栈callStack中,并且在运行结束后pop,所以我们运行代码就会发生以下处理。
// 执行math函数
callStack.push('math');
// 在math中还调用了add,所以我们还要创建add的函数上下文
callStack.psuh('add');
// 在add执行结束后,我们要将其弹出
callStack.pop();
// 执行完math后,同理也应当弹出
callStack.pop();
// 在这之中可以运行无数函数,但是底部的global纹丝不动。
3 变量提升:
3.1 变量提升的概念:
指使用var声明的变量以及函数声明会被提升到作用域顶部,而变量提升的结果,就是可以在变量初始化之前访问该变量(返回undefined),在函数声明前可以调用该函数。
例如:
console.log(a); // undefined
var a = 1;
console.log(a); // 1
在示例中虽然在代码还没执行到var a = 1时就运行console.log(a),但是不会报错,因为在代码执行之前就先进行了变量提升,会返还undefined
同理,函数声明也会被提升:
foo();// Hello world
function foo(){
console.log("Hello world");
}
因为在代码执行之前就对foo()函数进行了声明,所以会正常运行。
3.2 变量提升的本质:
可以理解为在执行 JS 代码之前,需要先解析代码,而在解析的时候会先创建一个全局上下文,会先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。
3.3 注意事项:
let和const不会变量提升,要实现定义好。- 函数声明会提升,而函数表达式不会提升。
---欢迎各位点赞、收藏、关注,如果觉得有收获或者需要改进的地方,希望评论在下方,不定期更新,都看到这里了,真的忍心不点个赞吗~~~