JS基礎知識:變量對象、做用域鏈和閉包

前言:這段時間一直在消化做用域鏈和閉包的相關知識。以前看《JS高程》和一些技術博客,對於這些概念的論述多多少少不太清楚或者不太完整,包括一些大神的技術文章。這也給個人學習上形成了一些困惑,這幾個概念的理解也是始終處於一個半懂不懂的狀態。後來在某公衆號看到了@波同窗的基礎文章,這應該是我所看到的最清楚,最全面,最好懂的文章了。因此我在學習之餘決定寫一篇文章,總結學到的知識點,用個人理解來闡述,不足之處,見請諒解。

執行上下文(Execution Context)

也叫執行環境,也能夠簡稱「環境」。是JS在執行過程當中產生的,當JS執行一段可執行的代碼時,就會生成一個叫執行環境的東西。JS中每一個函數都會有本身的執行環境,當函數執行時,就生成了它的執行環境,執行上下文會生成函數的做用域。前端

除了函數有執行環境,還有全局的環境。在JS中,每每不止一個執行環境。數組

讓咱們先來看一個栗子:閉包

var a=10;
function foo(){
  var b=5;
  function fn(){
    var c=20;
    var d=100;
  }
  fn();
}
foo();

在這個栗子中,包括了三個執行環境:全局環境,foo()執行環境,fn()執行環境;函數

執行環境的處理機制

在這裏咱們要了解到執行上下文的第一個特色:內部的環境能夠訪問外部的環境,而外部的環境沒法訪問內部的環境。學習

例如:咱們能夠在fn()中訪問到位於foo()中的b,在全局環境中的a,而在foo()中卻沒法訪問到c或者d。this

爲何會這樣,這就要了解JS處理代碼的一個機制了。code

咱們知道JS的處理過程是以堆棧的方式來處理,JS引擎會把執行環境一個個放入棧裏,而後先放進去的後處理,後放進去的先處理,上面這個栗子,最早被放進棧中的是全局環境,而後是foo(),再是fn(),而後處理完一個拿出一個來,因此咱們知道爲何foo()不能訪問fn()裏的了,由於它已經走了。對象

執行環境的生命週期

好了,瞭解完執行環境的的處理方式,咱們要說明執行環境的生命週期。
執行環境的生命週期分爲兩個階段,這兩個階段描述了執行環境在棧裏面作了些什麼。教程

  1. 建立階段;
  2. 執行階段

建立階段

執行環境在建立階段會完成這麼幾個任務:1.生成變量對象;2.創建做用域鏈;3.肯定this指向索引

執行階段

到了執行階段,會給變量賦值,函數引用,而後還有執行其餘的代碼。

完成了這兩個步驟,執行環境就能夠準備出棧,一路走好了。

以上就是執行環境的具體執行內容。上面提到了執行環境在建立階段會生成變量對象,這也是一個很重要的概念,咱們下文會詳細論述。

變量對象(variable object)

變量對象是什麼呢?《JS高程》是這樣說的:「每一個執行環境都有與之關聯的變量對象,環境中定義的全部變量和函數都保存在這個對象中。」

那變量對象裏有些什麼東西呢?看下文:

變量對象的內容

在變量對象建立時,通過了這樣三個步驟:

  1. 生成arguments屬性;
  2. 找到function函數聲明,建立屬性;
  3. 找到var變量聲明,建立屬性

其中值得注意的是:function函數聲明的級別比var變量聲明的級別要高,因此在實際執行的過程當中會先尋找function的聲明。

還須要注意的是:在執行環境的執行階段以前,變量對象中的屬性都沒法訪問,這裏還有一個活動對象(activation object)的概念,其實這個概念正是由進入執行階段的變量對象轉化而來。

來看一個栗子:

function foo(){
  var a=10;
  function fn(){
    return 5;
  }
}
foo();

讓咱們來看看foo()函數的執行環境:

它會包括三個部分:1.變量對象;2.做用域鏈;3.this指向對象

建立階段:

  1. 創建arguments
  2. 找到fn();
  3. 找到變量a,undefined;

執行階段:

  1. 變量對象變成活動對象;
  2. arguments仍是它~
  3. fn();
  4. a=10;

以上就是變量對象的內容了,須要記住這個東西,由於會方便咱們瞭解下文另外一個重要的概念:做用域鏈。

做用域鏈(scope chain)

什麼是做用域鏈?《JS高程》裏的文字是:「做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。」懵不懵逼?反正我第一次看到的時候確實是懵逼了。前面咱們說過做用域,那麼做用域鏈是否是就是串在一塊兒的做用域呢?並非。

