【原】javascript执行环境及做用域

  最近在重读《javascript高级程序设计3》,以为应该写一些博客记录一下学习的一些知识,否则都忘光啦。今天要总结的是js执行环境和做用域。javascript

首先来讲一下执行环境前端

1、执行环境

书上概念,执行环境定义了变量或者函数有权访问的其余数据,决定了他们各自的行为。每一个执行环境都有一个与之关联的变量对象。环境中定义的全部变量和函数都保存在这个对象中。虽然咱们在编写代码的时候没法访问这个对象,但解析器在处理数据时会在后台用到它。java

  执行环境是一个概念,一种机制,它定义了变量或函数是否有权访问其余数据面试

 在javascript中,可执行的JavaScript代码分三种类型: 
       1. Global Code,即全局的、不在任何函数里面的代码,例如:一个js文件、嵌入在HTML页面中的js代码等。 
       2. Eval Code,即便用eval()函数动态执行的JS代码。 
       3. Function Code,即用户自定义函数中的函数体JS代码。 
c#

跳过Eval Code,只说全局执行环境和函数执行环境。浏览器

 

  一、全局环境:闭包

  全局环境是最外围的一个执行环境。全局执行环境被认为是window对象。所以全部全局变量和函数都是做为window对象的属性和方法建立的。代码载入浏览器时,全局执行环境被建立(当咱们关闭网页或者浏览器时全局执行环境才被销毁)。好比在一个页面中,第一次载入JS代码时建立一个全局执行环境。函数

  这也是为何闭包有一个内存泄露的缺点。由于闭包中外部函数被当成了全局环境。因此不会被销毁,一直保存在内存中。学习

  二、函数执行环境

  每一个函数都有本身的执行环境,当执行进入一个函数时,函数的执行环境就会被推入一个执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给以前执行环境。这就是ECMAScript程序中的执行流。this

  也能够这样解读:当调用一个 JavaScript 函数时,该函数就会进入与该函数相对应的执行环境。若是又调用了另一个函数,则又会建立一个新的执行环境,而且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。于是,运行中的 JavaScript 代码就构成了一个执行环境栈。

  当函数被调用时函数的局部环境被建立(函数内的代码执行完毕后,该环境被销毁,同时保存在其中的全部变量和函数定义也随之被销毁)。

 

    2-1定义期

 

  函数定义的时候,都会建立一个[[scope]]属性,通这个对象对应的是一个对象的列表,列表中的对象仅能javascript内部访问,无法经过语法访问。

  (scope也就是做用域的意思。)

 

  咱们定义一全局函数A,那么A函数就建立了一个A的[[scope]]属性。此时,[[scope]]里面只包含了全局对象【Global Object】。

  而若是, 咱们在A的内部定义一个B函数,那B函数一样会建立一个[[scope]]属性,B的[[scope]]属性包含了两个对象,一个是A的活动对象Activation Object、一个是全局对象,A的活动对象在前面,全局对象排在后面。

  简而言之,一个函数的[Scope]属性中对象列表的顺序是上一层函数的Activation Object对象,而后是上上层的,一直到最外层的全局对象。

 

 

下面是示例代码:A只有一个scope,B有两个scope

