深入理解JavaScript执行环境与作用域链

作者:代码拾荒时间:2026-10-23 21:34:21浏览:68

本文深入解析JavaScript的执行环境与作用域链机制。详细阐述全局与局部执行环境的创建与销毁过程,解释作用域链如何确保变量访问权限。重点剖析闭包原理,说明函数定义位置决定作用域链的特性,以及闭包对内存占用的影响,帮助开发者写出更可靠的代码。

JavaScript执行环境与作用域链概述

前言

在JavaScript中,执行环境和作用域链是核心概念,它们决定了JS引擎如何处理变量和函数。准确理解这两个概念,有助于我们更好地预测和理解代码的运行行为。

执行环境的定义与生命周期

执行环境

执行环境定义了变量或函数可访问的数据集合。每个执行环境都关联一个变量对象,该环境中定义的所有变量和函数均保存于此对象中。虽然我们无法直接访问这个对象,但解析器在处理数据时会使用它。

通常所说的全局变量,是在最外层执行环境中定义的变量。全局执行环境的具体表示取决于 ECMAScript 的实现:在Web浏览器中,全局执行环境对应window对象,所有全局变量和函数均作为window对象的属性和方法创建;而在Node.js中,全局执行环境则对应global对象。 除了全局执行环境,每个函数都拥有自己的执行环境。当执行流进入函数时,该函数的环境会被推入环境栈;函数执行完毕后,栈会弹出该环境,将控制权交还给之前的执行环境。这意味着,当某个执行环境中的代码全部执行完毕,该环境即被销毁,其中保存的所有变量和函数定义也随之消失。全局执行环境仅在应用程序退出(如网页或浏览器关闭)时才被销毁。

作用域链的构建与变量查找

作用域链

前文提到,每个执行环境都有一个变量对象来保存其中定义的变量和函数。由于环境是层层嵌套的,当代码进入新环境执行时,会创建一个变量对象的作用域链,将嵌套执行环境之间的变量对象建立有序联系。作用域链的主要作用是确保当前执行环境有权访问的变量和函数,并按序进行查找。作用域链的最前端始终是当前正在执行的代码所处执行环境的变量对象;若该环境是函数,则其活动对象作为变量对象,在函数未定义新变量时,该活动对象即为函数的arguments 对象。作用域链的下一个变量对象来自当前执行环境的包含环境,依此类推,逐层嵌套,直至全局执行环境;全局执行环境的变量对象始终是作用域链中的最后一个对象。

当代码执行时,遇到的每个标识符解析都会沿作用域链逐级搜索,从作用域链前端(当前执行环境的变量对象)向后回溯,直到找到该标识符。若在作用域链上未找到该标识符,通常会导致错误。我们在浏览器中常遇到的 Uncaught ReferenceError: x is not defined 正是这一错误的表现。

JS解释器在执行时会对变量和函数进行声明提前。在声明函数时,会给函数一个[[scope]]属性,该属性包含当前函数所有包含环境的变量对象,即函数在声明提前时已生成其包含环境的作用域链。随后,当函数执行时,会将自己的 arguments 和内部定义的函数及变量打包成一个变量对象,添加到 scope chain 的末尾。

函数参数也被视为变量,因此其访问规则与执行环境中的其他变量相同。

作用域链的特性直观易懂,但在实际代码中,由于情况复杂,某些行为可能反直觉或易产生误解。例如以下情况:

作用域链与函数定义位置的关系

作用域链看的是函数定义的位置而不是执行的位置


var x = 10
bar()
function foo() {
 console.log(x)
}
function bar(){
 var x = 30
 foo()
}

在此例中,有人可能误以为 bar() 会输出 30。理解这一点的关键在于:函数保存在堆中,函数名只是指向堆中地址的引用。执行函数时,根据该引用去堆中查找对应函数并执行。因此,无论在哪里执行函数,函数的位置不变,作用域链的确定也不取决于函数在哪里执行,而是取决于函数在哪里定义。作用域链可视为在函数声明时即已生成。

个人认为,ECMAScript 这样处理作用域链是为了保持其不变性,避免持续维护,并根据环境嵌套保持一致性。

闭包原理与内存管理

闭包

除了全局执行环境的变量对象始终存在外,其他局部函数的变量对象仅在函数执行过程中存在。通常,函数执行完毕后,局部活动对象即被销毁,内存中仅保留全局执行环境的变量对象。但闭包的情况不同。

闭包是指有权访问另一个函数作用域中变量的函数,如下例所示:


 function outer(){
   var scope = "outer";
   return function (){
    return scope;
   }
 }
var fn = outer();
fn();

在函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到其作用域链中。因此,在outer函数内部定义的匿名函数(下文称为inner 函数)的作用域链中,实际上包含了外部函数outer() 的活动对象。下图展示了代码执行时,outer 和 inner 函数的作用域链。

JavaScript执行环境与作用域链概述

当匿名函数从outer()中被返回后,inner() 函数仍可访问在outer() 中定义的所有变量。也就是说,当outer() 函数执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍在引用该活动对象。换言之,当outer() 函数执行完毕返回后,其执行环境和作用域链虽被销毁,但其活动对象仍保存在内存中。如果匿名函数不销毁,则该活动对象将一直存在于内存中。

JS中的对象均保存在堆中,代码中书写的是对对象的引用,作用域链亦然。因此,上述 outer() 函数执行完毕后,作用域链虽被销毁但对象仍存在,其实销毁的只是引用。JS的垃圾回收机制采用引用计数策略,当某个变量或对象的引用次数为0时,内存会被收回。outer 函数的变量对象有两个引用:一个是outer 的作用域链,另一个是匿名函数的作用域链。因此,只要匿名函数不被销毁,该引用就持续存在,outer() 的活动对象也会一直存在。

轮子哥在知乎提供了一个易于理解的说法:“闭”的意思并非封闭内部状态,而是封闭外部状态。一个函数如何能封闭外部状态?当外部状态的scope失效时,它自己还保留了一份。

JavaScript执行环境与作用域链概述

由于闭包会携带包含它的函数的作用域,因此会避免其他函数占用更多内存。过度使用闭包可能导致内存占用过多,因此仅在必要时使用闭包。

总结:作用域在JS中的核心地位

总结

任何编程语言都有作用域概念。程序围绕变量操作,因此设计语言时,变量如何存储、存储到哪里、程序如何找到对应变量是首要解决的问题。作用域即语言设计者针对此问题制定的一套良好规则,用于存储和搜索对象,这就是作用域的概念。而JS中的这一规则即为作用域链。在编写程序时,我们需要知道变量(及函数)如何存储,以及JS引擎在遇到标识符解析时按何种规则搜索变量或函数,只有这样,我们才能写出更可靠的代码。

以上就是JavaScript中的执行环境和作用域链的详细内容,更多关于JavaScript执行环境和作用域链的资料请关注本站其它相关文章!

作者最新文章