前言
在之前的文章详细介绍了作用域和预编译,作用域链和这两者都有着紧密的联系。JavaScript 的作用域链(Scope Chain)是一个重要的概念,它决定了在代码执行过程中如何查找变量和函数。理解作用域链需要了解 JavaScript 中的词法作用域、执行上下文和作用域链的关系。
执行上下文
- 在 JavaScript 中,执行上下文(Execution Context)是代码执行时的环境。每当 JavaScript 代码运行时,都会创建一个执行上下文。
- 每个执行上下文都有自己的变量对象(Variable Object),用于存储变量、函数和参数。
- 在 JavaScript 中,执行上下文对象在以下情况下会被创建:
-
全局执行上下文:当 JavaScript 程序开始执行时,会创建一个全局执行上下文,代表整个 JavaScript 文件的运行环境。这是代码中的最顶层执行上下文。
-
函数调用:每次调用函数时,都会创建一个新的执行上下文对象。这意味着每个函数调用都有自己的执行上下文,其中包含了函数的局部变量、参数以及在函数内部声明的其他内容。
-
Eval 函数 :当使用
eval()
函数执行动态代码时,也会创建一个执行上下文对象。
- 在预编译的时候会创建执行上下文对象和可执行的代码两部分,其中执行上下文里包含变量环境和词法环境,如下图所示:
js
var a = 2;
function add(){
var b = 10;
return a + b;
}
add();
上面一段代码在预编译时先创建一个全局执行上下文,如下图所示,a的值在预编译时被赋值为undefined,在执行代码时被赋值为2
当执行到函数add的调用时,会创建一个add执行上下文,如下图所示:
而执行上下文是通过什么管理的呢,这就不得不提到栈结构了,JS引擎就是通过栈结构来管理执行上下文的。
调用栈
- 栈结构:特殊的数组,后进先出
下面的一段代码你知道会打印出来的结果是什么吗?
js
function foo(){
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a);//1
console.log(b);//3
}
console.log(b);//2
console.log(c);//4
console.log(d);//error
}
foo();
在执行上下文中查找值是先在词法环境中找,在词法环境中不可以找到值再去变量环境中找值,这也是为什么第9行代码中打印出来的结果是3,因为词法环境里面是一个栈结构,在foo函数中先声明let b = 2
,先将这个b压入栈底,后面声明let b = 3
,再将其压入栈中,查找时是从上往下,所以查找出来b的值为3。
- 调用栈: JavaScript 中的调用栈是一个用于管理函数调用的数据结构,它遵循后进先出(LIFO)的原则。每当执行一个函数时,其对应的执行上下文会被推入调用栈,当函数执行完成后,执行上下文会被弹出。这个过程反复进行,直到调用栈为空。
举个例子:
javascript
function firstFunction() {
console.log("Inside firstFunction");
secondFunction();
}
function secondFunction() {
console.log("Inside secondFunction");
thirdFunction();
}
function thirdFunction() {
console.log("Inside thirdFunction");
}
firstFunction();
在执行这段代码时,调用栈的变化如下:
- 程序开始执行时,创建全局执行上下文,并将其推入调用栈中。
firstFunction
被调用,创建firstFunction
的执行上下文,并将其推入调用栈中。- 在
firstFunction
中调用secondFunction
,创建secondFunction
的执行上下文,并将其推入调用栈中。 - 在
secondFunction
中调用thirdFunction
,创建thirdFunction
的执行上下文,并将其推入调用栈中。 thirdFunction
执行完毕,其执行上下文从调用栈中弹出。secondFunction
执行完毕,其执行上下文从调用栈中弹出。firstFunction
执行完毕,其执行上下文从调用栈中弹出。- 程序执行完毕,全局执行上下文从调用栈中弹出。
在这个过程中,调用栈记录了函数调用的顺序和当前的执行位置。如果函数调用嵌套太深,或者出现了递归调用没有终止条件,就可能导致调用栈溢出错误(stack overflow)。
- 栈溢出: 调用栈的内存超出限制
作用域链
在 JavaScript 中,每个函数都会创建一个新的执行上下文,每个执行上下文都有自己的变量对象。当函数被调用时,会创建一个新的执行上下文,并将其推入执行上下文栈(Execution Context Stack)中,形成一个执行上下文的链式结构,即作用域链。
js
function bar(){
console.log(myName);// 结果为Jerry
}
function foo(){
var myName = 'Tom';
bar();
}
var myName = 'Jerry';
foo();
在作用域链中,outer 指向的是外部(上一级)执行上下文的作用域。这与词法作用域有关,bar函数声明在了全局作用域里,所以bar的outer指向全局执行上下文,同理,foo函数的声明也在全局作用域里,所以foo的outer也指向全局执行上下文。所以最终的打印结果是Jerry。
- 作用域链并不是在调用栈中从上到下查找,而是看当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,我的词法作用域在哪里,outer就指向哪里。
- 词法作用域: 函数定义是所在时所在的作用域
结语
理解作用域链对于理解 JavaScript 中的变量作用域和作用域嵌套非常重要,这对于编写可维护、可扩展的代码至关重要。希望这篇文章可以给你带来帮助。