javascript執(zhí)行環(huán)境及作用域詳解

字號:


    這篇文章主要為大家詳細介紹了javascript執(zhí)行環(huán)境及作用域,分別針對javascript執(zhí)行環(huán)境及作用域進行探討,感興趣的小伙伴們可以參考一下
    最近在重讀《javascript高級程序設計3》,覺得應該寫一些博客記錄一下學習的一些知識,不然都忘光啦。今天要總結(jié)的是js執(zhí)行環(huán)境和作用域。
    首先來說一下執(zhí)行環(huán)境 
    一、執(zhí)行環(huán)境
    書上概念,執(zhí)行環(huán)境定義了變量或者函數(shù)有權訪問的其他數(shù)據(jù),決定了他們各自的行為。每個執(zhí)行環(huán)境都有一個與之關聯(lián)的變量對象。環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。雖然我們在編寫代碼的時候無法訪問這個對象,但解析器在處理數(shù)據(jù)時會在后臺用到它。
    執(zhí)行環(huán)境是一個概念,一種機制,它定義了變量或函數(shù)是否有權訪問其他數(shù)據(jù)
    在javascript中,可執(zhí)行的JavaScript代碼分三種類型: 
    1. Global Code,即全局的、不在任何函數(shù)里面的代碼,例如:一個js文件、嵌入在HTML頁面中的js代碼等。 
    2. Eval Code,即使用eval()函數(shù)動態(tài)執(zhí)行的JS代碼。 
    3. Function Code,即用戶自定義函數(shù)中的函數(shù)體JS代碼。
    跳過Eval Code,只說全局執(zhí)行環(huán)境和函數(shù)執(zhí)行環(huán)境。 
    1、全局環(huán)境:
    全局環(huán)境是最外圍的一個執(zhí)行環(huán)境。全局執(zhí)行環(huán)境被認為是window對象。因此所有全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的。代碼載入瀏覽器時,全局執(zhí)行環(huán)境被創(chuàng)建(當我們關閉網(wǎng)頁或者瀏覽器時全局執(zhí)行環(huán)境才被銷毀)。比如在一個頁面中,第一次載入JS代碼時創(chuàng)建一個全局執(zhí)行環(huán)境。
    這也是為什么閉包有一個內(nèi)存泄露的缺點。因為閉包中外部函數(shù)被當成了全局環(huán)境。所以不會被銷毀,一直保存在內(nèi)存中。
    2、函數(shù)執(zhí)行環(huán)境
    每個函數(shù)都有自己的執(zhí)行環(huán)境,當執(zhí)行進入一個函數(shù)時,函數(shù)的執(zhí)行環(huán)境就會被推入一個執(zhí)行環(huán)境棧的頂部并獲取執(zhí)行權。當這個函數(shù)執(zhí)行完畢,它的執(zhí)行環(huán)境又從這個棧的頂部被刪除,并把執(zhí)行權并還給之前執(zhí)行環(huán)境。這就是ECMAScript程序中的執(zhí)行流。
    也可以這樣解讀:當調(diào)用一個 JavaScript 函數(shù)時,該函數(shù)就會進入與該函數(shù)相對應的執(zhí)行環(huán)境。如果又調(diào)用了另外一個函數(shù),則又會創(chuàng)建一個新的執(zhí)行環(huán)境,并且在函數(shù)調(diào)用期間執(zhí)行過程都處于該環(huán)境中。當調(diào)用的函數(shù)返回后,執(zhí)行過程會返回原始執(zhí)行環(huán)境。因而,運行中的 JavaScript 代碼就構(gòu)成了一個執(zhí)行環(huán)境棧。
    當函數(shù)被調(diào)用時函數(shù)的局部環(huán)境被創(chuàng)建(函數(shù)內(nèi)的代碼執(zhí)行完畢后,該環(huán)境被銷毀,同時保存在其中的所有變量和函數(shù)定義也隨之被銷毀)。
    2-1定義期 
    函數(shù)定義的時候,都會創(chuàng)建一個[[scope]]屬性,通這個對象對應的是一個對象的列表,列表中的對象僅能javascript內(nèi)部訪問,沒法通過語法訪問。
    (scope也就是作用域的意思。) 
    我們定義一全局函數(shù)A,那么A函數(shù)就創(chuàng)建了一個A的[[scope]]屬性。此時,[[scope]]里面只包含了全局對象【Global Object】。
    而如果, 我們在A的內(nèi)部定義一個B函數(shù),那B函數(shù)同樣會創(chuàng)建一個[[scope]]屬性,B的[[scope]]屬性包含了兩個對象,一個是A的活動對象Activation Object、一個是全局對象,A的活動對象在前面,全局對象排在后面。
    簡而言之,一個函數(shù)的[Scope]屬性中對象列表的順序是上一層函數(shù)的Activation Object對象,然后是上上層的,一直到最外層的全局對象。  
    下面是示例代碼:A只有一個scope,B有兩個scope
    // 外部函數(shù)
    function A(){
      var somevar;
      // 內(nèi)部函數(shù)
     function B(){
       var somevar;
      }
    }
    2-2執(zhí)行期 
    當函數(shù)被執(zhí)行的時候,就是進入這個函數(shù)的執(zhí)行環(huán)境,首先會創(chuàng)一個它自己的活動對象【Activation Object】(這個對象中包含了this、參數(shù)(arguments)、局部變量(包括命名的參數(shù))的定義和一個變量對象的作用域鏈[[scope chain]],然后,把這個執(zhí)行環(huán)境的[scope]按順序復制到[[scope chain]]里,最后把這個活動對象推入到[[scope chain]]的頂部。這樣[[scope chain]]就是一個有序的棧,這樣保了對執(zhí)行環(huán)境有權訪問的所有變量和對象的有序訪問。
    // 第一步頁面載入創(chuàng)全局執(zhí)行環(huán)境global executing context和全局活動象
    // 定義全局[[scope]],只含有Window對象
    // 掃描全局的定義變量及函數(shù)對象:color【undefined】、changecolor【FD創(chuàng)建changecolor的[[scope]],此時里面只含有全局活動對象】,加入到window中,所以全局變量和全局函數(shù)對象都是做為window的屬性定義的。
    // 程序已經(jīng)定義好所以在此執(zhí)行環(huán)境內(nèi)任何位置都可以執(zhí)行changecolor(),color也已經(jīng)被定義,但是它的值是undefined
    // 第二步color賦值"blue"
    var color = "blue";
    // 它是不需要賦值的,它就是引用本身
    function changecolor() {
     // 第四步進入changecolor的執(zhí)行環(huán)境
     // 復制changecolor的[[scope]]到scope chain
     // 創(chuàng)建活動對象,掃描定義變量和定義函數(shù),anothercolor【undefined】和swapcolors【FD創(chuàng)建swapcolors的[[scope]]加入changecolor的活動對象和全局活動對象】加入到活動對象,活動對象中同時還要加入arguments和this
     // 活動對象推入scope chain 頂端
     // 程序已經(jīng)定義好所以在此執(zhí)行環(huán)境內(nèi)任何位置都可以執(zhí)行swapcolors(),anothercolor也已經(jīng)被定義好,但它的值是undefined
     // 第五anothercolor賦值"red"
     var anothercolor = "red";
     // 它是不需要賦值的,它就是引用本身
     function swapcolors() {
      // 第七步進入swapcolors的執(zhí)行環(huán)境,創(chuàng)建它的活動對象
      // 復制swapcolors的[[scope]]到scope chain
      // 掃描定義變量和定義函數(shù)對象,活動對象中加入變量tempcolor【undefined】以及arguments和this
      // 活動對象推入scope chain 頂端
      // 第八步tempcolor賦值anothercolor,anothercolor和color會沿著scope chain被查到,并繼續(xù)往下執(zhí)行
      var tempcolor = anothercolor;
       anothercolor = color;
       color = tempcolor; 
     }
     // 第六步執(zhí)行swapcolors,進入其執(zhí)行環(huán)境
     swapcolors();
    }
    // 第三步執(zhí)行changecolor,進入其執(zhí)行環(huán)境
    changecolor();
    2-3訪問標識符:
    當執(zhí)行js代碼的過程中,遇到一個標識符,就會根據(jù)標識符的名稱,在執(zhí)行上下文(Execution Context)的作用域鏈中進行搜索。從作用域鏈的第一個對象(該函數(shù)的Activation Object對象)開始,如果沒有找到,就搜索作用域鏈中的下一個對象,如此往復,直到找到了標識符的定義。如果在搜索完作用域中的最后一個對象,也就是全局對象(Global Object)以后也沒有找到,則會拋出一個錯誤,提示undefined。  
    二、Scope/Scope Chain(作用域/作用域鏈)
     當代碼在一個環(huán)境中執(zhí)行時,都會創(chuàng)建一個作用域鏈。 作用域鏈的用途是保證對執(zhí)行環(huán)境有權訪問的所有變量和函數(shù)的有序訪問。整個作用域鏈是由不同執(zhí)行位置上的變量對象按照規(guī)則所構(gòu)建一個鏈表。作用域鏈的最前端,始終是當前正在執(zhí)行的代碼所在環(huán)境的變量對象。
    如果這個環(huán)境是函數(shù),則將其活動對象(activation object)作為變量對象?;顒訉ο笤谧铋_始時只包含一個變量,就是函數(shù)內(nèi)部的arguments對象。作用域鏈中的下一個變量對象來自該函數(shù)的包含環(huán)境,而再下一個變量對象來自再下一個包含環(huán)境。這樣,一直延續(xù)到全局執(zhí)行環(huán)境,全局執(zhí)行環(huán)境的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);
       通過上面的分析,我們可以得知內(nèi)部環(huán)境可以通過作用域鏈訪問所有的外部環(huán)境,但外部環(huán)境不能訪問內(nèi)部環(huán)境中的任何變量和函數(shù)。
    這些環(huán)境之間是線性、有次序的。每個環(huán)境都可以向上搜索作用域鏈,以便查詢變量和函數(shù)名;但任何環(huán)境不能通過向下搜索作用域鏈條而進入另一個執(zhí)行環(huán)境。
    對于上述例子的swapcolor()函數(shù)而言,其作用域鏈包括:swapcolor()的變量對象、changecolor()變量對象和全局對象。swapcolor()的局部環(huán)境開始先在自己的Variable Object中搜索變量和函數(shù)名,找不到,則向上搜索changecolor作用域鏈。。。。。以此類推。但是,changecolor()函數(shù)是無法訪問swapcolor中的變量
     啟示:盡量使用局部變量,能夠減少搜索的時間 
    1、沒有塊級作用域
    與C、C++以及JAVA不同,Javscript沒有塊級作用域??聪旅娲a:
    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循環(huán)體重,在運行完for循環(huán)后,for中的所有變量就被銷毀了。而在javascript中則不是這樣的,在for中的變量聲明將會添加到當前的執(zhí)行環(huán)境中(這里是全局執(zhí)行環(huán)境),因此在for循環(huán)完后,變量i依舊存在于循環(huán)外部的執(zhí)行環(huán)境。因此,會輸出10。
     2、聲明變量
       使用var聲明變量時,這個變量將被自動添加到距離最近的可用環(huán)境中。對于函數(shù)內(nèi)部,最接近的環(huán)境就是函數(shù)的局部變量。如果初始化變量時沒有使用var,該變量會自動添加到全局函數(shù)中。
    代碼如下:
    var name = "小明";
    function getName(){
     alert( name ); //'undefined'
     var name = '小黃';
     alert(name ); //小黃
    }
    getName()
    為什么第一個name是undefined呢。這是因為,javascript解析器,進入一個函數(shù)執(zhí)行環(huán)境,先對var 和 function進行掃描。
    相當于會把var或者function【函數(shù)聲明】聲明提升到執(zhí)行環(huán)境頂部。
    也就是說,進入我們的getName函數(shù)的時候,標識符查找機制查找到了var,查找的name是局部變量name,而不是全局的name,因為函數(shù)里面的name被提升到了頂部。
    上面的代碼會被解析成下面這樣:
    var name = "小明";
    function getName(){
     var name;
     alert( name ); //'undefined'
     var name = '小黃';
     alert(name ); //小黃
    }
    getName()
    延長作用域鏈:
    雖然執(zhí)行環(huán)境只有兩種——全局作用域和函數(shù)作用域,但是還是可以通過某種方式來延長作用域鏈。因為有些語句可以在作用域鏈的頂部增加一個臨時的變量對象。
    有兩種情況會發(fā)生這種現(xiàn)象:
    1、try-catch語句的catch塊;
    2、with語句;
    以上就是本文的全部內(nèi)容,希望對大家學習理解javascript執(zhí)行環(huán)境及作用域有所幫助。