JS中的变量作用域

最初接触“词法域”的概念是在Lua的学习过程中。所谓“词法域”指的是:若将一个函数写在另一个函数之内,那么这个位于内部的函数可以访问外部函数中的局部变量。由于变量函数作用域与C++中的块级作用域完全不一样,当时理解真是煞费苦心。现在在JS中居然又碰见了词法作用域,可谓是感叹万分呀!毕竟看Lua纯粹只是当时的一点兴趣罢了。下面就是关于JS中的变量作用域,以及对于闭包的简单理解。

<!--more-->

JS中的变量作用域分为全局作用域与函数作用域。所谓全局作用域,指的是不再任何函数内声明的变量的作用域。全局变量可以看作是全局对象的属性。这篇文章主要关注的是函数作用域。

《JavaScript权威指南》关于JS函数作用域的定义是:变量在声明他们的函数体内以及这个函数所嵌套的任意函数体内都是有定义的(作用域链),且声明的变量在函数体内都是可见的(即变量声明提前)。

1. 变量声明提前

了解JS解释器的工作过程对于理解“变量声明提前”很有帮助。惭愧的是对于编译原理、操作系统等计算机知识一无所知,因此这个具体的过程我并不知道一丁点的东西,这大概也是相对于科班出身的同学来说,自己很大的短板。啊扯远了。JS解释器工作过程可分成“预解析”与“逐行运行代码”两部分。

  • 预解析:根据var function等声明寻找找所有的变量及函数,在正式运行代码前所有的变量都赋值为undefined,而所有的函数都只是函数块(不会调用函数);然后将变量放在一个“仓库”(只是为了理解罢了,可以理解为作用域的具体化),如果遇见与变量名相同的函数,则保留函数;如果是重名函数,则后声明的函数会覆盖前面声明的函数;如果只是两个变量重名,由于提前赋值均为undefined,因此并没有什么区别。

  • 逐行运行代码:按程序逐行运行代码,遇见变量则从“仓库”中取出并使用。因此即使函数放在最下面,也可以在其声明之前调用;当遇见赋值表达式的时候,存放在仓库中的变量值会被改变。

下面通过一个例子说明。


    alert(a);//function a(){alert(4);}

    var a = 1;

    alert(a);//1

    function a(){alert(2);}

    alert(a);//1

    var a = 3;

    alert(a);//3

    function a(){alert(4);}

    alert(a);//3

通过运行代码可以看见:

  • 第一次弹出的是//function a(){alert(4);}整个代码块;

  • 第二次弹出的是1(由于其前面那句的赋值表达式改变了仓库中的值,从一个函数块变为了1);

  • 第三次弹出的仍然是1;

  • 第四次弹出的是3;

  • 第五次弹出的仍然是3;

可以看见,虽然声明了函数,但是并没有调用它,在代码运行的过程中声明的函数被覆盖,且在整个程序结束之后,a为一个数字而非函数了,当使用a()时会报错。

2. 作用域链

当从全局域的第二步(也就是上面所说的逐行运行代码)到一个函数调用的时候,进入函数作用域,并在函数作用域开始预解析,过程与全局域相同。首先将变量放在另外一个小的独立仓库(即该函数作用域),需要注意的是形参也是变量声明因此也会被放入“仓库”。然后开始逐行运行函数中的代码,遇见变量则优先从该函数的仓库里寻找变量,如果有,则使用其值;如果没有则从父级的仓库逐步向上寻找(作用域链);继而如果从全局仓库都没有找到,程序就会报错。

函数中的局部变量可以看作是调用该函数的那个对象的自定义属性,那么可以换个角度理解作用域链。 每一段代码(全局代码或者函数块)都有一个与之相关的作用域链,这个链是一个对象列表,这组对象定义了作用域中的变量,当需要查找某个变量时,将从第一个对象(最近的那个函数作用域)一直寻找至全局对象。下面这个例子:


    var a = 1;

    var b = 1;

    function f(){

        alert(a);//undefine

        alert(b);//1

        //alert(c);//报错

        var a = 2;//不会更改全局仓库里面的值,如果没有前面的var关键字,则会修改全局仓库里面的值

    }

    f();

    alert(a);//1

可以看见,当调用f()时,由于首先进行的预解析,因此alert(a)并不会报错而是一个undefined;由于b元素是上一个作用域链的变量(更大的那个仓库),因此会显示1;由于c并没有声明,且又不存在于作用链中,因此会报错。

如果在上面的例子中为f()声明传入一个参数,但是调用的时候不传实参进去,且不在函数体内单独声明该变量则会出现下面情况。


    var a = 1;

    var b = 1;

    function f(a){

    alert(a);//undefine

    a = 2;

    }

    f();//不传参数

    alert(a);//1

这就是前面所说的形参也可以看做是局部变量声明。

但是如果在调用的时候将实参(全局作用域里面的变量)传进函数,则相当于在函数内部的第一句表达式之前隐藏着 var a (形参)= a(实参)。由于函数域仓库存在着变量,因此就不会改变父级变量的值(JS中并没有C++类似的指针或者引用,如果想要改变父级变量的值,则直接使用作用域链上的变量而不要在单独声明新的变量。)


    var a = 1;

    var b = 1;

    function f(a){

        alert(a);//1

        a = 2;

        b = 0;

    }

    f(a);//将a传入函数

    alert(a);//1

    alert(b);//0

3. 最后

这篇文章是最初学习JS时所做的笔记了,在整理的时候发现了一些错误并已经纠正。变量作用域是学习JS中很重要的一点,接下来就应该整理一下闭包了。

《计算机科学导论》读书笔记 《JavaScript权威指南》读书笔记