文章目录
  1. 1. 作用域
    1. 1.1. 全局作用域(Global Scope)
    2. 1.2. 局部作用域
  2. 2. 作用域链(Scope Chain)
    1. 2.1. 作用域链和代码优化
    2. 2.2. 改变作用域链
  3. 3. Javascript的预编译

作用域(scope)是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理。

作用域

所有编程语言都有作用域的概念。简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在程序设计语言中一般分几种作用域:全局作用域、局部作用域和块作用域。在JavaScript中没有块作用域。

全局作用域(Global Scope)

在程序中的任何位置都能够访问到的对象具有全局作用域。

  1. 一般在最外层函数外定义的变量具有全局作用域:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var a="a";
    function doSomething(){
    var b="b";
    function innerSay(){
    alert(b);
    }
    innerSay();
    }
    alert(a); // a
    alert(b); // undefinde
    doSomething(); //b
    innerSay() //脚本错误
  2. 在JavaScript中未声明直接赋值的变量具有全局作用域:

    1
    2
    3
    4
    5
    6
    7
    8
    function doSomething(){
    var a="a";
    b= ="b";
    alert(a);
    }
    doSomething(); // a
    alert(b); // b
    alert(a); // 脚本错误
  3. 在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
5
function changeColor(){
document.getElementById("btnChange").onclick=function(){
document.getElementById("targetCanvas").style.backgroundColor="red";
};
}

这个函数引用了两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。这段代码可以重写如下:

1
2
3
4
5
6
function 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
4
alert(typeof eve); //function
function eve() {
alert('I am Laruence');
};

在eve声明前调用它是有意义的。JavaScript在执行每一段JavaScript代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式)。在调用函数执行之前, 会首先创建一个活动对象, 然后搜寻这个函数中的局部变量定义,和函数定义, 将变量名和函数名都做为这个活动对象的同名属性; 对于局部变量定义,变量的值会在真正执行的时候才计算, 此时只是简单的赋为undefined。

对于函数的定义需要注意

1
2
3
4
5
6
7
8
alert(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
9
var name = 'laruence';
function echo() {
alert(name);
var name = 'eve';
alert(name);
alert(age);
}

echo();

三次alert()输出的内容分别是什么?

很多人可能马上给出的答案是这样的:

1
2
3
laruence
eve
[脚本出错]

但其实, 运行结果应该是:

1
2
3
undefined
eve
[脚本出错]

因为会以为在echo中, 第一次alert的时候, 会取到全局变量name的值, 而第二次值被局部变量name覆盖, 所以第二次alert是’eve’. 而age属性没有定义, 所以脚本会出错。

文章目录
  1. 1. 作用域
    1. 1.1. 全局作用域(Global Scope)
    2. 1.2. 局部作用域
  2. 2. 作用域链(Scope Chain)
    1. 2.1. 作用域链和代码优化
    2. 2.2. 改变作用域链
  3. 3. Javascript的预编译