JavaScript执行环境与作用域机制深度解析

作者:半夏

时间:2026-10-30

浏览:438

本文详解JavaScript的执行环境分类、作用域链构建过程及变量查找机制。重点解析全局与函数执行环境的区别、[[scope]]属性的形成、活动对象的作用,以及变量提升对代码执行结果的影响,帮助开发者深入理解JS内存管理与作用域规则。

文章目录

JavaScript执行环境概述

近期重读《JavaScript高级程序设计(第3版)》后,决定通过博客整理学习笔记,以防知识遗忘。本文重点总结JavaScript的执行环境与作用域相关概念。

首先介绍执行环境的基本概念。

一、执行环境概述 根据书中定义,执行环境(Execution Environment)决定了变量或函数可以访问哪些数据,并规定了它们各自的行为。每个执行环境都关联着一个变量对象(Variable Object)。在该环境中定义的所有变量和函数均存储于此对象中。尽管开发者在编码时无法直接访问该对象,但解析器在处理数据时会在后台依赖它。

执行环境是一个抽象概念和运行机制,用于界定变量或函数对其他数据的访问权限。

在JavaScript中,可执行的代码主要分为三类: 1. 全局代码(Global Code):指位于任何函数外部的代码,例如独立的JS文件或嵌入在HTML页面中的脚本。 2. 评估代码(Eval Code):指通过eval()函数动态执行的JavaScript代码。 3. 函数代码(Function Code):指用户自定义函数体内的JavaScript代码。

此处暂不讨论Eval Code,仅聚焦于全局执行环境和函数执行环境。

1、全局执行环境:

全局执行环境是位于最外层的执行环境,在JavaScript中通常对应window对象。因此,所有全局变量和函数均作为window对象的属性和方法创建。当代码加载至浏览器时,全局执行环境随之创建;只有当网页关闭或浏览器退出时,该环境才会被销毁。例如,页面首次加载JS代码时,会创建一个全局执行环境。

这也解释了为什么闭包可能导致内存泄漏:因为闭包中的外部函数被视作全局环境的一部分,无法被垃圾回收机制销毁,从而一直驻留在内存中。

2、函数执行环境

每个函数都拥有独立的执行环境。当执行流进入一个函数时,该函数的执行环境会被压入执行环境栈(Execution Context Stack)的顶部并获得执行权。函数执行完毕后,其执行环境从栈顶弹出,并将执行权交还给之前的执行环境。这就是ECMAScript中的执行流机制。

另一种解读方式是:调用JavaScript函数时,该函数进入其对应的执行环境。若函数内部又调用了其他函数,则会创建新的执行环境,且函数调用期间的执行过程均处于该新环境中。当被调用的函数返回后,执行过程回到原始执行环境。因此,运行中的JavaScript代码构成了一个执行环境栈。

当函数被调用时,会创建其局部环境(函数内代码执行完毕后,该环境及其包含的所有变量和函数定义均被销毁)。

2-1 定义期

在函数定义阶段,系统会创建一个[[scope]]属性。该属性对应一个对象列表,列表中的对象仅能被JavaScript内部访问,无法通过语法直接访问。

(scope即作用域。)

若定义一个全局函数A,则A会创建一个[[scope]]属性。此时,[[scope]]内仅包含全局对象(Global Object)。

若在函数A内部定义函数B,则B也会创建自己的[[scope]]属性。B的[[scope]]包含两个对象:A的活动对象(Activation Object)和全局对象,其中A的活动对象位于列表前端,全局对象位于后端。

简而言之,函数[Scope]属性中对象列表的顺序为:上一层函数的Activation Object对象,接着是上上层的,直至最外层的全局对象。

以下是示例代码:A只有一个scope,B有两个scope


// 外部函数
function A(){
  var somevar;
  
  // 内部函数
 function B(){
   var somevar;
  }
}

2-2 执行期

当函数被执行时,即进入该函数的执行环境。首先会创建其自己的活动对象(Activation Object),该对象包含this、参数(arguments)、局部变量(包括命名参数)的定义以及一个变量对象的作用域链([[scope chain]])。随后,将该执行环境的[scope]按顺序复制到[[scope chain]]中,最后将活动对象推入[[scope chain]]的顶部。这样,[[scope chain]]便成为一个有序的栈,确保了对执行环境有权访问的所有变量和对象的有序访问。


// 第一步页面载入创全局执行环境global executing context和全局活动象
// 定义全局[[scope]],只含有Window对象
// 扫描全局的定义变量及函数对象:color【undefined】、changecolor【FD创建changecolor的[[scope]],此时里面只含有全局活动对象】,加入到window中,所以全局变量和全局函数对象都是做为window的属性定义的。
// 程序已经定义好所以在此执行环境内任何位置都可以执行changecolor(),color也已经被定义,但是它的值是undefined

// 第二步color赋值"blue"
var color = "blue";