做用域和做用域鏈的關係,用@波同窗的話說,做用域是一套經過標識符查找變量的規則。而做用域鏈則是這套規則這套規則的具體運行。

是否是仍是有點懵逼?仍是看例子吧:

function foo(){
  var a=10;
  function fn(){
    return 5;
  }
}
foo();

咱們仍是用上面的栗子,此次咱們只看做用域鏈,根據規則,在一個函數的執行環境的做用域鏈上,會依次放入本身的變量對象,父級的變量對象,祖級的變量對象.....一直到全局的變量對象。

好比上面這個栗子,fn()的執行環境的做用域鏈上會有些什麼呢?首先是本身的OV,而後是foo()的OV,接着就是全局的OV。而foo()的做用域鏈則會少一個fn()的OV。(OV是變量對象的縮寫)

那這樣放有什麼好處呢?咱們知道「做用域鏈保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。」有序!外層函數不能訪問內層函數的變量,而內層可以訪問外層。正是有了這個做用域鏈,經過這個有方向的鏈,咱們能夠查找標識符,進而找到變量,才能實現這個特性。

閉包

好了,終於要講到這個前端小萌新眼裏的小boss了。在技術博客和書裏翻滾了將將一週,對閉包的各類解釋把我搞得精力憔悴,懷疑人生。以致於在寫下這段關於閉包的論述時,也是心裏忐忑,由於我也不肯定我說的是百分之百正確。

先看看《JS高程》說的:「閉包是指有權訪問另外一個函數做用域中的變量的函數。」

@波同窗的說法是:「當函數能夠記住並訪問所在的做用域(全局做用域除外)時,就產生了閉包,即便函數是在當前做用域以外執行。」

......

好吧其實我以爲都說的不是太清楚。讓咱們這樣來理解,就是內部函數引用了外部函數的變量對象時,外部函數就是一個閉包。

仍是看例子吧。

function foo(){
  var a=20;
  return function(){
    return a;
  }
}
foo()();

在這個栗子中,foo()函數內部返回了一個匿名函數,而匿名函數內部引用了外部函數foo()的變量a,因爲做用域鏈,這個引用是有效的,按照JS的機制,foo()執行完畢後,執行環境會失去引用,內存會銷燬,可是因爲內部的匿名函數的引用,a會被暫時保存下來,罩着a的就是閉包。

return一個匿名函數時創造一個閉包的最簡單的方式,實際上創造閉包十分靈活,再看一個栗子:

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(a);
    }
    fn = innnerFoo; 
}

function bar() {
    fn();
}

foo();
bar(); // 2

栗子來自@波同窗;

如上,能夠看到:經過把innnerFoo()賦值給全局變量fn,內部的函數在當前做用域外執行了,可是這不會影響foo造成了一個閉包。

閉包和兩個不一樣的案例

這兩組栗子都是在各類書籍和各類博客上司空見慣了的栗子,其實跟閉包的關係不是很大,可是涉及到了函數相關的知識點,因此在這裏寫下來。也算是積累。

閉包和變量(見《JS高程》P181)

一個例子

function createFunction(){
     var result=new Array();
    for(i=0;i<10;i++){
        result[i]=function(){
              return i;
         }
      }
     return result;
}
alert(createFunction());

這個例子並不會如咱們覺得的返回從0到9的一串索引值。
當咱們執行createFunction()時,函數內會return result,而咱們注意到result是一個數組,而每個result[i]呢?它返回的則是一個函數,而不是這個函數的執行結果 i。

因此咱們想要返回一串索引值的時候,試着選擇result數組的其中一個,再加上圓括號讓它執行起來,像這樣:

createFunction()[2]()

這樣子就能執行了嗎?運行起來發現並無,執行的結果是一串的i,爲何呢?

緣由是在執行createFunction()的時候,i的值已經增長到了10,即退出循環的值,而再要執行result內部的匿名函數時,它能獲取到的i就只有10了,因此無論引用多少次,i的值都會是10;

那要如何修改才能達到咱們的目的呢?

function createFunction(){
    var result=[];
    for(i=0;i<10;i++){
        result[i]=function(num){
            return function(){
              return num;
            };
        }(i);
    }
    return result;
}
alert(createFunction()[2]());

彈出的警告和索引值如出一轍。這又是什麼緣由呢?

咱們執行createFunction()時,把外部的匿名函數的執行結果賦值給了result,返回的result就是十個函數的數組。

