JavaScript作用域和作用域链
作用域(scope)是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理。
作用域
所有编程语言都有作用域的概念。简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在程序设计语言中一般分几种作用域:全局作用域、局部作用域和块作用域。在JavaScript中没有块作用域。
全局作用域(Global Scope)
在程序中的任何位置都能够访问到的对象具有全局作用域。
一般在最外层函数外定义的变量具有全局作用域:
1
2
3
4
5
6
7
8
9
10
11
12var a="a";
function doSomething(){
var b="b";
function innerSay(){
alert(b);
}
innerSay();
}
alert(a); // a
alert(b); // undefinde
doSomething(); //b
innerSay() //脚本错误在JavaScript中未声明直接赋值的变量具有全局作用域:
1
2
3
4
5
6
7
8function doSomething(){
var a="a";
b= ="b";
alert(a);
}
doSomething(); // a
alert(b); // b
alert(a); // 脚本错误在JavaScript中,window对象的属性也拥有全局作用域。
局部作用域
具有局部作用域的对象一般只能在一段代码内访问到,在JavaScript中通常是函数内部。如上面的例子,在函数内部定义的变量不能在函数外面访问到。
作用域链(Scope Chain)
在JavaScript中,函数也是对象,实际上,JavaScript里一切都是对象。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
JavaScript的作用域和C语言的作用域有些不同。在JavaScript中,函数的作用域在函数定义的时候就已经确定了,和函数调用的位置没有关系。
JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里
任何执行上下文时刻的作用域, 都是由作用域链(scope chain)来实现.在一个函数被定义的时候, 会将它定义时刻的scope chain链接到这个函数对象的[[scope]]属性.在一个函数对象被调用的时候,会创建一个活动对象(也就是一个对象), 然后对于每一个函数的形参,都命名为该活动对象的命名属性, 然后将这个活动对象做为此时的作用域链(scope chain)最前端, 并将这个函数对象的[[scope]]加入到scope chain中.
在函数执行过程中,没遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义。函数执行过程中,每个标识符都要经历这样的搜索过程。
作用域链和代码优化
从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。如上图所示,因为全局变量总是存在于运行期上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。一个好的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。例如下面的代码:1
2
3
4
5function changeColor(){
document.getElementById("btnChange").onclick=function(){
document.getElementById("targetCanvas").style.backgroundColor="red";
};
}
这个函数引用了两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。这段代码可以重写如下:1
2
3
4
5
6function changeColor(){
var doc=document;
doc.getElementById("btnChange").onclick=function(){
doc.getElementById("targetCanvas").style.backgroundColor="red";
};
}
这段代码比较简单,重写后不会显示出巨大的性能提升,但是如果程序中有大量的全局变量被从反复访问,那么重写后的代码性能会有显著改善
改变作用域链
函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文,当函数执行完毕,执行上下文会被销毁。每一个运行期上下文都和一个作用域链关联。一般情况下,在运行期上下文运行的过程中,其作用域链只会被 with 语句和 catch 语句影响。
with会降低代码执行效率。
另外一个会改变作用域链的是try-catch语句中的catch语句。当try代码块中发生错误时,执行过程会跳转到catch语句,然后把异常对象推入一个可变对象并置于作用域的头部。在catch代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中。
请注意,一旦catch语句执行完毕,作用域链机会返回到之前的状态。try-catch语句在代码调试和异常处理中非常有用,因此不建议完全避免。你可以通过优化代码来减少catch语句对性能的影响。一个很好的模式是将错误委托给一个函数处理。
Javascript的预编译
JavaScript是一种脚本语言,JavaScript执行过程是一种翻译执行的过程,那么JavaScript的执行中, 有没有类似编译的过程呢?其实JavaScript是有预编译过程的。看下面的例子:1
2
3
4alert(typeof eve); //function
function eve() {
alert('I am Laruence');
};
在eve声明前调用它是有意义的。JavaScript在执行每一段JavaScript代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式)。在调用函数执行之前, 会首先创建一个活动对象, 然后搜寻这个函数中的局部变量定义,和函数定义, 将变量名和函数名都做为这个活动对象的同名属性; 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined。
对于函数的定义需要注意1
2
3
4
5
6
7
8alert(typeof eve); //结果:function
alert(typeof walle); //结果:undefined
function eve() { //函数定义式
alert('I am Laruence');
};
var walle = function() { //函数表达式
}
alert(typeof walle); //结果:function
函数定义式和函数表达式是不同, 对于函数定义式, 会将函数定义提前,而函数表达式, 会在执行过程中才计算
JavaScript在执行每一段代码JavaScript时会进行预编译,看下面的例子:1
2
3
4
5
6
7
8<script>
alert(typeof eve); //结果:undefined
</script>
<script>
function eve() {
alert('I am Laruence');
}
</script>
一个问题:1
2
3
4
5
6
7
8
9var name = 'laruence';
function echo() {
alert(name);
var name = 'eve';
alert(name);
alert(age);
}
echo();
三次alert()输出的内容分别是什么?
很多人可能马上给出的答案是这样的:1
2
3laruence
eve
[脚本出错]
但其实, 运行结果应该是:1
2
3undefined
eve
[脚本出错]
因为会以为在echo中, 第一次alert的时候, 会取到全局变量name的值, 而第二次值被局部变量name覆盖, 所以第二次alert是’eve’. 而age属性没有定义, 所以脚本会出错。