【原】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語句;

 

作個筆記,加深一下印象。有誤之處,歡迎各位大神指出

相關文章
相關標籤/搜索