javascript 作用域鏈及閉包,AO,VO,執行環境


下面的文章內容會根據理解程度不斷修正。

js變量作用域:

   定義:變量在它申明的函數體以及函數體內嵌套的任意函數體內有定義。

function AA(){

  var bb='我是AA內部變量';
  
   function TT(){
        alert(bb);
     }
   alert(bb);
    TT();
}
AA();

如上圖,兩次彈出的都是“我是AA內部變量”。

JS的變量作用域是函數級的,也就是在AA內部申明的變量,在AA內部任意位置,包括它嵌套的函數內也是有定義的。

在函數AA外面,bb就是沒有定義的。當然如果去掉bb前面var,bb變量就會自動變成全局變量,此時bb在函數AA外也會有效。

 

JS變量提升:

    定義:函數體內申明的變量,會被自動提前到最前面申明。

function AA(){
  alert(bb);//undefined
  alert(cc);//報錯,變量未定義
  var bb='我是AA內部變量';
  alert(bb);//我是AA內部變量
}

如上,會依次 返回 undefined,報錯,我是AA內部變量

第一次alert為什么沒有報錯呢?這就是變量提升的原因,js執行時會自動將變量申明提升到最前面,但是賦值並不會因此提升。

提升之后就等價於下面這樣。

function AA(){
var bb; //定義自動提前,不賦值, alert(bb);
bb='我是AA內部變量'; }

 

JS執行環境

   執行環境或者叫執行上下文,我理解為代碼執行時所處的環境,這個環境決定了它有權訪問哪些變量或者函數。

var bb='全局變量'
function AA(){
  var bb='局部變量';
  var s=function(){
    alert(bb);
  }
  s();
  return s;
}
var cc=AA();//局部變量
cc();       //局部變量

如上,依次進入的執行環境是window,AA,s

window---全局執行環境

AA,s ---函數執行環境

 

變量對象VO[variable object]  活動對象AO[activation object]

  每個執行環境都有一個變量對象,這個變量對象的屬性綁定了在這個環境里定義的所有變量和函數,形參。VO理解為代碼編譯時產生。

 VO綁定以下屬性:

1.函數形參

2.函數申明

3.變量申明

 函數被調用后,執行環境就切換成了對應的函數,此時活動對象就會產生。也就是說AO可以理解為函數執行時產生的。

 進入函數執行環境后,實際上AO就相當於函數的VO,只是說在函數執行環境里 VO屬性不能被直接訪問,所以生成AO來替代訪問。

var bb='全局變量'
function AA(y){
  var bb='局部變量';
  function s(){
    alert(bb);
  }
}
AA(5);

上面代碼依次進入的執行環境有兩個,首先window,然后是函數AA

全局執行環境window:VO綁定的屬性依次,函數AA,變量bb

函數AA執行環境:VO綁定的屬性依次是,形參y=5,函數s,變量bb

在全局執行環境中,VO屬性是可以被訪問的,而進入函數執行環境后VO屬性不能被直接訪問,此時會生成活動對象AO替代VO,可以訪問AO屬性。

VO/AO產生的過程也是變量提升的過程,優先提升函數,然后是變量。

此時VO[function]===AO

注:在函數執行環境中,用表達式的方式申明的函數,對應的函數表達式不會加入VO

function AA(){
var sub = function _sub(a, b){
    alert(typeof _sub);
    return a - b;
}

}

sub作為變量會加入VO,_sub作為函數表達式則不會加入。

 

JS作用域鏈

    作用域鏈包含了執行環境有權訪問的變量、函數的有序訪問。它是一個由變量對象(VO/AO)組成的單向鏈表,主要用來進行變量查找。

   JS內部有一個[[scope]]屬性,這個屬性就是指向作用域鏈的頂端。

var bb='全局變量'
function AA(y){
  var bb='局部變量';
  function s(){
var z=0; alert(bb); }
s(); } AA(
5);

暫且理解JS在代碼編譯時創建作用域鏈,分析上面的代碼的 作用域鏈:

全局執行環境:[[scope]]----->VO[AA,bb]  只有全局VO,[[scope]]直接指向VO。

