深入解析JavaScript作用域与作用域链机制

作者:小括号

时间:2026-10-29

浏览:96

本文深入解析JavaScript的作用域与作用域链机制,详细阐述全局与局部作用域的区别,剖析函数创建与执行时的作用域链构建过程。文章重点讨论作用域链对性能的影响,提供通过局部变量优化全局访问、合理使用with与catch语句的具体策略,帮助开发者编写更高效、规范的JavaScript代码。

文章目录

JavaScript作用域基础

作用域是JavaScript核心概念之一,深入理解其工作原理及作用域链机制,是掌握JavaScript的关键。本文将简要介绍JavaScript作用域与作用域链的基础知识,旨在协助读者更好地学习和应用JavaScript。

所有编程语言都具备作用域的概念,其本质是变量与函数的可访问范围。作用域决定了变量和函数的可见性及生命周期。在JavaScript中,变量的作用域主要分为全局作用域和局部作用域两类。

1. 全局作用域(Global Scope)

全局作用域意味着在代码的任何位置均可访问该对象。通常情况下,以下几种情况会形成全局作用域:

(1)最外层的函数以及在该函数外部定义的变量拥有全局作用域,具体示例如下:


var authorName="小平果";
function doSomething(){
 var blogName="你不知道的JavaScript";
 function innerSay(){
 alert(blogName);
 }
 innerSay();
}
alert(authorName); //小平果
alert(blogName); //脚本错误
doSomething(); //你不知道的JavaScript
innerSay() //脚本错误

(2)所有未定义直接赋值的变量会自动声明为拥有全局作用域,例如:


function doSomething(){
 var authorName="小平果";
 blogName="你不知道的JavaScript";
 alert(authorName);
}
doSomething(); //小平果
alert(blogName); //你不知道的JavaScript
alert(authorName); //脚本错误

在上述示例中,变量blogName拥有全局作用域,而authorName仅在函数内部有效,在函数外部无法访问。

(3)所有window对象的属性均拥有全局作用域。

通常情况下,window对象的内置属性(如window.name、window.location、window.top等)都具备全局作用域。

2. 局部作用域(Local Scope) 与全局作用域相对,局部作用域通常仅在特定的代码片段(如函数内部)中可访问。因此,这种作用域有时也被称为函数作用域。例如,下列代码中的blogName和函数innerSay仅拥有局部作用域。


function doSomething(){
 var blogName="你不知道的JavaScript";
 function innerSay(){
 alert(blogName);
 }
 innerSay();
}
alert(blogName); //脚本错误
innerSay(); //脚本错误

作用域链机制解析

二、作用域链(Scope Chain)

在JavaScript中,函数也是对象,且JavaScript中万物皆对象。函数对象与其他对象一样,拥有可通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义。该内部属性包含了函数被创建时所在作用域中的对象集合,这个集合被称为函数的作用域链,它决定了函数可以访问哪些数据。

当一个函数被创建后,其作用域链会被填充为该函数创建时所在作用域中所有可访问的数据对象。例如,定义如下函数:


function add(num1,num2) {
 var sum = num1 + num2;
 return sum;
}

在函数add创建时,其作用域链中会包含一个全局对象,该全局对象包含了所有全局变量,如下图所示(注意:图片仅列举了部分全局变量):

JavaScript作用域基础

函数add的作用域将在执行时被使用。例如,执行以下代码:


var total = add(5,10);

执行该函数时,会创建一个称为“运行期上下文(execution context)”的内部对象,该对象定义了函数执行时的环境。每个运行期上下文都拥有自己的作用域链,用于标识符解析。当运行期上下文被创建时,其作用域链初始化为当前运行函数的[[Scope]]所包含的对象。

这些值按照它们在函数中出现的顺序被复制到运行期上下文的作用域链中。它们共同组成一个新的对象,称为“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this。随后,此对象会被推入作用域链的前端。当运行期上下文被销毁时,活动对象也随之销毁。新的作用域链如下图所示:

JavaScript作用域基础

在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程,以决定从哪里获取和存储数据。该过程从作用域链头部(即活动对象)开始搜索,查找同名的标识符。如果找到,则使用该标识符对应的变量;如果未找到,则继续搜索作用域链中的下一个对象。如果搜索完所有对象仍未找到,则认为该标识符未定义。在函数执行过程中,每个标识符都要经历这样的搜索过程。

作用域链与代码优化

三、作用域链与代码优化

从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就越慢。如上图所示,由于全局变量总是存在于运行期上下文作用域链的最末端,因此在标识符解析时,查找全局变量是最慢的。因此,在编写代码时应尽量减少使用全局变量,尽可能使用局部变量。一个良好的经验法则是:如果一个跨作用域的对象被引用一次以上,则应先将其存储到局部变量中再使用。例如以下代码:


function changeColor(){
 document.getElementById("btnChange").onclick=function(){
 document.getElementById("targetCanvas").style.backgroundColor="red";
 };
}

该函数引用了两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。这段代码可以重写如下:


function changeColor(){
 var doc=document;
 doc.getElementById("btnChange").onclick=function(){
 doc.getElementById("targetCanvas").style.backgroundColor="red";
 };
}

这段代码较为简单,重写后可能不会显示出巨大的性能提升。但是,如果程序中有大量全局变量被反复访问,那么重写后的代码性能会有显著改善。

改变作用域链的情况

四、改变作用域链

函数每次执行时对应的运行期上下文都是独一无二的,因此多次调用同一个函数会导致创建多个运行期上下文。当函数执行完毕,执行上下文会被销毁。每一个运行期上下文都和一个作用域链关联。一般情况下,在运行期上下文运行过程中,其作用域链只会被with语句和catch语句影响。

with语句是对象的快捷应用方式,用于避免书写重复代码。例如:


function initUI(){
 with(document){
 var bd=body,
 links=getElementsByTagName("a"),
 i=0,
 len=links.length;
 while(i < len){
 update(links[i++]);
 }
 getElementById("btnInit").onclick=function(){
 doSomething();
 };
 }
}

这里使用with语句来避免多次书写document,看似更高效,实际上却产生了性能问题。

当代码运行到with语句时,运行期上下文的作用域链会临时被改变。一个新的可变对象被创建,它包含了参数指定对象的所有属性。这个对象将被推入作用域链的头部,这意味着函数的所有局部变量现在处于第二个作用域链对象中,因此访问代价更高了。如下图所示:

JavaScript作用域基础 因此在程序中应避免使用with语句,在这个例子中,只要简单地把document存储在一个局部变量中就可以提升性能。

另一个会改变作用域链的是try-catch语句中的catch语句。当try代码块中发生错误时,执行过程会跳转到catch语句,然后把异常对象推入一个可变对象并置于作用域的头部。在catch代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中。示例代码:


try{
 doSomething();
}catch(ex){
 alert(ex.message); //作用域链在此处改变
}

请注意,一旦catch语句执行完毕,作用域链将恢复到之前的状态。try-catch语句在代码调试和异常处理中非常有用,因此不建议完全避免。你可以通过优化代码来减少catch语句对性能的影响。一个很好的模式是将错误委托给一个函数处理,例如:


try{
 doSomething();
}catch(ex){
 handleError(ex); //委托给处理器方法
}

  

优化后的代码中,handleError方法是catch子句中唯一执行的代码。该函数接收异常对象作为参数,这样你可以更加灵活和统一地处理错误。由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能了。

以上就是本文的全部内容,希望通过这篇文章大家更加了解javascript的作用域与作用域链,大家共同进步。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章