JavaScript做用域鏈詳解

JavaScript的做用域鏈仍是頗有味道的,搞懂了這個知識點,閉包的問題也就迎刃而解咯前端

 

一、JavaScript的全局變量和局部變量閉包

  首先,先來看看js的全局變量和局部變量,js不是塊級做用域,因此不能把你學過的C/C++做用域的知識用在js中!前端優化

(1)全局變量函數

  js的全局變量也能夠看作window對象的屬性,這句話怎麼理解,請看如下代碼:優化

var x = 10;
alert(window.x);//彈出10

  也就是說var x = 10;等價於window.x=10;spa

  再來看一段代碼3d

function foo(){
    x = 10;
}
foo();
alert(window.x);

  這會彈出什麼呢?answer is 10!code

  若是在函數中定義變量時沒有用關鍵字var,那麼實際上定義的就是全局變量。你常常會看到前端優化的一個點:儘可能少定義全局變量!若是不可避免的用到全局變量,那麼就在局部變量中保存。像這樣:對象

function foo(){
    var doc = document;
    var divObj = doc.getElementByTagName('div');
}

  把document對象保存在局部變量doc中,而後對doc進行操做。blog

  可是,問題來了,爲何這樣能提升效率呢?這個問題咱們先留着,等講完做用域鏈再來看。

(2)局部變量

  要說塊級做用域,那麼在js中就只有函數塊,函數中定義的變量就是局部變量,固然必須有關鍵字var!(沒有關鍵字var定義的都是全局變量)

也就是說if else語句和for循環中建立的變量在外部均可以訪問的到

function foo(){
    var x = 1;
}
for(var i = 0;i<10;i++){
    
}
if(i){
    var y = 10;
}
foo();
alert(i);//10
alert(y);//10
alert(x);//error x is not defined

 

二、做用域鏈

  這是重點咯,什麼是做用域鏈,仍是經過代碼來解釋

var x = 1;
function foo(){
    var y = 2;
    
    function bar(){
        var z = 3;
        alert(x+y+z);
    }
    bar();
}
foo();

答案是幾不用說吧,在bar函數中沒有y和z,執行x+y+z時,js搜索x,y,z變量的一種機制就是做用域鏈,這個例子的搜索順序:bar->foo->window

前面講的太簡單,可能已經有人看不下去了,來點乾貨吧

bar的做用域鏈是:

barScopeChain = [ bar.AO, foo.AO, global.VO ];

foo的做用域鏈是:

fooScopeChain = [
    foo.AO,
    global.VO
];

可能各位看官都會迷糊,可能會問,這個AO,VO,是個什麼玩意兒?咱們慢慢來,先來看看變量bar函數變量搜尋過程

例如:找x變量;bar函數在搜尋變量x的過程當中,先從自身AO對象上找,若是bar.AO存在這個屬性,那麼會直接使用這個屬性的值,若是不存在,則會轉到父級函數的AO對象,也就是foo.AO

若是找到x屬性則使用,找不到繼續 在global.VO對象查找,找到x的屬性,返回屬性值。若是在global.VO中沒有找到,則會拋出異常ReferenceError。

  在函數執行過程當中,每遇到一個變量,都會檢索從哪裏獲取和存儲數據,該過程從做用域鏈頭部,也就是從活動對象開始搜索,查找同名的標識符,若是找到了就使用這個標識符對應的變量,若是沒有則繼續搜索做用域鏈中的下一個對象,若是搜索完全部對象都未找到,則認爲該標識符未定義,函數執行過程當中,每一個標識符都要經歷這樣的搜索過程。

知道了這一點,回過頭看剛開始的那個問題,爲何要少定義全局變量,從而進行優化?

由於做用域鏈是棧的結構,全局變量在棧底,每次訪問全局變量都會遍歷一次棧,這樣確定會影響效率。

在函數建立時,每一個函數都會建立一個活動對象Active Object(AO),全局對象爲Global Object(VO),建立函數的過程也就是爲這個對象添加屬性的過程,做用域鏈就是由這些綁定了屬性的活動對象構成的。

  在函數執行的過程當中,會建立函數的執行上下文,這裏就不作過多解釋,能夠去看Tom大叔的深刻理解JavaScript系列

  執行上下文是一個動態的概念,當函數運行的時候建立,活動對象 Active Object 也是一個動態的概念,它是被執行上下文的做用域鏈引用的,能夠得出結論:執行上下文和活動對象都是動態概念,而且執行上下文的做用域鏈是由函數做用域鏈初始化的。PS:這些概念都是js引擎解析代碼的內部機制,外部是沒法訪問的!

  

  仍是剛纔那段代碼,咱們來看看js引擎編譯的過程,進一步瞭解具體是怎麼建立做用域鏈的

  函數進入全局,建立VO對象,綁定x屬性<入棧>(這裏只是預解析,爲ao對象綁定聲明的屬性,函數執行時纔會執行賦值語句,因此值是underfind)

global.VO = {
   x:underfind;
   foo:reference of function 
}

  遇到foo函數,建立foo.VO,綁定y屬性<入棧>

foo.AO = {
   y:undefined;
   fbar:reference of function 
}

  接下來是bar函數,z屬性<入棧>

bar.AO = {
   z:undefined;
}

做用域鏈和執行上下文都會保存在堆棧中,因此

bar函數的scope chain爲[0]bar.AO-->[1]foo.AO-->[2]global.VO

foo函數的scope chain爲[0]foo.AO-->[1]global.VO

這裏有一個等式:scope=AO|VO + [[scope]]

函數scope等於自身的AO對象加上父級的scope,也能夠理解爲一個函數的做用域等於自身活動對象加上父級做用域。

 

三、來看一個閉包的例子

個人本意是給每一個li標籤綁定一個點擊事件,點擊後彈出對應的索引,單實際上每次都會彈出「這是第四個li標籤」

分析:在建立foo函數時,foo.AO={liObj:undefined,i:undefined,onclick:refeerence of function}

在函數執行中,這個匿名函數自身沒有i這個變量,因此會到foo的活動對象中找i變量,此時for循環已執行,變量i的值已經改變,因此老是會輸出4

那怎麼解決這個問題呢?也很簡單

第一種解決方法:

用一個函數來保存變量i便可

第二種解決方法:

細心的人絕對會發現實際上是一種方法,都是把全局變量放在局部保存

 

最後一個點:變量查找時原型鏈是優先於做用域鏈的。js引擎先在函數AO對象查找,再到原型鏈查找,接着是做用域鏈。

還沒寫完,今天實在沒什麼感受,腦子很亂,仍是出去走走,透透氣吧,有不對的地方還但願好心人指出。

相關文章
相關標籤/搜索