Skip to content

所谓的执行上下文,就是函数调用之前创建的一块内存空间,空间中包含了该函数所需要的所有数据,为该函数的执行提供支持。

此处默认读者已经掌握了栈相关知识,如果读者对栈的概念还不清楚,可以参考数据结构相关课程。

每一个函数运行前都会想执行上下文栈,JS 使用栈这种数据结构来管理这些执行上下文,形成一个执行上下文栈,当一个函数运行的时候,将会把这个函数的执行上下文入栈,运行结束后就会把这个执行上下文出栈。实际上,在其他语言中,这个执行上下文栈一般称作函数调用栈。

一个程序具体的执行过程,或者说执行上下文栈的变化过程,可以参考递归那一块儿,总体来说就一句话,函数执行前会创建一个执行上下文入栈,函数执行结束后执行上下文会出栈,JS 引擎执行的永远是执行上下文栈顶的函数。

在 JS 中,一个函数的执行上下文里面会存放两个东西,分别是 this 指向和变量对象(VO)。

this 关键字是在函数执行的时候确定的,实际上说准确一点,是在函数执行前创建执行上下文的时候确定的,当创建好了执行上下文后,就会把 this 指向放到执行上下文中去,在函数内部访问 this 的时候就会从执行上下文中拿到这个 this 指向。至于 this 指向,这里再复习一次

  • 在全局环境,this 指向全局对象
  • 直接调用函数,this 指向全局对象
  • 通过 new 关键字调用的函数,this 指向新对象
  • 通过对象的某个属性调用,this 指向该对象

当然,call ,apply,bind 可以强制修改 this 指向,这里就不讨论了。

执行上下文更重要的其实是 VO,是变量对象,变量对象记录了该环境中所有声明的参数、变量、函数。也就是说,一个 VO 里包含了参数、变量、函数三个东西。我们直接访问的所有属性和方法实际上都是从 VO 中拿到的。

在全局环境中声明的变量实际上就是全局对象的属性,所以全局环境下所有的属性,都可以看做是全局 VO 的东西,所以在全局环境下的 VO 我们可以直接认为是全局对象。实际上全局环境下的 VO 是很特殊的,它没有参数这个东西,并且它的 VO 指向 window,这和它自己的 this 指向一样。正因为全局环境下的 VO 很特殊,所以有时候我们也称为 GO。

当一个函数执行的时候,他的执行上下文里面会包含 this 和 VO,this 的确定我们已经说过了,关键是 VO,函数执行前的 VO 确定可以有以下三个步骤

  1. 确定函数中所有通过 var 声明的变量,将他们的值设置为 undefined
  2. 确定所有形参值,以及 arguments 引用
  3. 确定函数中通过字面量声明的函数,将他们的值指向函数对象

经过以上三个步骤,这个函数中所有声明的变量,函数和参数,就都放到 VO 对象里面去了,而我们如果直接访问某个函数,本质上就是在访问 VO 里面的属性,这也就是为什么我们可以在函数里面直接访问变量的原因,同时,由于以上三个步骤是在函数声明之前就做好的,这样就可以解释一些问题了。

js
A();
function A(){
	console.log(c);//undefined
	var c = 1;
}

为什么这里可以在声明 c 之前访问 c 呢,实际上就是因为在函数执行的时候就已经创建了 VO,此时 VO 中的 c 就是 undefined,所以这里就会输出 undefined。同样,为什么可以在 A 函数声明之前调用呢?因为在全局环境执行之前,就会先产生 GO,将 A 函数挂载到 GO 上去。

读者可能会想到很早以前学过的变量提升,是的,变量提升是一个很形象的说法,他的本质其实就是执行上下文。

需要注意的是,创建 VO 的三个步骤是有顺序的,也就是说这是会覆盖的

js
var a;
function a(){}
console.log(a);

此时会输出函数 a,因为变量 a 是在第一步中放到 VO 里面去的,但是函数是在第三步放到 VO 中的,此时就覆盖掉了 VO 中的 a。

js
function a(b){
	console.log(b);
	var b = 1;
}
a(123);

同样,在这个例子中,输出的应该是 123,因为第一步将 b 放到 VO,且赋值为 undefined,第二步将参数赋值给 VO,此时 VO 里面已经有了 b 这个变量了,就会直接覆盖,所以就会把 b 赋值为 123。

对于全局环境,由于没有参数这个概念,所以就没有第二步,只有第一步和第三步。

从上面的介绍,读者应该知道,每一个函数执行的时候,都会生成一个执行上下文,然后放到执行上下文栈中,每个执行上下文中,最主要的有 this 和 VO,其中全局环境下的 VO 我们称为 GO。JS 始终只会运行栈顶的执行上下文所对应的函数,我们可以认为栈顶的执行上下文是活跃的,所以,实际上栈顶的 VO 我们也可以称为 AO。

我们对变量和函数的操作实际上都是对函数上下文中的 VO 的操作,如果访问一个当前函数上下文中不存在的属性,则会从之前的上下文依次寻找,一直找到全局上下文,这也是为什么函数内部可以用外部的东西。

js
var b = 1;
function a(){
	console.log(b);
}
a();

此时就会输出 1,因为函数 a 的上下文中不存在 b 这个属性,此时就会往外面找,而外面的 AO 中正好有一个 b,此时就会使用这个 b 变量。

以笔记为镜,可以知得失