深入解析JavaScript执行模型:活动对象、作用域链与变量实例化

作者:纸飞机

时间:2026-10-25

浏览:207

本文深入解析JavaScript引擎的执行模型,详细阐述函数执行环境的创建过程,包括活动对象(Activation Object)的生成、arguments对象的绑定以及作用域链的分配。重点解析变量实例化阶段对函数声明与变量声明的不同处理,揭示函数声明提升与变量提升导致的执行差异。同时对比全局执行环境与Eval执行环境的可变对象机制,并通过嵌套函数调用实例,逐步演示作用域链的构建、变量查找及内存回收过程,帮助开发者理解JavaScript底层运行机制。

文章目录
函数运行上下文

函数执行环境详解

首先观察一段基础代码示例:


代码示例

具体代码实现如下:

 
function say(msg,other){
var str = "nobody say:";
this.name = '笨蛋的座右铭';
function method(){};//var method = function(){};
alert(str+msg);
} 
say('hello world');
alert(name);//输出:笨蛋的座右铭

当调用say函数时,引擎首先创建其执行环境。在构建该环境的过程中,会按定义顺序依次执行以下操作:


1. 创建‘活动对象’(Activation Object)。这是规范定义的一种特殊机制。它之所以被称为对象,是因为它包含可访问的命名属性;但它不同于普通对象,没有预定义的原型链,且无法通过JavaScript代码直接引用。


2. 创建arguments对象。这是一个类数组对象,其整数索引成员与调用时传递的参数一一对应。该对象包含length和callee属性(详细机制参见《理解Javascript_14_函数形式参数与arguments》)。随后,在活动对象上创建名为“arguments”的属性,指向该arguments对象。


3. 分配作用域。作用域由对象列表(即作用域链)构成。(具体机制较复杂,参见:《理解Javascript_15_作用域分配与变量访问规则,再送个闭包》)


4. 执行ECMA 262定义的‘变量实例化’(Variable Instantiation)过程。此时,函数的形式参数会被创建为活动对象的命名属性。若调用时传递了参数,则将这些值赋给对应属性;否则赋值为undefined。对于内部函数声明,以其名称为活动对象创建同名属性,并将函数对象赋值给该属性。最后,将函数内部声明的所有局部变量作为活动对象的命名属性。注意:在此阶段,除已赋值的实际参数和函数定义外,其他变量均被‘预解析’为undefined。


5. 最后处理this关键字的赋值。此时this指向全局对象(即window)。



执行环境创建完成后,进入第二步:在函数体内,从上至下逐行执行代码。


1. 当执行到var str='nobody say'时,触发‘计算赋值表达式’过程。此时,活动对象中key为str的值从undefined更新为'nobody say:'。


2. 执行到this.name='笨蛋的座右铭'时,为this指向的对象添加name属性,并赋值为'笨蛋的座右铭'。


3. 接着执行function innerMethod(){}(注:原文此处函数名与定义不符,按原文逻辑处理),最后执行'alert(str+msg)',输出'nobody say:hello world'。



函数声明与函数表达式赋值的差异

函数声明与变量声明的区别

再看一个简单示例:


代码示例

代码如下:

 
function say(){
method01();//调用method01
method02();//报错
function method01(){
alert('method01');
} 
var method02 = function(){
alert('method02');
} 
} 
say();

为何method01调用成功而method02调用失败?

首先需明确:method01是函数声明创建的函数对象,而method02是变量,指向另一个函数对象。根据前文‘变量实例化’机制,method01在预解析阶段被正常处理,而method02在预解析阶段被赋值为undefined。当代码执行到method02()调用时,由于此时尚未执行var method02 = function(){...}这一表达式计算,method02仍为undefined,将其作为方法调用必然报错。解决方案是将var method02=function(){...}移至method02()调用之前,或使用函数声明方式定义函数(function method(){...})。


注:‘计算函数表达式’指程序执行到var method02 = function(){...}语句。此时method02才真正指向函数对象。因为在预解析阶段,method02仅被赋予undefined值。



全局运行上下文(前文已述,此处略过)

全局执行环境机制

在页面首次加载JS代码时,会创建一个全局执行环境。全局执行环境的作用域链仅包含一个对象,即全局对象(window)。在JavaScript代码执行前,引擎会预先创建好Scope Chain结构。全局执行环境同样存在变量实例化过程,其内部函数即为大部分JavaScript代码涉及的常规顶级函数声明。在变量实例化过程中,全局对象充当可变对象,这也是全局声明的函数成为全局对象属性的原因。全局声明的变量同理,全局执行环境使用this对象引用全局对象。



