Skip to main content

旧-变量对象

执行上下文是如何被创建的? 执行上下文(GEC或FEC)的创建分为两个阶段:

  1. 创建阶段
  2. 执行阶段

JS解析器是如何找到变量的呢? 得对执行上下文有一个进一步的了解。 当调用一个函数时(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。

  1. 创建阶段 在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。

  2. 代码执行阶段 创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。

可以看出详细了解执行上下文极为重要,因为其中涉及到了变量对象,作用域链,this等很多人没有怎么弄明白,但是却极为重要的概念,它关系到我们能不能真正理解JavaScript。

概念1.全局执行上下文,GEC 每当 JavaScript 引擎接收到脚本文件时,它首先会创建一个默认的执行上下文,称为 全局执行上下文 (GEC)

GEC是基础/默认的执行上下文,所有 不在函数内部的JavaScript代码都在这里执行。每一个JavaScript文件只能有一个GEC。

概念2.函数执行上下文,FEC 每当函数被调用时,JavaScript引擎就会在GEC内部创建另一种执行上下文,称为函数执行上下文(FEC),并在FEC中评估和执行函数中的代码。

因为每个函数调用都创建自己的FEC,所以在脚本运行期间会有多个FEC。

1. 创建阶段

在创建阶段,执行上下文首先与执行上下文对象(ECO)相关联。执行上下文对象存储了许多重要的数据,执行上下文中的代码在运行时会使用这些数据。

创建阶段分三个步骤来定义和设置执行上下文对象的属性:

  1. 创建变量对象(VO)
  2. 创建作用域链
  3. 设置 this关键字的值

2. 创建阶段:创建变量对象(VO)

变量对象(VO)是一个在执行上下文中创建的类似于对象的容器,存储执行上下文中变量和函数声明。

在GEC中,每当使用var关键字声明变量,VO就会添加一个指向该变量的属性,并将值设置为"undefined"。初始化相关变量为 undefined.

同时,每当函数声明时,VO就会添加一个指向该函数的属性,并将这个属性存储在内存中。这就意味着在开始运行代码之前,所有函数声明就已经存储在VO中,并可以在VO中访问。

但在FEC中并不创建VO,而是生成一个类数组对象,称为arguments对象,包含传入函数的所有参数。

这种将变量和函数声明存储在内存中优先于执行代码的过程被称为提升。

变量对象的创建,依次经历了以下几个过程。

  1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。

  2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。

  3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。

// var foo = 30;
function foo() { console.log('function foo') }
var foo = 20;

console.log(foo); // 20
// console.log(foo()); // 执行错误
/*
既然变量声明的foo遇到函数声明的foo会跳过,可是为什么最后foo的输出结果仍然是被覆盖了?
因为上面的三条规则仅仅适用于变量对象的创建过程。也就是执行上下文的创建过程。foo = 20是在执行上下文的执行过程中运行的,输出结果自然会是20。
*/
console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;


// 执行顺序为
// 首先将所有函数声明放入变量对象中
function foo() { console.log('function foo') }

// 其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值
// var foo = undefined;

// 然后开始执行阶段代码的执行
console.log(foo); // function foo
foo = 20;

扩展

foo = 10
console.log(foo); // 10
function foo() { console.log('function foo') }
console.log(foo); // 10
var foo = 20;
console.log(foo); // 20

仔细对比这个例子中变量对象从创建阶段到执行阶段的变化,如果你已经理解了,说明变量对象相关的东西都已经难不倒你了。

function test() {
console.log('test1:',foo);
console.log('test2:',bar);

var foo = 'world';
console.log('test3:',foo);
var bar = function () {
return 'say';
}

function foo() {
return 'hello';
}
}

test();
/*
test1: ƒ foo() {
return 'hello';
}
test2: undefined
test3: world
*/

// 创建阶段
VO = {
arguments: {...},
foo: <foo reference>,
bar: undefined
}
// 这里有一个需要注意的地方,因为var声明的变量当遇到同名的属性时,会跳过而不会覆盖

// 执行阶段
VO -> AO
VO = {
arguments: {...},
foo: 'Hello',
bar: <bar reference>,
this: Window
}

3. 创建阶段:创建作用域链

创建完变量对象(VO),紧接着就是执行上下文的创建阶段的下一步——创建作用域链。

JavaScript中的作用域链是一个机制,决定了一段代码对于代码库中其他一些代码来说的可访问性。作用域回答这样一些问题: 一段代码可以在哪里访问? 哪里不能访问? 代码哪些部分可以被访问,哪些部分不能?

每一个函数执行上下文都会创建一个作用域:作用域相当于是一个空间/环境,变量和函数定义在这个空间里,并且可以通过一个叫做作用域查找的过程访问。

也就是说代码被写入代码库的位置,就是这段代码被读取的位置。

如果函数被定义在另一个函数内部,处在内部的函数可以访问自己内部的代码以及外部函数(父函数)的代码。这种行为被称作词法作用域查找。

但外部函数并不能访问内部函数的代码。

作用域的概念就引出了JavaScript另一个相关的现象——闭包。闭包指的是内部函数永远可以访问外部函数中的代码,即便外部函数已经执行完毕