从“JS变量的作用域”一节例 1 的运行结果中我们可以看到,在函数体内可以访问函数体外的全局变量,为什么会这样呢?这是因为函数存在一个称为作用域链的集合对象。ECMA-262 标准第 3 版定义所有函数都有一个称为 Scope 的内部属性,该内部属性可供 JavaScript 引擎(解释器)访问,其中包含了函数作用域中的对象的集合,这个集合称为函数的作用域链,它决定了函数可访问哪些数据。
对作用域链更通俗的解释为:JavaScript在运行的时候,需要一些空间来存储脚本所用到的变量,存储变量的这些空间称为作用域对象(Scope object),也称为词法作用域。作用域对象可以有父作用域对象。当脚本代码访问一个变量的时候,JavaScript 引擎将在当前的作用域对象中查找这个变量。
如果这个变量不存在,JavaScript 引擎就会在父作用域对象中查找这个变量。依此类推,直到找到该变量或者再也没有父作用域对象为止。这个查找变量的过程可能经过的作用域对象就称为作用域链(Scope chain)。
作用域链中的对象访问顺序是:访问的第一个对象为当前作用域对象,下一个对象来自包含(外部)环境,即父作用域对象,再下一个变量对象则来自于在下一个包含环境,即祖父作用域对象,依此类推,一直延续到全局执行环境,即全局作用域对象,全局作用域对象是作用域链中的最后一个对象。
注意:在 JavaScript 中,作用域对象是在堆中被创建的,在函数返回后,如果还有其他对象引用它们时,则不会被销毁,所以仍可以被访问。
函数作用域链中的数据根据作用域及生成时机的不同,可分为不同类型。下面我们以示例 1 的代码为例具体介绍函数的作用域链中所涉及的几类对象。
【例 1】函数作用域链示例。
<script>
var v1 = "JavaScript";
function testScope(value1,value2){
var result = value1 + value2;
return result;
}
var v2 = "JScript";
var sum = testScope(10,20);
console.log("v1 = " + v1);
console.log("v2 = " + v2);
console.log("sum = " + sum);
</script>
当一个函数被定义后,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量。此时函数作用域链如图 1 所示。
当程序执行到 var sum=testScope(10,20) 进行函数调用时,会创建一个称为“运行期上下文(execution context)”的内部对象,该对象定义了函数执行时的环境。每个执行上下文都有自己的作用域链,用于标识符解析,当运行期上下文被创建时,它的作用域链初始化为当前运行函数的 [[Scope]] 所包含的对象。
这些对象按照它们在函数中出现的顺序被复到运行期上下文的作用域链中,它们共同组成了一个称为“活动对象(activation object)”的新对象,该对象包含了函数的所有局部变量、命名参数、参数集合以及 this。活动对象生成后会被推入作用域链的前端。
函数运行期上下文作用域链如图 2 所示。
当函数执行完毕时,运行期上下文会被销毁,活动对象因没有被引用也会随之被销毁,因此,离开函数后,属于活动对象的局部变量无效。
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到就继续搜索作用域链中的下一个对象,如果搜索完作用域链所有对象都未找到,则认为该标识符未定义而报引用错误。
从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。如图 2 所示,因为全局变量总是存在于运行期上下文作用域的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码时应尽量少使用全局变量,而尽可能使用局部变量。
一个优化代码的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储为局部变量再使用。例如下面的代码:
function changeColor(){
document.getElementById("btnChange").onclick = function(){
document.getElementById("targetCanvas").style.backgroundColor = "red";
};
}
函数 changeColor() 引用了两次全局变量 document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。
按上面所述的代码优化规则,可将上述代码修改如下:
function changeColor(){
var doc = document;
doc.getElementById("btnChange").onclick = function(){
doc.getElementById("targetCanvas").style.backgroundColor = "red";
};
}
这段代码比较简单,重写后不会显示出巨大的性能提升,但是如果程序中有大量的全局变量被反复访问,那么重写后的代码性能会有显著改善。