深入理解 JavaScript 变量的作用域和作用域链
一个变量的作用域(scope)是程序源代码中定义这个变量的区域。简单的说,作用域就是变量与函数的可访问范围。全局变量拥有全局作用域,在JavaScript代码中的任何地方都有定义。局部变量是在函数体内声明而且只作用在函数体内部以及该函数体的子函数的变量。下面我们对全局作用域和局部作用域来做一个深入的理解。
1. 全局作用域(Global Scope)
全部变量拥有全局作用域,在代码的任何地方都有定义,一般来说以下几种情形拥有全局作用域:
(1)最外层函数和在最外层函数外面定义的变量拥有全局作用域,例如:
1 var scope="global"; //声明一个全局变量 2 function checksope(){ 3 function showglobal(){ 4 alert(scope); //弹窗全局变量 5 } 6 showglobal(); 7 } 8 checksope() // global 内部函数可以访问全局变量
(2)所有末定义直接赋值的变量自动声明为拥有全局作用域,例如:
1 function checksope(){ 2 var scope="local"; 3 scopeglobal="global"; 4 alert(scope); 5 } 6 checksope(); // local 7 alert(scopeglobal); // global 不带var关键词声明的变量, 8 直接升级为全局变量,同时也是全局变量 9 的一个属性 10 alert(scope); //脚本错误
变量scopeglobal拥有全局作用域,而scope在函数外部无法访问到。
(3)所有window对象的属性拥有全局作用域
一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。
2. 局部作用域(Local Scope)
局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所有在一些地方也会看到有人把这种作用域称为函数作用域,例如下列代码中的blogName和函数innerSay都只拥有局部作用域。
1 function checksocpe(){ 2 var socpe="local"; 3 function inner(){ 4 alert(socpe); 5 } 6 inner(); 7 } 8 alert(socpe); //脚本错误 9 inner(); //脚本错误 函数外部无法访问内部定义的函数
函数作用域和提前声明
一、函数作用域
在一些类似c语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称之为块级作用域,而JavaScript没有块级作用域。JavaScript取而代之地使用了函数作用域:变量在声明他们的函数体以及这个函数体嵌套的任意函数体内都有定义的。
在如下所示的代码中,在不同位置定义了变量i、j、k,它们都在同一个作用域内——这三个变量在函数体内均是有定义的。
1 function test(0){ 2 var i = 0; // i在行函数体内时有定义的, 3 if(typof 0 == "object"){ 4 var j = 0; //j在函数体内是有定义的,不仅仅是在循环内 5 for(var k=0; k<10;k++){ //k在行函数体内是有定义的,不仅仅是在循环内 6 console.log(k);//输出数字0-9 7 8 } 9 console.log(k); //k 已经定义了,输出10 10 } 11 console.log(j); //j 已经定义了,但是可能没有初始化 12 }
二、函数的提前声明
javascript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的,有意思的是,这意味着变量在声明之前甚至已经可用。JavaScript的这个特性被非正式地称为声明提前,即JavaScript函数里声明的所有变量(但不涉及赋值)都被"提前"至函数体的顶部。
tip:"声明提前"这步操作是在JavaScript引擎的"预编译"时进行的,是在代码开始运行之前。
1 var scope = "glocal"; 2 function f(){ 3 console.log(scope);//输出"undefined",而不是"global" 4 var scope = "local";//变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的 5 console.log(scope);//输出"local" 6 }
也许您会误认为第一行会输出“global”,因为代码还没有执行到var语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量覆盖了同名全局变量。尽管如此,只有在程序执行到var语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的变量声明“提前”至函数顶部,同时变量初始化留在原来的位置:
function f(){ var scope; //在函数定部声明了局部变量 console.log(scope); //undefined scope = "local";// console.log(scope); // 输出local }
作为属性的变量
当声明一个全局变量时,实际上是定义了全局对象的一个属性。使用var声明的变量不可配置,未声明的可配置。如下:
var truvar = 1; //声明一个不可删除的全局变量 fakevar = 2; //创建全局对象的一个可删除的属性 this.fakecar2 = 3;//同上 delete truevar //=> false:变量并没有被删除 delete fakevar //=> true:变量并没有被删除 delete this.fakevar2 //=> true:变量并没有被删除
此规则只对全局变量有效。
作用域链
1、作用域链变量的寻址
如果讲一个局部变量看做是自定义实现的对象的属性的话,那么可以换一个角度来理解作用域。
每一段javascript代码(全局代码或函数)都有一个与之关联的作用域链。这个作用域连是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当javascript需要查找变量x的值的时候(这个过程称作“变量解析”),它会从链中的第一个对象开始查找,如果这个对象有一个名为x属性,则会直接使用这个属性的值,如果第一个对象中不存在,则会继续寻找下一个对象,依次类推。如果作用域链上没有任何一个对象含有属性x,则抛出错误(ReferenceError)异常。
2、不同的层级作用域上对象的分布
- 在javascript的最顶层(也就是不包含任何函数定义内的代码),作用域链由一个全局对象组成。
- 在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。
- 在一个嵌套的函数体内,作用域链上至少有三个对象。当调用这个函数时,它创建一个新的对象来存储它的局部变量,它实际上保存在同一个作用域链。
对于嵌套函数来讲,事情更有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是不相同的,而且关联这段代码的作用域链也不相同。