函数在javascript中扮演着一个重要的角色,作用域可以确定哪些变量可以被函数访问,确定this的值,而且也关系到代码的性能,所以理解函数的创建和执行过程及作用域至关重要。
首先得了解几个名词(其实有些名词本人也不是很明白):
1.作用域(scope):在javascript没有块级作用域,是由函数来划分的。变量和函数的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域(with和eval除外)。当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部份。在最顶级作用域链仅由全局对象组成,而不和词法作用域相关,然而,当定义一个嵌套的函数时,作用域链就包括外面的包含函数。这意味着嵌套函数可以访问包含函数的所有参数和局部变量。尽管当一个函数定义时作用域链就固定了,但作用域链中定义的属性还没有固定。作用域链是活的,并且函数被调用时,可以访问任何当前的绑定。
2.作用域链(scope chain):存储变量对象的集合(环境栈?),保证对执行环境有权访问的所有变量和函数的有序访问,也就是用于标识符解析(变量访问)。
3.执行环境(execution context):定义了变量和函数有权访问的其他数据,决定了它们的各自行为。每个执行环境都有一个与之关联的变量对象。
在web浏览器中,window对象就是全局执行环境。每个函数都有自己的执行环境,当函数执行完毕,该环境被销毁,保存在其中的变量和函数也随之销毁。
4.变量对象(variable object):保存了环境中定义的所有变量和函数。该对象无法访问,仅供解析器在后台使用。
5.活动对象(activation object):如果执行环境是函数,则将其活动对象作为变量对象(调用对象?)。活动对象最开始的两个属性是arguments和this
总的来说,以上所说的名词都是js程序员不可直接操作的,了解它们可帮助我们理解js引擎的在处理代码的是如何工作的。
一、作用域链
a) 一个函数创建时,javascript后台(引擎)会默认创建一个仅供后台使用的内部属性[[Scope]],此属性存储函数的作用域链,如果是全局函数,此时则包含一个变量对象(全局变量),如果是嵌套函数(闭包),作用域链还加上了父函数的变量对象。例如下面的这个全局函数:
function add(num1,num2){ var sum = num1 + num2; return sum; }
(此图是函数定义时的作用域链)
b) 函数被调用时--add(5,10),javascript后台会创建一个内部对象(execution context)--“执行环境”或“运行期上下文”,执行环境有它自己的作用域链,执行环境创建时就以定义函数时的作用域链初始化它自己的作用域链,并且随后创建了一个活动对象,活动对象作为函数执行期的一个变量对象,包含所有局部变量(在函数内定义的)、命名参数、arguments、this,它会被推入到执行环境作用域链的前端(如下图)。每执行一次函数都会创建一个新的执行环境,当函数执行完毕执行环境就会被销毁。
(此图是函数运行时的作用域链)
另外关于延长作用域链问题:以下的两种情况会使作用域链延长
1) try-catch语句的catch块;
2) with语句;
这个两个语句都会再原本的作用域链的前端添加一个变量对象。对于with语句来说,新添加的变量对象包含着with括号中指定对象的所有属性和方法所作的变量声明。对于catch来说,当try块发生错误时,代码执行流程自动转入到catch块,并将异常对象推入到作用域链的前端。catch块执行完毕后,作用域链就会返回原来的状态。
请看下面的例子:
function initUI(){ with(document){ var bd = body, links = getElementsByTagName("a"), i = 0, len = links.length; while(i<len){ update(links[i++]); } getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active" } }
当代码流执行到一个with表达式时,执行环境的作用域链会被临时改变,此时with的变量对象会被创建添加到作用域链的前端,这就意味着此时函数的所有局部变量都被推入到第二个作用域链中的变量对象,如下图:
由上图可清晰的看到,在执行with语句时,访问局部变量的代价更高了。所以尽可能避免使用with语句,可以使用局部变量代替
var doc = document; // 代替with(document){...}
二、标识符解析/变量访问
当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么,搜索过程始终从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符,搜索过程就会停止,变量就绪。否则继续向上级搜索直到找到标识符为止(如果在全局环境都找不到标识符,则意味着该变量未声明,通常会导致错误发生),通过下面的例子来理解一下标识符查询过程:
var color = "blue"; function getColor(){ var color = "red"; return color; } alert(getColor()); //red
当执行函数getColor()会引用变量color,为了确定变量color的值,将开始变量color的搜索过程,通过前面所述,我们知道这个函数执行环境的作用域链包含两个变量对象,一个是执行函数时本身的活动对象,另一个就是全局对象。在这个搜索过程中,首先就搜索getColor()的变量对象,如果存在一个局部的变量定义,则搜索会自动停止,不在进入下一个变量对象。所以函数搜素变量color,返回"red"。
理解上面标识符的解析过程,很明显知道,变量查询是要付出代价的,访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链。所以,当函数需要重复引用一个变量时,最好在局部变量定义它,尽管它已经在全局环境中已经定义好了。
如果你完全理解了下面的几个例子,那你就掌握了本文所述的知识:
例子1:
1 var name = "window"; 2 var obj ={ 3 name:"object", 4 getName:function(){ 5 return function(){ 6 return this.name; 7 } 8 }() 9 }; 10 alert(obj.getName()); // 输出什么?
例子2:
1 f = function(){return true;}; 2 g = function(){return false;}; 3 (function() { 4 //alert(g()); 5 if (g() && [] == ![]) { 6 f = function f() {return false;}; 7 function g() {return true;} 8 } 9 10 })(); 11 alert(f()); // true or false ?
例子3:
1 function createFunc(){ 2 var funcs = new Array(); 3 for(var i=0;i<10;i++){ 4 funcs[i] = function(num){ 5 return function(){return num;} 6 }(i); 7 } 8 return funcs; 9 } 10 var aFuncs = createFunc(); 11 alert(aFuncs[1]());