// 外部函数
function A(){
     var somevar;
        
     // 内部函数
    function B(){
         var somevar;
     }
}

 

    2-2执行期

 

    当函数被执行的时候,就是进入这个函数的执行环境,首先会创一个它本身的活动对象【Activation Object】(这个对象中包含了this、参数(arguments)、局部变量(包括命名的参数)的定义和一个变量对象的做用域链[[scope chain]],而后,把这个执行环境的[scope]按顺序复制到[[scope chain]]里,最后把这个活动对象推入到[[scope chain]]的顶部。这样[[scope chain]]就是一个有序的栈,这样保了对执行环境有权访问的全部变量和对象的有序访问。

// 第一步页面载入创全局执行环境global executing context和全局活动象
// 定义全局[[scope]],只含有Window对象
// 扫描全局的定义变量及函数对象:color【undefined】、changecolor【FD建立changecolor的[[scope]],此时里面只含有全局活动对象】,加入到window中,因此全局变量和全局函数对象都是作为window的属性定义的。
// 程序已经定义好因此在此执行环境内任何位置均可以执行changecolor(),color也已经被定义,可是它的值是undefined

// 第二步color赋值"blue"
var color = "blue";

// 它是不须要赋值的,它就是引用自己
function changecolor() {
    // 第四步进入changecolor的执行环境
    // 复制changecolor的[[scope]]到scope chain
    // 建立活动对象,扫描定义变量和定义函数,anothercolor【undefined】和swapcolors【FD建立swapcolors的[[scope]]加入changecolor的活动对象和全局活动对象】加入到活动对象,活动对象中同时还要加入arguments和this
    // 活动对象推入scope chain 顶端
    // 程序已经定义好因此在此执行环境内任何位置均可以执行swapcolors(),anothercolor也已经被定义好,但它的值是undefined
    
    // 第五anothercolor赋值"red"
    var anothercolor = "red";
    
    // 它是不须要赋值的,它就是引用自己
    function swapcolors() {
        // 第七步进入swapcolors的执行环境,建立它的活动对象
        // 复制swapcolors的[[scope]]到scope chain
        // 扫描定义变量和定义函数对象,活动对象中加入变量tempcolor【undefined】以及arguments和this
        // 活动对象推入scope chain 顶端
        
        // 第八步tempcolor赋值anothercolor,anothercolor和color会沿着scope chain被查到,并继续往下执行
        var tempcolor = anothercolor;
            anothercolor = color;
            color = tempcolor;    
    }

    // 第六步执行swapcolors,进入其执行环境
    swapcolors();
}

// 第三步执行changecolor,进入其执行环境
changecolor();

 

   2-3访问标识符:

  当执行js代码的过程当中,遇到一个标识符,就会根据标识符的名称,在执行上下文(Execution Context)的做用域链中进行搜索。从做用域链的第一个对象(该函数的Activation Object对象)开始,若是没有找到,就搜索做用域链中的下一个对象,如此往复,直到找到了标识符的定义。若是在搜索完做用域中的最后一个对象,也就是全局对象(Global Object)之后也没有找到,则会抛出一个错误,提示undefined。

 

 

2、Scope/Scope Chain(做用域/做用域链)

   当代码在一个环境中执行时,都会建立一个做用域链。 做用域链的用途是保证对执行环境有权访问的全部变量和函数的有序访问。整个做用域链是由不一样执行位置上的变量对象按照规则所构建一个链表。做用域链的最前端,始终是当前正在执行的代码所在环境的变量对象。

  若是这个环境是函数,则将其活动对象(activation object)做为变量对象。活动对象在最开始时只包含一个变量,就是函数内部的arguments对象。做用域链中的下一个变量对象来自该函数的包含环境,而再下一个变量对象来自再下一个包含环境。这样,一直延续到全局执行环境,全局执行环境的Variable Object始终是做用域链中的最后一个对象。

如图所示:

  书中例子:

 var color="blue";
 function changecolor(){
    var anothercolor="red";
    function swapcolors(){
    var tempcolor=anothercolor;
    anothercolor=color;
    color=tempcolor;
       // Todo something        
     }
    swapcolors();
}
changecolor();
 //这里不能访问tempcolor和anocolor;可是能够访问color;
alert("Color is now  "+color);

 

    经过上面的分析,咱们能够得知内部环境能够经过做用域链访问全部的外部环境,但外部环境不能访问内部环境中的任何变量和函数。

  这些环境之间是线性、有次序的。每一个环境均可以向上搜索做用域链,以便查询变量和函数名;但任何环境不能经过向下搜索做用域链条而进入另外一个执行环境

  对于上述例子的swapcolor()函数而言,其做用域链包括:swapcolor()的变量对象、changecolor()变量对象和全局对象。swapcolor()的局部环境开始先在本身的Variable Object中搜索变量和函数名,找不到,则向上搜索changecolor做用域链。。。。。以此类推。可是,changecolor()函数是没法访问swapcolor中的变量

 

启示:尽可能使用局部变量,可以减小搜索的时间

 

一、没有块级做用域

与C、C++以及JAVA不一样,Javscript没有块级做用域。看下面代码

if(true){
        var myvar = "张三";    
    }
    alert(myvar);// 张三

若是有块级做用域,外部是访问不到myvar的。再看下面

 

for (var i=0;i<10;i++){
            console.log(i)    
        }
        
        alert(i); // 10

对于有块级做用域的语言来讲,好比java或是c#代码,i作为for初始化的变量,在for以外是访问不到的。由于i只存在于for循环体重,在运行完for循环后,for中的全部变量就被销毁了。而在javascript中则不是这样的,在for中的变量声明将会添加到当前的执行环境中(这里是全局执行环境),所以在for循环完后,变量i依旧存在于循环外部的执行环境。所以,会输出10。

 

二、声明变量

   使用var声明变量时,这个变量将被自动添加到距离最近的可用环境中。对于函数内部,最接近的环境就是函数的局部变量。若是初始化变量时没有使用var,该变量会自动添加到全局函数中。

代码以下:

var name = "小明";
function getName(){
    alert( name  );    //'undefined'
    var name = '小黄';
    alert(name  );    //小黄
}
getName()

为何第一个name是undefined呢。这是由于,javascript解析器,进入一个函数执行环境,先对var 和 function进行扫描。

至关于会把var或者function【函数声明】声明提高到执行环境顶部。

也就是说,进入咱们的getName函数的时候,标识符查找机制查找到了var,查找的name是局部变量name,而不是全局的name,由于函数里面的name被提高到了顶部。

以前这里面试的时候被坑过,切记切记!!

上面的代码会被解析成下面这样:

var name = "小明";
function getName(){
    var name;
    alert( name  );    //'undefined'
    var name = '小黄';
    alert(name  );    //小黄
}
getName()

 

 

延长做用域链:

  虽然执行环境只有两种——全局做用域和函数做用域,可是仍是能够经过某种方式来延长做用域链。由于有些语句能够在做用域链的顶部增长一个临时的变量对象。
有两种状况会发生这种现象:
一、try-catch语句的catch块;
二、with语句;

 

作个笔记,加深一下印象。有误之处,欢迎各位大神指出