--愿你内心有种不灭的火焰,将你与别人区分开来--

0%

变量对象与执行环境

变量对象与执行环境

变量对象 基础数据类型 引用数据类型

JavaScript 的执行上下文生成以后,会创建一个变量对象(Variable Object),JavaScript 的基础数据类型会保存在变量对象中。JavaScript 中有 6 中基本数据类型,Undefined,Null,Boolean,String,Number,Symbol。而基础数据类型是按值访问的,所以我们可以直接操作保存在变量中的值。

而对于引用数据类型的值是保存在堆内存中的对象。JavaScript 不允许直接访问堆内存中的位置,而是通过对象的引用去操作的,而这个引用可以理解为是保存在变量对象中的一个地址。所以,引用类型的值是按引用访问的。

严格意义上来说,变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。

变量对象与堆内存

执行上下文(Execution Context)

执行上下文可以理解为但当前代码的执行环境,它会形成一个作用域。 JavaScript 中的运行环境包括三种情况,全局环境,函数环境,eval。

因此,当程序运行时必然会产生多个执行上下文,JavaScript 引擎会以堆栈的方式来处理他们,而这个堆栈即是 函数调用栈(call stack)。栈底永远是全局上下文,而栈顶永远是当前正在执行的上下文。

我们可以得出关于执行上下文的一些结论:执行上下文是单线程同步执行的,只有栈顶的上下文处于执行状态,其它的需要等待,而每个某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的函数本身。

变量对象与执行上下文

执行上下文是有生命周期的,而它的生命周期可以分为两个阶段:

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

  • 代码执行阶段:
    创建完成以后,开始执行代码,这时会完成变量赋值,函数引用,以及执行其它代码。

    执行上下文的生命周期

深入理解变量对象与声明提升

变量对象的创建,依次执行了如下过程:

(1) 建立 arguments 对象。

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

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

比如,我们一段代码是这样的:

1
2
3
4
5
6
7
8
9
10
// Code One
function test (){
console.log(a)
console.log(foo());
var a = 1;
function foo() {
return 2;
}
}
test();

下面是执行长下文的创建过程:

1
2
3
4
5
6
7
8
9
// 创建阶段
testEC = {
// 变量对象
VO: {
arguments: {...},
foo: <foo reference>,
a: undefined
}
}

这里需要注意的是,未进入执行阶段之前,变量对象中的属性都不能访问。只要进入到执行阶段,等到变量对象变为活动对象之后,里面的属性才能被访问到,然后开始执行执行阶段的操作。

1
2
3
4
5
6
7
// 执行阶段
VO -> AO
VO = {
arguments: {...},
foo: <foo reference>,
a: 1
}

OK,我们从变量对象的角度理解了变量声明提升和函数声明提升的过程,总结下,就是,函数声明提升具备覆盖能力,即是后声明的函数会覆盖之的函数声明,同样会覆盖同名的变量声明。但是我们需要注意的是,一旦变量被初始化,那么便不会存在覆盖问题。

1
2
3
4
5
6
7
8
9
function a(x) {
x && a(--x);
};
var a;
alert(typeof a)
// 'function'
a = 1;
alert(typeof a);
// 'number'

另一种情况是函数表达式,其实函数表达式与变量是一回事儿:

1
2
3
4
5
6
7
console.log(getName())

var getName = function () { console.log(4);};
console.log(getName())

function getName() { console.log(5);}
console.log(getName())

A: 5 4 4
B: 报错 4 4
C: 4 4 5
D: 报错 4 5

JavaScript 的代码的执行分为两个阶段,编译阶段和执行阶段。编译阶段由编译器完成,将代码翻译成为可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文会在该阶段被创建。

作用域可执行上下文 是两个不同的概念,在 JavaScript 中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符(即变量名或者函数名)名称进行变量查找。

它们之间的关系是: 作用域在编译阶段确定规则,作用域链在可执行上下文中被创建。作用域链是对作用域这一套规则的具体实现。

作用域链其实是,由当前环境与上一层环境的一系列变量对象组成,保证了当前执行环境对符合访问权限的变量和函数的有序访问。

再接再厉,我们直接抛出闭包的定义:当函数可以记住并访问所在的作用域时,就产生了闭包。

附加题:

你接触(有贡献过代码)或者实现过的一个完整的系统:

  1. 系统的主要功能有哪些?

  2. 系统的模块划分(代码组织结构)是怎样的,请画一下系统代码组织结构(前后端都有的请分开画)?

  3. 用到了哪些技术栈或者知识点(前后端都有的请分开描述)?

  4. 这个系统中你遇到的最难的或者你最得意的实现点是什么?

暂时性死区

ref

详解js执行环境——声明提升的本质