// 它是不需要赋值的,它就是引用本身
function changecolor() {
 // 第四步进入changecolor的执行环境
 // 复制changecolor的[[scope]]到scope chain
 // 创建活动对象,扫描定义变量和定义函数,anothercolor【undefined】和swapcolors【FD创建swapcolors的[[scope]]加入changecolor的活动对象和全局活动对象】加入到活动对象,活动对象中同时还要加入arguments和this
 // 活动对象推入scope chain 顶端
 // 程序已经定义好所以在此执行环境内任何位置都可以执行swapcolors(),anothercolor也已经被定义好,但它的值是undefined
 
 // 第五anothercolor赋值"red"
 var anothercolor = "red";
 
 // 它是不需要赋值的,它就是引用本身
 function swapcolors() {
  // 第七步进入swapcolors的执行环境,创建它的活动对象
  // 复制swapcolors的[[scope]]到scope chain
  // 扫描定义变量和定义函数对象,活动对象中加入变量tempcolor【undefined】以及arguments和this
  // 活动对象推入scope chain 顶端
  
  // 第八步tempcolor赋值anothercolor,anothercolor和color会沿着scope chain被查到,并继续往下执行
  var tempcolor = anothercolor;
   anothercolor = color;
   color = tempcolor; 
 }

 // 第六步执行swapcolors,进入其执行环境
 swapcolors();
}

// 第三步执行changecolor,进入其执行环境
changecolor();

2-3 标识符访问:

在执行JavaScript代码过程中,若遇到标识符,系统会根据标识符名称在执行上下文(Execution Context)的作用域链中进行搜索。搜索从作用域链的第一个对象(即该函数的Activation Object对象)开始,若未找到,则继续搜索作用域链中的下一个对象,如此往复,直到找到标识符的定义。若在搜索完作用域中的最后一个对象(即全局对象Global Object)后仍未找到,则会抛出一个错误,提示undefined。

作用域链机制

二、Scope/Scope Chain(作用域/作用域链)

当代码在某个环境中执行时,会创建一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。整个作用域链是由不同执行位置上的变量对象按照规则构建的一个链表。作用域链的最前端,始终是当前正在执行的代码所在环境的变量对象。

若该环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在初始时仅包含一个变量,即函数内部的arguments对象。作用域链中的下一个变量对象来自该函数的包含环境,而再下一个变量对象来自再下一个包含环境。如此延续至全局执行环境,全局执行环境的Variable Object始终是作用域链中的最后一个对象。

如图所示:

JavaScript执行环境概述

书中示例:


 var color="blue";
 function changecolor(){
 var anothercolor="red";
 function swapcolors(){
 var tempcolor=anothercolor;
 anothercolor=color;
 color=tempcolor;
  // Todo something  
  }
 swapcolors();
}
changecolor();
 //这里不能访问tempcolor和anocolor;但是可以访问color;
alert("Color is now "+color);

通过上述分析可知,内部环境可以通过作用域链访问所有外部环境,但外部环境无法访问内部环境中的任何变量和函数。

这些环境之间呈线性、有次序的关系。每个环境均可向上搜索作用域链以查询变量和函数名;但任何环境不能通过向下搜索作用域链进入另一个执行环境。

对于上述示例中的swapcolor()函数而言,其作用域链包括:swapcolor()的变量对象、changecolor()变量对象和全局对象。swapcolor()的局部环境首先在自己的Variable Object中搜索变量和函数名,若找不到,则向上搜索changecolor作用域链……以此类推。然而,changecolor()函数无法访问swapcolor中的变量。

启示:尽量使用局部变量,以减少搜索时间。

块级作用域与变量声明

1、没有块级作用域

与C、C++以及Java不同,JavaScript没有块级作用域。参考以下代码:


if(true){
  var myvar = "张三"; 
 }
 alert(myvar);// 张三

假设有块级作用域,外部将无法访问myvar。再看以下代码:


for (var i=0;i<10;i++){
   console.log(i) 
  }
  
  alert(i); // 10

对于具有块级作用域的语言(如Java或C#),i作为for循环初始化的变量,在for循环外部是不可访问的。因为i仅存在于for循环体内,循环运行结束后,for中的所有变量即被销毁。而在JavaScript中并非如此,for循环中的变量声明会被添加到当前执行环境(此处为全局执行环境)中。因此,在for循环结束后,变量i依然存在于循环外部的执行环境中,故输出结果为10。

2、声明变量

使用var声明变量时,该变量将自动添加到距离最近的可用环境中。在函数内部,最近的环境即为函数的局部变量。若初始化变量时未使用var,该变量将自动添加到全局函数中。

代码如下:


var name = "小明";
function getName(){
 alert( name ); //'undefined'
 var name = '小黄';
 alert(name ); //小黄
}
getName()

为何第一个name显示为undefined?这是因为JavaScript解析器在进入一个函数执行环境时,会先对var和function进行扫描。

这相当于将var或function【函数声明】的声明提升到执行环境的顶部。

也就是说,当进入getName函数时,标识符查找机制查找到var,此时查找的name是局部变量name,而非全局的name,因为函数内的name已被提升至顶部。

上述代码会被解析为如下形式:


var name = "小明";
function getName(){
 var name;
 alert( name ); //'undefined'
 var name = '小黄';
 alert(name ); //小黄
}
getName()
 

延长作用域链:

尽管执行环境只有两种——全局作用域和函数作用域,但仍可通过特定方式延长作用域链。因为某些语句可以在作用域链的顶部增加一个临时的变量对象。 有两种情况会发生这种现象: 1. try-catch语句的catch块; 2. with语句;

以上就是本文的全部内容,希望对大家学习理解JavaScript执行环境及作用域有所帮助。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章