函數AA執行環境:[[scope]]---->VO[[y,s,bb]VO[[AA,bb]],首先全局VO壓入棧,然后函數AA VO壓入棧頂,[[scope]]屬性指向棧頂,變量、函數搜索就從棧頂開始。

函數s執行環境:[[scope]]--->VO[[z]]VO[[y,s,bb]VO[[AA,bb]],首先全局VO壓入棧,然后依次AA,s壓入棧,s處於棧頂,[[scope]]屬性直接指向s的VO。

應用:比如調用s,進入s執行環境,在執行alert時,首先會去查找bb的申明,會先在作用域鏈的頂端查找,沒查到就會沿着鏈繼續往下查找,直到查到就停止。

總結:

函數執行時,將當前的函數的VO放在鏈表開頭,后面依次是上層函數,最后是全局對象。變量查找則依次從鏈表的頂端開始。JS有個內部屬性[[scope]],這個屬性包含了

函數的作用域對象的集合,這個集合就稱為函數的作用域鏈。它決定了,哪些變量或者函數能在當前函數中被訪問,以及它的訪問順序。

 

閉包

我理解為,函數能夠訪問另一個函數中的變量,這樣就構成了一個閉包。只要某個變量在另外一個函數中還存在引用,那么這個變量的值在內存中就不會被釋放,除非這個函數不會再執行。

function getvalue(){
 for(var i=0;i<10;i++){
     var yy=i;
      setTimeout(
      function(){
      var zz=document.getElementById('tt').innerText;
      zz+=',';
      zz+=yy;
      document.getElementById('tt').innerText=zz;
      }
      ,
      100)
    }
}

定時器中的匿名函數會在for結束之后依次執行,匿名函數中引用了變量yy,根據作用域鏈規則查找,首先在匿名函數中尋找yy的定義,沒有找到,然后去它的上層查找。

yy定義在getvalue中,又yy被其他函數引用着,所以它的結果不會被釋放。

而for循環執行結束之后yy的結果是9,所以最后的輸出會是:,9,9,9,9,9,9,9,9,9,9

那如果我們想要輸出0123456789怎么辦呢?看下面的代碼

function getvalue(){
for(var i=0;i<10;i++){
 var yy=i;
(
 function(yy)
 {
 setTimeout(
  function(){
  var zz=document.getElementById('tt').innerText;
  zz+=',';
  zz+=yy;
  document.getElementById('tt').innerText=zz;
  }
  ,
  100)
  }
  )(yy)
}
}

上面在定時器外面套了一個立即執行函數

(function(yy){

....

})(yy)

代碼執行到這個匿名函數時,這個匿名函數會自動執行,也就是for循環,每次循環到這里這個函數就會立即執行掉,並把參數yy傳入匿名函數中。

現在來看定時器中的匿名函數的作用域鏈。

[[scope]]--->VO[[變量z]] VO[[形參yy,匿名function]] VO[[匿名function,變量yy]] VO[[函數getvalue]]

此時,定時中的匿名函數引用的變量yy,從作用域鏈中查找可以發現,它來自於上層立即執行函數的形參,而立即執行函數是每次

for循環都會立即執行並把參數傳入。我們知道,只要某個變量在另外一個函數中還存在引用,那么這個變量的值在內存中就不會被釋放,

系統會每次把立即函數執行的形參傳入值保存起來,所以定時器的中的匿名函數在執行結果就會是下面這樣。

,0,1,2,3,4,5,6,7,8,9
每個函數都是一個執行環境,每個執行環境的[[scope]]都會指向一條作用域鏈。
可以認為,定時器外的函數會執行10次,函數每次執行,系統都會給他分配一個空間,這個空間保存了它的作用域鏈[[scope]],變量對象VO/AO[形參,函數申明,變量申明]等信息,
如果它的變量還會被其他函數繼續引用,比如定時器中的匿名函數,那么它就不會銷毀,等待被使用,反之執行完就會銷毀。
既然沒有銷毀,那么定時器中的匿名函數在執行時,就能獲取到想要的值。
這個例子中,定時器中的匿名函數與定時器外的立即執行函數構成閉包。

 

淺談this

   一般而言,this指向執行環境所處的環境,也就是函數被調用時所處的環境。看到資料說:函數調用f(x,y)其實內部是f.call(this, x, y)這樣執行的,所以this是在調用的時候傳入的。

如果函數是直接執行的那么this指向window,如果有調用者,那么this指向調用者。

var a=1;
var BB=function(){
  alert(this.a);
}

var DD={
   a:4,
   f:BB
}
BB();//1 this 指向window
var zz=DD.f;
zz(); //1 this指向window
DD.f(); //4 this指向f的調用者DD

上面的例子說明,作為對象方法時,this指向的是函數的調用者,直接調用或者在一般函數中調用時,this指向的就是全局對象

var a=1;
var BB=function(){
this.a=2;
}
var zz=new BB();//this 指向new出來的對象
alert(a); //1 所以this.a賦值不會影響全局變量中的a,此時的this指向的不是全局對象
alert(zz.a);//2 
BB();//直接執行,this指向window,執行之后,全局變量a的值被改變
alert(a); //2 

上面說明,作為構造函數時,函數里的this指向的是new出來的對象。

 

var a=1;
function BB(){
 alert(this.a);
}
function CC(){
this.a=3;
}
BB();  //1
var dd=new CC();
BB.apply(dd); //3 實際上this指向dd
BB.call(dd);//3   實際上this指向dd

上面說明,call和apply可以改變this的指向,使用call、apply時,傳入的第一個參數就會成為this的指向對象。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM