在一些类似C语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的(也就是我们不能在代码段外直接访问代码段内声明的变量),我们称之为块级作用域,然而,不同于这类型的编程语言,javascript是没有块级作用域。取而代之的,javascript使用的是块级作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
在如下的所示的代码中,在不同位置定义了变量 i 、 j 和 k ,它们都在同一个作用域内——这三个变量在函数体内均是有定义的。
function text(o){ var i = 0; // i 在整个函数体内均是由定义的 if(typeof o == "object"){ var j = 0; // j 在函数体内是有定义的,不仅仅是这个代码段内 for (var k = 0; k < 10; k++) { // k 在函数体内是有定义的,不仅仅是在循环体内 console.log(k); //输出数字 0 到 9 }; console.log(k); // k 已经定义了,输出 10 } console.log(j); // j 已经定义了,但可能没有初始化 }
javascript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。javascript的这个特征被非正式地称为声明提前,即javascript函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部,看一下如下代码:
1 function f(){ 2 console.log(scope); //输出"undefined",而不是"global" 3 var scope = "local"; //变量在这里赋初始值,但变量本身在函数体任何地方均是有定义的 4 console.log(scope); //输出"local" 5 }
你可能会误以为函数中的第一行会输出"global",因为代码还没有执行到var语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部函数在整个函数体始终是有定义的,也就是说,在函数体局部变量遮盖了全名全局变量。尽管如此,只有在程序执行到var语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的变量声明"提前"至函数体顶部,同时变量初始化留在原来的位置:
1 function f(){ 2 var scope; //在函数顶部声明了局部变量 3 console.log(scope); //变量存在,但其值是"undefined" 4 scope = "local"; //这里将其初始化并赋值 5 console.log(scope); //这里它具有了我们所期望的值 6 }
接下来,谈谈javascript的作用域链的概念。在javascript犀牛这本书中,有一小段对作用域链的定义和介绍。
javascript是基于词法作用域的语言:通过阅读包含变量定义在内的数行源码就能知道变量的作用域。全局变量在程序中始终都是有定义的。局部变量在声明它的函数体内以及其所在嵌套的函数内始终是有定义的。
如果将一个局部变量看做是自定义实现的对象的属性的话,那么可以换个角度来解读变量作用域。每一段javascripe代码(全局代码或函数)都有一个与之关联的作用域链。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码"作用域中"的变量。当javascript需要查找变量x的值的时候(这个过程称作"变量解析"),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为x的属性,javascript会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个对象,以此类推,如果作用域链上没有任何一个对象含有属性x,那么就认为这段代码的作用域链上不存在x,并最终抛出一个引用错误异常。
在javascript的最顶层代码中(也就是不包含任何函数定义内的代码),作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。理解对象链的创建规则是非常重要的。当定义一个函数时,它实际上保存了一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的"链"。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。外部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。
这段定义和介绍可能比较难理解,那么,我们先来看一段代码:
1 name="lwy"; //全局作用域中定义一个全局变量name,值为lwy 2 function t(){ 3 var name="tlwy"; //函数 t 的作用域中定义一个局部变量name,值为tlwy 4 function s(){ 5 var name="slwy"; //函数 t 的内嵌函数 s 的作用域中定义一个局部变量name,值为slwy 6 console.log(name); //输出name 7 } 8 function ss(){ 9 console.log(name); //输出name 10 } 11 s(); //调用执行函数s 12 ss(); //调用执行函数ss 13 } 14 t(); //调用执行函数t
当执行s时,将创建函数s的执行环境(调用对象),并将该对象置于链表开头,然后将函数t的调用对象链接在之后,最后是全局对象。
作用域链为:s()->t()->window (函数 s 和函数 t 以及对象window中都能查找到变量name)
然后从链表开头寻找变量name,很明显name是"slwy"。
但执行ss()时,作用域链是: ss()->t()->window (除了函数 ss ,函数 t 和对象window都能找到变量name) ,所以name是”tlwy"
那么,接下来我们来看看一个有意思的例子:
1 <html> 2 <head> 3 <script type="text/javascript"> 4 function buttonInit(){ 5 for(var i=1;i<4;i++){ 6 var b=document.getElementById("button"+i); 7 b.addEventListener("click",function(){ alert("Button"+i);},false); 8 } 9 } 10 window.onload=buttonInit; 11 </script> 12 </head> 13 <body> 14 <button id="button1">Button1</button> 15 <button id="button2">Button2</button> 16 <button id="button3">Button3</button> 17 </body> 18 </html>
文档加载完毕,给几个按钮注册点击事件。当我们点击按钮时,你会觉得每个按钮点击后都会弹出按钮内相对应的内容。
然而不正确,三个按钮最终都会弹出:"Button4"。
原因:当注册事件结束后,i的值为4,当点击按钮时,事件函数即function(){ alert("Button"+i);}这个匿名函数中没有i,根据作用域链(匿名函数->函数buttonInit->window),所以到buttonInit函数中找,此时i在循环结束过后的值为4,所以,不管你点击任何哪个按钮都会弹出”button4“。