注:需区分‘函数执行环境’中的活动对象(Activation Object)与全局执行环境中的可变对象(Variable Object)。由于存在差异,规范中重新命名以示区别:全局执行环境/Eval执行环境中称为Variable Object,函数执行环境中称为Activation Object。



Eval运行上下文

Eval执行环境分析

构建Eval执行环境时的可变对象(Variable Object)即为调用eval时当前执行上下文中的可变对象。在全局执行环境中调用eval,其可变对象为全局对象;在函数中调用eval,其可变对象为函数的活动对象。


代码示例

代码示例如下:

 
//Passed in FF2.0, IE7, Opera9.25, Safari3.0.4
function fn(arg){
var innerVar = "variable in function";
eval(' \
var evalVar = "variable in eval"; \
document.write(arg + "<br />"); \
document.write(innerVar + "<br />"); \
'); 
document.write(evalVar);
} 
fn("arguments for function");

运行结果如下:

输出:arguments for function


输出:variable in function


输出:variable in eval


说明:eval调用中可以访问函数fn的参数和局部变量;在eval中定义的局部变量在函数fn中也可访问,因为它们的Variable Object是同一个对象。


进入Eval代码执行时,会创建一个新的Scope Chain,其内容与当前执行上下文的Scope Chain完全一致。



最终案例

嵌套函数执行过程实例

以下是一个综合示例:


代码示例

代码如下:

 
var outerVar1="variable in global code";
function fn1(arg1, arg2){
var innerVar1="variable in function code";
function fn2() { return outerVar1+" - "+innerVar1+" - "+" - "+(arg1 + arg2); }
return fn2();
} 
var outerVar2=fn1(10, 20);

大致执行流程如下:

1. 初始化Global Object(即window对象),Variable Object为window对象本身。创建Scope Chain对象,假设为scope_1,其中仅包含window对象。


2. 扫描JS源代码(读入源代码,可能包含词法语法分析过程),从中提取定义的变量名和函数对象。按扫描顺序:


2.1 发现变量outerVar1,在window对象上添加outerVar1属性,值为undefined;


2.2 发现函数fn1的定义,基于此定义创建函数对象,传给创建过程的Scope Chain为scope_1。将结果添加到window的属性中,名称为fn1,值为返回的函数对象。注意fn1的内部[[Scope]]即为scope_1。另外需注意,创建过程不会对函数体内的JS代码做特殊处理,可理解为仅将函数体JS代码的扫描结果保存在函数对象的内部属性上,在函数执行时再做进一步处理。这对理解Function Code,尤其是嵌套函数定义中的Variable Instantiation至关重要;


2.3 发现变量outerVar2,在window对象上添加outerVar2属性,值为undefined;


3. 执行outerVar1赋值语句,赋值为"variable in global code"。


4. 执行函数fn1,得到返回值:


4.1 创建Activation Object,假设为activation_1;创建新的Scope Chain,假设为scope_2,scope_2中第一个对象为activation_1,第二个对象为window对象(取自fn1的[[Scope]],即scope_1的内容);


4.2 处理参数列表。在activation_1上设置属性arg1、arg2,值分别为10、20。创建arguments对象并进行设置,将arguments设置为activation_1的属性;


4.3 对fn1的函数体执行类似步骤2的处理过程:


4.3.1 发现变量innerVar1,在activation_1对象上添加innerVar1属性,值为undefined;


4.3.2 发现函数fn2的定义,基于此定义创建函数对象,传给创建过程的Scope Chain为scope_2(即函数fn1的Scope Chain,为当前执行上下文的内容)。将结果添加到activation_1的属性中,名称为fn2,值为返回的函数对象。注意fn2的内部[[Scope]]即为scope_2;


4.4 执行innerVar1赋值语句,赋值为"variable in function code"。


4.5 执行fn2:


4.5.1 创建Activation Object,假设为activation_2;创建新的Scope Chain,假设为scope_3,scope_3中第一个对象为activation_2,后续对象依次为activation_1、window对象(取自fn2的[[Scope]],即scope_2);


4.5.2 处理参数列表。因fn2无参数,仅创建arguments对象并设置为activation_2的属性。


4.5.3 对fn2的函数体执行类似步骤2的处理过程,未发现变量定义和函数声明。


4.5.4 执行函数体。对任何变量引用,从scope_3上进行搜索。此示例中,outerVar1将在window上找到;innerVar1、arg1、arg2将在activation_1上找到。


4.5.5 丢弃scope_3、activation_2(指它们可被垃圾回收)。


4.5.6 返回fn2的返回值。


4.6 丢弃activation_1、scope_2。


4.7 返回结果。


5. 将结果赋值给outerVar2。



参考资料:


http://www.cnblogs.com/RicCC/archive/2008/02/15/JavaScript-Object-Model-Execution-Model.html


http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.html

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章