JavaScript上下文执行过程


变量对象(Variable object)是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete函数标示符形参变量声明等。它们会被挂到这个对象上,对象的属性对应它们的名字,对象属性的值对应它们的值,但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到活动对象。

**对象(Activation object)有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被**,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了。

变量对象就是执行环境中包含了所有变量函数的对象。变量对象是后台的,保存在内存中,代码无法直接访问。可理解为:.js文件(或浏览器<script>标签)内的代码,初始化编译时,扫描到的全局(window下的)变量、作用域(函数 / 闭包),将其加载到内存中,以备调用时查找。


假设在全局环境下定义了函数pub()和变量pubvar:

var pubvar = 1;
console.log(pub(2));    //调用pub()函数

function pub() {
	var pravar = 2;
	return pubvar + pravar;
}

此时后台会创建一个作用域链(scope chain),这个作用域链包含了全局环境的变量对象并被保存在pub()函数内部的scope属性中。但是,当我们打开浏览器的时候已经存在了一个全局的执行环境,这个全局的执行环境属于浏览器,JS里浏览器被称为window对象,我们把这个环境叫做A环境,只要没有关闭浏览器,A环境会一直存在。下面会提到执行环境什么时候会被创建。
我们用色块表示执行环境,链条表示作用域链,作用域链上半部分是活动对象区域,下半部分是变量对象区域,如下图:

JavaScript上下文执行过程

当我们要调用pub()函数的时候,又会创建一个执行环境B,执行环境如其名是在运行和执行代码的时候才存在的,所以我们运行浏览器的时候会创建全局的执行环境。这个时候根据pub()函数scope属性中的作用域链,把pub()函数内的变量对象放入新的B环境中,作用域链也得到更新,如下图:

JavaScript上下文执行过程

上图的虚线表示正在执行,全局变量对象此时处于作用域链的第二位,所以标号变成了1。你可能也注意到那个arguments对象,它是在函数被创建的时候就一直存在的,无需用户创建。arguments对象保存的是函数圆括号内定义的参数,准确来说保存的是参数的值,因这里我们没有设置参数,所以显示未定义。

此时我们把属于B环境的变量对象(也就是pub()函数中的所有函数和变量)叫做活动对象。

因此我们可以说变量对象包含了活动对象,活动对象就是作用域链上正在被执行和引用的变量对象。我们从活动对象的名称中也能看出 “执行、运行、**” 等意味。你可以这样理解,整个代码的运行总有一个起始的对象吧,不管这个起始是变量还是函数,总要有一个称呼,虽然我们把执行环境中的变量和函数的总称叫做变量对象,但这不能反映代码的动态性,为了区别于普通的变量对象,我们创造了活动对象的概念。


上一段我们说到代码的动态性,所谓动态性在这里的意思是有些代码参与了运行,有些没有,就像高中的化学反应一样,总有一些化学物不会参与到整个反应中来。我们把上面的代码变成如下:

var pubvar = 1;
var pubvar2 = 3;
console.log(pub(2));    //调用pub()函数

function pub() {
	var pravar = 2;
	return pubvar + pravar;
}
function pub2() {
	var pravar = 2;
	return pubvar2 + pravar;
}

这个时候全局作用域链和执行环境如下:

JavaScript上下文执行过程

接着我们调用pub()函数,执行环境和作用域链如下:

JavaScript上下文执行过程

没有被调用的pub2()函数仍然只是闲着,甚至没有被pub()函数在内部引用。由于pub2()没有参与整个pub()函数的调用过程,所以pub2()中不存在活动对象,只有“处于静止状态”变量对象,当然也没有创建执行环境。


以上是两个平行且毫不关联的函数其中一个被调用的状况,言下之意就是也存在函数相互影响的例子,最典型的就是闭包,闭包是一种函数嵌套的情况。
定义如下代码:

var savefunc = returnfunc('name');          //调用returnfunc()
var result = savefunc({ name: 'Picasso' });   //调用savefunc()
console.log(result);    //返回字符串“Picasso”

function returnfunc(propertyName) {
	return function (obj) {     // 定义并返回了一个闭包,也被称之为一个匿名函数
		return obj[propertyName];          // 用方括号法访问属性,因为属性是变量(returnfunc()函数的参数)
	};
}

以上代码的最开始的作用域链和执行环境:

JavaScript上下文执行过程

先开始调用returnfunc()函数,马上会创建一个包含returnfunc()变量对象的行环境,作用域链开始变化,如下图:

JavaScript上下文执行过程

图的白色虚线表示执行程序产生的效果,它可能表示的是返回一个结果、复制某种值、产生一个新物体、建立某种联系等。

随后returnfunc()函数会返回它内部的匿名函数,当匿名函数被返回后,整个作用域链和执行环境又发生了变化:

JavaScript上下文执行过程

我们看到匿名函数(闭包)被添加到了最作用域链的最前端,returnfunc()的执行环境被销毁,但我们注意到returnfunc()函数的活动对象仍然在被引用(匿名函数仍在访问propertyName参数),因此returnfunc()函数的变量对象仍然在内存中,成为活动对象。这就是为什么匿名函数就能访问returnfunc()函数定义的所有变量和全局环境定义的变量,毕竟returnfunc()的活动对象仍然保持“**”状态。

题外话:你会发现上图的arguments参数的值和propertyName的值是一样的,这是因为arguments保存的就是参数,采用实时映射的方式与参数建立联系。如果你在returnfunc函数中,再加一个值为{name: 'nicholas'}的入参,那么arguments的值变成{'0': {name: 'Picasso'}, '1': {name: 'nicholas'} },obj的值还是{name: 'Picasso'}。

var result = returnfunc('name')({ name: 'Picasso' }, { name: 'nicholas' });   //调用savefunc()
console.log(result);    //返回"nicholas"
 
function returnfunc(propertyName) {
	return function (obj) {     // 定义并返回了一个闭包,也被称之为一个匿名函数
		console.log(typeof arguments, arguments);    //输出:object { '0': { name: 'Picasso' }, '1': { name: 'nicholas' } }
		return arguments[1][propertyName]; // 用方括号法访问属性,因为属性是变量(returnfunc()函数的参数)
	};
}

是的,你没看错,arguments是一个对象(Object类型),并非数组!!!


根据上面所述,随着代码一行一行的被执行、执行环境不断被创建和销毁、变量对象间的各种关系被建立,这些背后的逻辑导致活动对象也在不断变化,这足以证明活动对象只是正在被执行和引用的变量对象。