让我们先来看一道例题:

    alert(a);
    var a = 1;
    alert(a);
    function a(){alert(2);}
    alert(a);
    var a = 3;
    alert(a);
    function a(){alert(4);}
    alert(a);

实际上,在正式执行代码前,JS会有一个预解析的过程。那么这个预解析它要干什么呢?其实也就是我们平时见的诸如var,function等声明或者参数提前就是在这一部分进行的。即所谓的声明提前。

对于var a = 3;这样的一个表达式其实是分为两部分的:var a;和a = 3;前者是一个声明,后者是一个表达式。根据声明提前的原则可知,我们来把声明都提前来看,是这样的:

    var a;
    function a(){alert(2);}
    function a(){alert(4);}

既然有三个对于a的声明,那么究竟是哪一个呢?实际上在JS预解析的过程中,遇到重名的声明时,会只保留一个,哪一个呢?如果一个声明有值,另一个声明了但是没有赋值,那么会保留有值的那一个,如变量和函数重名了,就只留下函数,即上面代码前两个会保留第二个,而不是根据上下文顺序来进行保留,不信可以将var声明放到函数声明后面查看结果,什么时候根据上下文来解析呢?就是上面代码中的后两个的情况。

综上可知,JS在预解析的过程中,a最终被声明为function a(){alert(4);}这样的一个函数,所以当预解析完成,代码开始执行的时候,弹出的第一个a的值是function a(){alert(4);}

这个时候代码实际上可以看成这个样子的:

    function a(){alert(4);}
    alert(a);
    a = 1;
    alert(a);
    alert(a);
    a = 3;
    alert(a);
    alert(a);

预解析完毕就开始逐行解读代码了,而在执行的过程中遇到a = 1;这样的表达式的时候,执行表达式,a的值被改变。

弄懂上面的过程,那么在面对例题的变型的时候也很清晰了,比如:

    alert(a);
    var a = 1;
    alert(a);
    function a(){alert(2);}
    alert(a);
    var a = 3;
    alert(a);
    function a(){alert(4);}
    alert(a);

    a();

看起来似乎最后有一行对函数的调用,但通过上面的分析我们可以知道,这个时候,其实a已经不是一个函数了,那么它是什么呢?是上面最后表达式执行后的那个3,也就是说最后一行代码其实是执行这样:3();所以最后我们看见会浏览器报错了,提示a is not a function。通过typeof打出a的类型也是Number,得证。

我们再来看下面这一段代码:

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

同样的,先是JS的一个预解析,在这个过程中,a被声明,fn1函数被声明,然后才开始执行代码,a被修改,执行到函数调用的时候,运行函数里面的代码,这个时候,函数里又被看做成一个域,同样进行外面这样一个预解析然后执行代码的过程,此时alert(a);结果为undedined,然后a值被修改,需要注意的是这里的a值是一个局部的a值,所以对它的修改并不会影响到外面的a值,所以最后弹出a值的时候,结果依然为1。

如果对上面的代码修改如下:

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

去掉var,这个时候再来运行,前面执行都是一样的,当执行的函数调用的时候,函数里面没有对a的声明,找不到a了,那么这个时候怎么办呢?就会往上找,也就是如果在子级作用域中找不到会往父级作用域中查找,这样一个从子级作用域返回父级作用域的过程,就叫做作用域链,所以这个时候alert(a)会弹出一个1,然后执行表达式a = 2;没有用var声明,所以这里是对全局变量a的一个修改,a的最终结果为2

下面我们再来对上面的代码进行一个变型,如下

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

这里我们给函数传入了一个参数,而参数本质上就是一个局部变量,只是这个时候参数没有值,未定义,所以alert()的时候是undedined,然后修改了函数里面a的值,外部的a并没有改变,最后alert(a)的时候结果为1。

我们再来对上面代码进行变型如下:

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

当进行函数调用的时候,这时有参数进来了,是a,那么这个a的值,是来自于全局,在读取参数的过程中,进行了赋值,参数也是个表达式,可以改变值的。没有传参的时候看成这种function fn1(var a;){},有传参的时候时function fn1(var a = 1;){}}但始终记住,函数里的a和外面的a它们是两个不同的变量,不要混淆,所以修改里面的a并没有影响外面的值,两个弹出结果都是1。

再来看一个例子:

    function fn2(){
        var a = "Hello World!";
        fn3(a);
    }
    fn2();

    function fn3(a){
        alert(a);
    }

当执行fn2的时候,执行到fn3时,找不到fn3,这个时候就会去它的父级作用域中找,这个时候父级其实已经把它预解析出来了的,所以在fn2里面是可以找到外面的这个fn3函数的,也就是说在函数里面可以调用到fn3,既然能调用,可以把fn2里面的变量传参到fn3中,但是fn3(a)中的a不是fn2()函数中fn3(a)的这个a,此a非彼a,function fn3(a){}这个a是函数3里面的变量,这里即使把a改为b,c什么的都是可以的,因为是传参嘛。通过这样的方法可以把局部的东西拿到别的地方去

下列代码:

    alert(a);
    if(true){
        var a = 1;
    }

结果为undedined,通过这个例子想要说明的是类似于if,for这一类的即使有(){}它们也不是作用域,那是否意味着这样也可以呢:

    alert(fn1);
    if(true){
        var a = 1;
        function fn1(){
            alert(123);
        }
    }

在chrome中弹出fn1这个函数,唯独在火狐中就会报错,提示fn1未定义。这个例子想说明的是,一旦我们想要定义全局变量或全局函数,最好在外面定义。

再来看看这个例子:

    for(var i=o;i<aBtn.length;i++){
        aBtn[i].onclick = function(){
            alert(i);
            for(var i=0;i<aBtn.length;i++){
            aBtn[i].style.background="yellow";
            }
        }
    }

这段代码实现的是点击一个按钮实现所有按钮的颜色改变,不过这不是我们要讲的关键,关键在于alert(i);这段代码,这里会弹出结果是多少呢?是3吗?因为我们知道执行上面的for循环以后,i的最终值已经变成了3,但运行结果却是undefined。

原来,这里关键在于alert(i)后面的for当中已经声明了i,也就是var i;这段代码,将这个声明提前到onclick这个函数里面的前面,那么此时i即是undefined,如果里面的for循环不加一个var的话,弹出结果就是3。