JavaScript作用域那些事

时间:2023-12-29 18:57:14

作用域

  (1)、作用域也叫执行环境(execution context)是JavaScript中一个重要的概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。在JavaScript中变量的作用域有全局作用域和局部作用域,全局变量是指变量没有在函数体内声明或者在函数内声明的时候没有带var,即表示拥有全局作用域,相反变量在函数内声明带var称为局部变量,拥有局部作用域。特殊的虽然函数参数不带var但它也属于局部变量。

 var a = 1; //全局变量
function fn1( b ){ //局部变量
var c = 2; //局部变量
d = 3; //全局变量
}

  (2)、接下来介绍一下解析过程。浏览器有专门的一段程序用于解析JavaScript,暂且给他取个名字叫JavaScript解析工具,这个解析过程至少有两个过程(当然不仅仅有两个,像编译原理的词法分析什么的,这里不提):1.寻找目标(预解析)。包括var、function、参数等。 2.逐行解读代码。接下来通过实例来具体分析是怎么做的:

 alert( a ); //undefined
var a = 1;
function fn (){
alert( 2 );
}

  具体过程:

    1.寻找目标。包括var、function、参数等
      a = undefined       (所有的变量在正式运行代码之前,都会提前赋值一个值,即undefined)。
      fn = function() { alert(2) }    (所有函数在正式运行之前,都是整个函数块)
    2.逐行解读代码。
      解读代码时表达式会改变预解析中的值,如以上代码解读到第二行时,a = undefined 变为了 a = 1;
特殊的:如果预解析过程中遇到重名的,只留一个。如变量和函数重名了,就只留函数。再来一个例子说明:
 alert( b );  //function a() { alert( 4 ) };
var b = 1;
alert( b ); //
function b () {
alert( 2 );
}
alert( b ); //
var b = 3;
alert( b ); //
function b (){
alert( 4 );
}
alert( b ); //
解析过程还是那两步,只是多了重名的情况。1.预解析后只留下  b = function b(){ alert(4) }。2.逐行解读后,预解析中a的值变为3,若在代码的最后调用 b(), 则会在控制台报错。
 
  (3)JavaScript中没有块级作用域。先看一个例子:
 if( true ){
var a = 1;
}
alert( a ); //

在一个 if 语句中定义变量 a。如果是在 C、C++等语言中,a会在 if 语句执行完毕后被销毁。但在JavaScript中,if 语句中的变量声明会将变量添加到当前的作用域(在这里是全局作用域)中。特别是在使用 for 语句时:

 for( var i=0; i<10; i++ ){
doSothing(i);
}
alert(i); //

对于块级作用域的语言来说,for语句初始化变量的表达式所定义的变量,只会存在于循环的环境中。而对于JavaScript来说,有for语句创建的变量 i 即使在for循环执行结束后,也依旧会存在于循环外部的作用域中。

  (4)、函数是作用域,有预解析等过程,if  for语句不是作用域。尽量不要在 if  for中定义变量和函数调用,否则有浏览器问题:

 alert(fn1);   //chrome,FF 弹出 undefine. ie 弹出整段函数(function fn19=(){alert( 123 )})
if( true ){
var a = 1;
function fn1(){
alert( 123 )
}
}
作用域链
  1.当代码在一个环境中执行时,会创建变量对象的一个作用域链。它的作用是保证对执行环境有权访问所有变量和函数的有序访问。也就是说作用域链就像是一种绳索可以将各个作用域连接起来,已达到可以访问各个域中的变量,当然这种访问是要遵守一定的规则的,即局部作用域可以通过作用域链访问所有的全局作用域,但是全局作用域不能访问局部环境中的任何变量或函数。看下面的例子:
 
 var a = 1;
function change{
var b = 2;
function swap(){
var c = b;
b = a;
a = c; //这里可以访问a,b,c
} //这里可以访问a和b,但不能访问c
swap();
} //这里只能访问a
change();

通过上面例子我们知道作用域之间的 联系是线性、有次序的。每个作用域可以沿着作用域链向上搜索,但任何作用域不能通过向下搜索而进入另一个作用域中,搜索时先搜索自己作用域内是否存在该变量,若不存在则再一级一级往上搜索。

  2.既然我们不能通过这种方式去访问局部作用域,那我们也可以用以下方法去获取函数内部的值:

    (1)、通过设置全局变量获取

 var str = '';
function fn1(){
var a = '需要拿到的值';
str = a;
}
fn1();
alert( str ); //弹出'需要拿到的值'

    (2)、通过函数调用获取

 function fn2(){
var a = '需要拿到的值';
fn3( a );
}
fn2();
function fn3( a ){
alert( a );
}

作用域链的改变

  JavaScript里的 with语句和 catch语句可以在作用域的头部临时增加一个变量对象,该变量对象会在代码执行后被移除,具体来说就是当执行到这两个语句时,作用域链会得到加长。

  (1)、with语句的作用是避免重复书写代码,如:

 function fn(){
with(document){
var btn = getElementById('btn');
var input = getElementsByClassName('input');
}
}

  这里with语句接收一个document对象,因此它的变量对象中就包含了document对象的所有属性和方法,但这个变量对象就被添加到作用域的最前头,这样看似避免了重复书写,但是性能并不好。因为被推到作用域前头,其他的变量就处于第二个作用域当中了,若要逐级访问,访问代价比较大。可以用一局部变量代替document,即可解决,而不必用with语句。

  (2)catch语句与with相类似:

 try {
//可能出错的代码
} catch (error) {
//出错时怎么处理
}

  当出现错误执行catch语句,将出错对象放入作用域头部,然后catch中其他的变量就处于第二个作用域当中了。

  最后通过几个例子强化一下,每个例子都是在前一个例子的基础上做一些调整,但结果却不一样。

(1)、

 var a = 1;
function fn1(){
alert(a); //undefined
var a = 2;
}
fn1();
alert(a); //

(2)、将(1)的第四行改为 a = 2。

 var a = 1;
function fn1(){
alert(a); //
a = 2;
}
fn1();
alert(a); //

(3)、将(2)中的 fn1函数添加参数 a,虽然结果与(1)相同,但解析过程不同。

 var a = 1;
function fn1( a ){ //a相当于局部变量,相当于var a
alert(a); //undefined
a = 2;
}
fn1();
alert(a); //

(4)、在(3)的基础上给第6行添加参数 a。

 var a = 1;
function fn1( a ){ //a相当于局部变量,相当于var a
alert(a); //
a = 2;
}
fn1(a);
alert(a); //

有错误的地方请指正。

参考资料:《JavaScript高级程序设计》