Javascript 执行环境和作用域

  *javascript引擎内部在执行代码以前到底做了些什么?为什么某些函数以及变量在没有被声明以前就可以被使用?以及它们的最终的值是怎样被定义的?

  伴随着这些问题,再深入学习一下Javascript的执行环境、作用域,以备后续的闭包学习。

一、执行上下文

  在Javascript中有三种代码执行环境:

  • Global Code

  全局级别的代码。这个是最默认的代码执行环境,是最外围的一个执行环境(在web浏览器中,全局执行环境被认为是window对象)。一旦代码被载入,引擎最先进入的是这个环境。

  • Function Code

  函数级别的代码。当执行一个函数时,运行函数体中的代码。

  • Eval Code

  在Eval函数中运行的代码

  先看一个栗子:

       var color = "blue";
        function changeColor(){
            var anotherColor = "red";
            function swapColors(){
                var tempColor = anotherColor;
                anotherColor = color;
                color = tempColor;
            }
            swapColors();
        }
        changeColor();
        alert("color is now:"+color);  //red
        alert("tempColor:"+tempColor); //wrong

  在这个例子中,总共有三个执行环境:全局环境、changeColor()的局部环境、swapColors()的局部环境。为什么在swapColors()函数中,可以访问幷改变变量color的值?最后在全局环境中却不能访问swapColors()函数中定义的变量tempColor?

  这又得学习执行上下文堆栈。

二、执行上下文堆栈

  在浏览器中,javascript引擎的工作方式是单线程的。也就是说,某一时刻只有唯一的一个事件是被激活处理的,其它的事件被放入队列中,等待被处理。而每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

  

 三、建立阶段和代码执行阶段

  每当调用一个函数时,一个新的执行上下文就会被创建出来。然而,在javascript引擎内部,这个上下文的创建过程具体分为两个阶段:(1)建立阶段;(2)执行阶段

  Javascript引擎在每个阶段都做什么工作呢?

  先看几个例子:

    alert(message);
        var message;

  结果:

  

  恩??不是应该报错吗?可是却没有,Javascript引擎在执行alert函数时,貌似已经知道message已经被声明了,那如果在后边给初始化一下呢,在执行alert函数时,会不会知道这个message变量在后边被初始化了呢?

  测试下:  

    alert(message);
       var message = 10;

  结果:

  恩??看来Javascript引擎在执行alert函数前就已经声明了变量message,所以顺序是:声明变量message->执行alert()->初始化变量message。

  再看个例子:

     console.log(fun);
        function fun(a,b){
            return a+b;
        }

  结果:

  

  由此可知,在Javascript引擎执行console.log()方法时就已经知道fun是一个方法,而且还知道方法里的具体操作。

  那再看:

     console.log(f);
        var f = function fun(a,b){
            return a+b;
        }

  结果:

  

  在我们定义一个函数有几种方式,其中函数声明和函数表达式这两种情况在Javascript引擎工作时却有着差别。后者的f会被先申明。

  根据查看的资料可知:Javascript引擎在建立阶段主要做三件事:

  1. 建立变量,函数,arguments对象,参数
  2. 建立作用域链(后续还得继续学习)
  3. 确定this的值(后续还得继续学习)

  实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性:

 executionContextObj = {
   variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
   scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
   this: {}
 }

  而在代码执行阶段主要工作就是变量赋值,函数引用以及执行其它代码。

四、执行流程(http://blogread.cn/it/article/6178)

  确切地说,执行上下文对象(上述的executionContextObj)是在函数被调用时,但是在函数体被真正执行以前所创建的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 - 建立阶段。这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,然后基于这些信息建立执行上下文对象(executionContextObj)。在这个阶段,variableObject对象,作用域链,以及this所指向的对象都会被确定。

上述第一个阶段的具体过程如下:

  1. 找到当前上下文中的调用函数的代码

  2. 在执行被调用的函数体中的代码以前,开始创建执行上下文

  3. 进入第一个阶段-建立阶段:

    • 建立variableObject对象:

      1. 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值

      2. 检查当前上下文中的函数声明:

        • 每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用

        • 如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。

      3. 检查当前上下文中的变量声明:

        • 每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined。

        • 如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。

    • 初始化作用域链

    • 确定上下文中this的指向对象

  4. 代码执行阶段:

    • 执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。

  通过例子看下:

     var a =10,
               fn,
               bar = function(x){
                   var b = 5;
                   fn(x+b);
               }
        fu = function(y){
            var c = 5;
            console.log(y+c);
        }
        bar(10);

  在执行代码之前,首先会创建全局的上下文环境。

全局上下文环境
a undefined
fn undefined
bar undefined
this window

  

  然后是代码执行。在代码执行bar(10);之前,上下文的变量都被赋值。

全局上下文环境
a 10
fn function
bar function
this window

  然后就是执行bar(10);。在执行函数体语句之前,Javascript引擎先会进入准备阶段。创建一个新的执行上下文环境。

bar函数的上下文环境
b undefined
x 10
arguments

{

0:10,

length:1

}

this window
scope {...}

  执行开始,走到语句var b = 5;时,函数上下文变化为;

  

bar函数的上下文环境
b 5
x 10
arguments

{

0:10,

length:1

}

this window
scope {...}

  然后又走到fn(x+b);时调用函数fn,此时又会创建fn函数的上下文。

fn函数的上下文环境
c undefined
y 15
arguments

{

0:15,

length:1

}

this window
scope {...}

  然后开始执行fn函数的代码,fn函数的上下文变为:

fn函数的上下文环境
c 5
y 15
arguments

{

0:15,

length:1

}

this window
scope {...}

  fn函数的代码执行完后,fn函数的上下文就会被销毁,然后继续执行bar函数的代码,执行完bar函数的代码后,销毁bar函数的上下文。

  整个上下文的堆栈过程如图所示:

  

  这下,我们就知道为什么在调用之后声明都不会出错了~

推荐:

http://www.cnblogs.com/wangfupeng1988/

http://web.jobbole.com/84044/

原文地址:https://www.cnblogs.com/niulina/p/5695950.html