而在這個外部函數裏,有一個參數num,因爲IIFE(當即執行函數)的緣故,循環過程當中的i被賦值給了一個個的num,先後一共保存了10個num,爲何可以保存下來呢?由於內部的匿名函數引用了num。而這外部函數就是一個閉包

接下來,當執行createFunction()[2]()時其實是執行這個數組result的第三項,即:

function(){
   return num;
};

這個函數。

num值是多少呢?如前所述,正是對應的i。因此返回的值就可以達到咱們的預期了。

實際上,我認爲這個例子中更重要的是自執行函數這個概念,正是有了自執行,才能造成多對對多的引用,儘管這個例子裏確實存在閉包,不過我認爲用這個例子來介紹閉包並非太恰當。

閉包和this

this也是JS裏一個重中之重。咱們知道,JS的this十分靈活的,前面已經介紹過,this的指向在函數執行環境創建時肯定。函數中的this的指向是一個萌新們的難點,何時它是指向全局環境呢?何時它又是指向對象呢?注意:此處討論的是指函數中的this,全局環境下的this通常狀況指向window。

結論一:this的指向是在函數被調用的時候肯定的

由於當一個函數調用時,一個執行環境就建立了,接着它會執行,這是執行環境的生命週期。因此this的指向是在函數被調用時肯定的。

結論二:當函數執行時,若是這個函數是屬於某個對象,調用的方式是以對象的方法進行的,那麼this的指向就是這個對象,而其餘狀況,如函數獨立調用,則基本是指向全局對象。

PS:實際上這個說法不大準確,當函數獨立調用時,在嚴格模式下,this的指向時undefined,而非嚴格模式下,則時指向全局對象。

爲了更好的說明,讓咱們看一個例子:

var a = 20;
var foo = {
    a: 10,
    getA: function () {
        return this.a;
    }
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test());  // 20

在上面這個例子中,foo.getA()做爲對象方法的調用,指向的天然是這個對象,而test雖然指向和foo.getA相同,可是由於是獨立調用,因此在非嚴格模式下,指向的是全局對象。

除了上面的例子,在《JS高程》中還有一個經典的例子,衆多博客文章均有討論,可是看過以後以爲解釋仍是不夠清楚,至少我沒徹底理解,這裏我將試着用本身的語言來解釋。
var name="the window";
var object={
    name:"my object",    
    getNameFunc:function(){
        return function(){
            return this.name;
        };
    }
};
    
alert(object.getNameFunc()());   // the window

在這個帶有閉包的例子裏,咱們能夠看到object.getNameFunc()執行的返回是一個函數,再加()執行則是一個直接調用了。因此指向的是全局對象。

若是咱們想要返回變量對象怎麼辦呢?

讓咱們看一段代碼:

var name="the window";

var object={
name:"my object",
getFunc:function(){
        return this.name;
}
};
alert(object.getFunc());   //"my object"```

我去掉了上面例子的閉包,能夠看出在方法調用的狀況下,this指向的是對象,那麼咱們只要在閉包能訪問到的位置,同時也是在這個方法調用的同一個做用域裏設置一個「中轉站」就行了,讓咱們把這個位置的this賦值給一個變量來存儲,而後匿名函數調用這個變量時指向的就會是對象而不是全局對象了。

var name="the window";
    
    var object={
        name:"my object",
        getFunc:function(){
            var that=this;
            return function(){
                return that;
            };
        }
    };
    alert(object.getFunc());

that's all

閉包的應用

閉包的應用太多了,最重要的一個就是模塊模式了。不過說實話,實在還沒上路,因此這裏就用一個模塊的栗子來結尾吧。(強行結尾)
(function () {
    var a = 10;
    var b = 20;

    function add(num1, num2) {
        var num1 = !!num1 ? num1 : a;
        var num2 = !!num2 ? num2 : b;

        return num1 + num2;
    }

    window.add = add;
})();

add(10, 20);

咱們須要知道的是,所謂模塊利用的就是閉包外部沒法訪問內部,內部卻能訪問外部的特性,經過引用了指定的公共變量和方法,達到訪問私有變量和方法的目的。模塊能夠保證模塊內部的私有方法和變量不被外部變量污染,進而方便更大規模的開發項目。

so,這篇文就到這裏辣,寫了一個下午,最最最要感謝的是@波同窗,正是讀了他出色的教程,才能讓我對JS的理解更深一點,他的每一篇技術文章都是很是用心的,事實上,我以爲個人論述仍然不夠系統清晰,想要了解得更清晰的朋友能夠去簡書搜索@波同窗閱讀他寫得技術文章,好了,就這樣,債見
相關文章
相關標籤/搜索