理解js中的作用域,作用域鏈以及初探閉包

source.gif
前言

對于js中的閉包,無論是老司機還是小白,我想,見得不能再多了,然而有時三言兩語卻很難說得明白,反正在我初學(xué)時是這樣的,腦子里雖有概念,但是卻道不出個所以然來,在面試中經(jīng)常會被用來吊自己的胃口,考察基礎(chǔ),雖然網(wǎng)上自己也看過不少相關(guān)閉包的文章,帖子,但貌似這玩意,越看越復(fù)雜,滿滿逼格高,生涉難懂的專業(yè)詞匯常常把自己帶到溝里去了,越看越迷糊,其實終歸結(jié)底,用楊絳先生的一句話就是:“你的問題在于代碼寫得太少,書讀得不夠多",其實在我看來前者是主要的,是后者的檢驗, 自知目標(biāo)搬磚20年(還差19年..),其實,閉包的應(yīng)用,在我們不經(jīng)意間就使用了,是無處不在的,盡管知道閉包是個麻煩的玩意,自己也經(jīng)常吃回頭草看看這家伙的,所謂出去混,遲早是要還的,今天,我就對閉包的一點理解作一點點總結(jié),關(guān)于閉包,我也一直在學(xué)習(xí)當(dāng)中...話說多了,都是故事,直接擼起袖子,開始干吧

正文從這里開始~

理解上下文和作用域

其實上下文與作用域是兩個不同的概念,有時我自己也經(jīng)常混淆,把它們視為是同一個東西,我們知道函數(shù)的每次調(diào)用都會有與之緊密相連的作用域和上下文,從本質(zhì)上說,作用域其實是基于函數(shù)的,而上下文基于對象的,也就是說作用域是涉及到它所被調(diào)用函數(shù)中的變量訪問,而調(diào)用方法和訪問屬性又存在著不同的調(diào)用場景(4種調(diào)用場景,函數(shù)調(diào)用,方法調(diào)用,構(gòu)造器函數(shù)調(diào)用,call(),apply()間接調(diào)用),而上下文始終是this所代表的值,它是擁有控制當(dāng)前執(zhí)行代碼的對象的引用

變量作用域

在javascript中,作用域是執(zhí)行代碼的上下文(方法調(diào)用中this所代表的值),作用域有三種類型:全局作用域(Global scope),局部作用域(Local/Function scope,函數(shù)作用域)和eval作用域,在函數(shù)內(nèi)部使用var定義的代碼,其作用域都是局部的,且只對該函數(shù)的其他表達(dá)式是可見的,包括嵌套子函數(shù)中的代碼,局部變量只能在它被調(diào)用的作用域范圍內(nèi)進行讀和寫的操作,在全局作用域內(nèi)定義的變量從任何地方都是可以訪問的,因為它是作用域鏈中的最高層中的最后一個,在整個范圍內(nèi)都是可見的,注意在Es6之前是沒有塊級作用域的,而Es6后是有的,也就是說Esif,while,switch,for語句是有了塊級作用域的,可以使用let關(guān)鍵字聲明變量,修正了var關(guān)鍵字的缺點,注意let使用規(guī)則
看如下代碼所示:

              * 全局變量與局部變量
              * 
              * @global variable {variable="itclab"}
              * @function myFun 
              * @local variable {variable="itclanCode",variable=24}
              * @function otherFun
              * @eval作用域 evalfun
              */
              var variable = "itclan";        //全局變量
              console.log("全局variable","=",variable); // 全局variable = itclan
              // 函數(shù)表達(dá)式
              var myFun = function(){
                 var variable = "itclanCode"; //局部變量
                 console.log("局部variable","=",variable); // 局部variable = itclanCode
                 var otherFun = function(){
                     var variable = 24;       //局部變量
                     console.log("局部variable","=",variable);  // 局部variable = 24
                 }
                 otherFun();
              }
              myFun();
              eval("var evalfun = 20;console.log('evalfun作用域','=',evalfun)");// evalfun作用域 = 20

01.png

注意

  • 函數(shù)可以嵌套函數(shù),并可以無限的嵌套下去,也就是可以創(chuàng)建無數(shù)的函數(shù)作用域和eval作用域,而javascript壞境只是用一個全局作用域
  • 全局作用域(global scope)是作用域鏈中的最后一層
  • 包含函數(shù)的函數(shù),會創(chuàng)建堆棧執(zhí)行的作用域,這些鏈接在一起的棧通常被稱為作用域鏈(也就是后面會提到閉包產(chǎn)生的本質(zhì)原因)
什么是執(zhí)行壞境

所謂執(zhí)行壞境,它定義了變量或函數(shù)有訪問的其他數(shù)據(jù)的能力,它決定了各自的行為,它的側(cè)重點在于函數(shù)的作用域,而并不是所要糾結(jié)的上下文,一旦函數(shù)一聲明定義,就會自動的分配產(chǎn)生了作用域,有著自己的執(zhí)行壞境,執(zhí)行壞境可以分為創(chuàng)建與執(zhí)行兩個階段,在創(chuàng)建階段,js解析器首先會創(chuàng)建一個變量對象(活動對象),它由定義在執(zhí)行壞境中的變量,函數(shù)聲明和參數(shù)組成,在這個階段,系統(tǒng)會自動的產(chǎn)生一個this對象,作用域鏈會被初始化,隨之,this的值也會被確定,第二階段,也就是代碼執(zhí)行,代碼會被解釋執(zhí)行,你會發(fā)現(xiàn),每個執(zhí)行壞境都有一個與之關(guān)聯(lián)的變量對象,執(zhí)行壞境中所有定義的變量和函數(shù)都保存在這個對象中,注意,我們是無法手動的訪問這個對象的,只有js解析器才能夠訪問它,其實也就是this,盡管很抽象,但是理解它還是蠻重要的

作用域鏈(詞法作用域)

當(dāng)javascript查找與變量相關(guān)聯(lián)的值時,會遵循一定的規(guī)則,也就是沿著作用域鏈從當(dāng)前函數(shù)作用域內(nèi)逐級的向上查找,直到頂層全局作用域結(jié)束,若找到則返回該值,若無則返回undefined,這個鏈條是基于作用域的層次結(jié)構(gòu)的,一旦當(dāng)代碼在壞境中執(zhí)行時,會自動的創(chuàng)建一個變量對象的作用域鏈,其作用域鏈的用途也就是保證對執(zhí)行壞境的全局變量和具有訪問權(quán)限函數(shù)內(nèi)的局部變量定制特殊的規(guī)則,由內(nèi)到外有序的對變量或者函數(shù)進行訪問,作用域鏈包含了在壞境棧中的每個執(zhí)行壞境對應(yīng)的變量對象,通過作用域鏈可以決定變量的訪問與標(biāo)識符的解析,如下代碼所示:

               * 作用域鏈變量的訪問
               *
               * @global variable {name="隨筆川跡"}
               * @function fun1,fun2
               * @local variable {oTherName="哇嘎嘎",AliasName = "川川"}
               * @return {fun2,name,oTherName,AliasName}
               * @return fun2,fun1函數(shù)的返回結(jié)果值為fun2的值
               *
               *
               */
               var name = "隨筆川跡";               // 全局變量
               var fun1 = function(){
                   var oTherName = "哇嘎嘎";        // 局部變量
                   var fun2 = function(){
                       var AliasName = "川川";      // 局部變量
                       AliasName = oTherName;
                       oTherName = AliasName;
                       return {name,oTherName,AliasName};
                   }
                   console.log(fun2());
                   return fun2();
               }
               //console.log(fun2()); // 若在全局作用域調(diào)用訪問fun()會失敗,顯示fun2 is not defined
               console.log(fun1(),"name is","=",name)
02作用域鏈.png

當(dāng)我們分析這段代碼時,首先全局范圍全局變量name,函數(shù)fun1嵌套fun2函數(shù),fun1,fun2函數(shù)內(nèi)局部變量分別為:oTherName,AliasName當(dāng)在函數(shù)fun2內(nèi),并未聲明name變量

便在該函數(shù)fun2內(nèi)進行了訪問,這是如何找到的?javascript首先在當(dāng)前fun2函數(shù)作用域內(nèi)查找一個名為name的變量,但是在fun2并未找到,于是它會查找它的父函數(shù)fun1的作用域內(nèi)進行查找,但是發(fā)現(xiàn)仍然沒有找到,于是在往外進行查找,結(jié)果在全局作用域范圍內(nèi)查找了name的值

于是找到了便把該值進行返回,若是在全局作用域內(nèi)還未找到則會返回undefined,注意在函數(shù)fun2作用域內(nèi),name,oTherName,AliasName都是可以訪問的,而在函數(shù)fun1函數(shù)作用域內(nèi)是訪問不了oTherName的,因為它脫離了fun1的函數(shù)的作用域嘛,我們知道在函數(shù)外是無法訪問函數(shù)里面的的變量的,訪問變量由內(nèi)向外進行查找是可以的,但是反之則就不行,從上圖的箭頭分析圖可知,內(nèi)部壞境中,是可以通過作用域鏈訪問它所有的外部壞境

但是在外部壞境是無法訪問內(nèi)部壞境中的任何變量和函數(shù),這點很重要,我們在函數(shù)嵌套函數(shù),并且進行函數(shù)調(diào)用時,要格外注意,如果在編程當(dāng)中出現(xiàn)這種函數(shù)is not defined那么就是牽扯到函數(shù)作用域的問題了,在函數(shù)外是無法訪問函數(shù)內(nèi)的變量或者函數(shù)的,當(dāng)然這種問題是可以解決的,也就是后面提到的閉包

其實上面我們的代碼中就已經(jīng)無形用了閉包,匿名函數(shù)fun1,fun2就是個閉包,嵌套函數(shù)與被嵌套壞境的連接是線性的,有次序的,對于標(biāo)識符(也就是變量或者函數(shù)名查找)是從當(dāng)前函數(shù)作用域開始,沿著作用域鏈逐級的向上查找,直到最頂端全局變量壞境,若找到該值則返回,若無則返回undefined

注意:理解作用域以及作用域鏈對理解原型鏈?zhǔn)呛苡袔椭?其實他們區(qū)別并不是很大,兩者都是通過位置體系(上下嵌套關(guān)系)和分層體系來查找值的方法,進而可以對變量或者函數(shù)進行讀和寫的操作,如下代碼所示:


              var x = 5;
              var fun1 = function(){
                 var y = 10;
                 var fun2 = function(){
                     var z = 20;
                     return z+y+x;
                 }
                 fun2();
                 return fun2();
              }
              console.log("x+y+z的和=",fun1()); //x+y+z的和= 35
javascript沒有塊級作用域

在Es6之前,如if,for,while,switch邏輯語句是無法創(chuàng)建作用域,也就是它后面的雙大括號并沒有域的作用,這才得式變量可以相互覆蓋,解決辦法,你可以使用es6的let關(guān)鍵字聲明變量,注意let的使用,如下代碼所示


              var str = "itclan";     // 全局變量
              console.log(str);       // itclan
              if(true){               // if邏輯語句
                 str = "itclanCode"; 
                 console.log(str);    // itclanCode
                 for(var i = 0;i<=2;i++){
                    str = i;
                    console.log(str); // 0,1,2
                 }
                   console.log(str);    // 2
              }
              console.log(str);       // 2

因此,代碼在執(zhí)行過程中,從上到下,str是變化的,因為在Es6之前,沒有塊級作用域,只有全局作用域,函數(shù)作用域,eval()作用域
注意
在函數(shù)中應(yīng)用var聲明變量,避免作用域的陷阱
javascript會將缺少var的變量聲明,即便在函數(shù)或者封裝在函數(shù)中,都會視為全局變量作用域,而非局部作用域,我們是不應(yīng)該出現(xiàn)這種不要var聲明的,這樣會造成全局變量的污染,易混淆,如下代碼所示

             * 如果不使用var來聲明變量,那么,該變量實際上是在全局作用域中定義,而不是局部作用域中定義(它本是在局部作用域中定義)
             * 
             * @descortion:這樣很容易產(chǎn)生誤解,應(yīng)當(dāng)杜絕這么干
             * @在函數(shù)內(nèi)定義的變量應(yīng)用var,當(dāng)然要在函數(shù)內(nèi)部創(chuàng)建或更改全局作用域內(nèi)的屬性就另當(dāng)別論了的
             *
             *
             */
             var fun1Exp = function(){
                 var fun2Exp = function(){
                     name = "污葵"; // 沒有使用var,它相相當(dāng)于window.name
                 }
                 fun2Exp();
             }
             fun1Exp();
             console.log({name});
             // 相反,使用var的情況
             var fun3Exp = function(){
                 var fun4Exp = function(){
                     var age = 20; //使用var,局部變量
                 }
                 fun4Exp();
             }
             fun3Exp();
             console.log(age);  //Uncaught ReferenceError: age is not defined,報錯的原因,age在fun4Exp函數(shù)作用域中,在函數(shù)外是訪問不了函數(shù)內(nèi)部的變量的
作用域是在函數(shù)定義時就確定的,而非調(diào)用時確定

因為函數(shù)決定作用域,又因為函數(shù)也是對象,也是一種數(shù)據(jù)類型,一樣可以像基本數(shù)據(jù)類型值一樣被作為值來傳遞,作用域就是根據(jù)函數(shù)定義時的位置確定的,而與該函數(shù)在哪里被調(diào)用無關(guān),其實就是詞法作用域,作用域鏈?zhǔn)窃谡{(diào)用函數(shù)之前創(chuàng)建,也是這樣,我們就可以創(chuàng)建閉包,我們常常是這么做的,讓函數(shù)向全局作用域返回一個嵌套函數(shù),但該函數(shù)仍然能夠通過作用域訪問它父函數(shù)的作用域,作用域鏈?zhǔn)窃诙x時確定的,并在函數(shù)內(nèi)部傳遞代碼不會改變作用域
如下代碼所示:

 
              * 作用域鏈?zhǔn)窃诤瘮?shù)定義時位置確定的,而非函數(shù)調(diào)用位置,在函數(shù)內(nèi)部傳遞代碼不會改變作用域鏈
              *
              * @funtion expression parentFun
              * @local variable localVal
              * @return parentFun的返回值為一個匿名函數(shù),訪問該匿名函數(shù)外的變量
              *
              */
              var parentFun = function(){
                  var localVar = "itclan是個有溫度的公眾號";
                  return function(){  // 返回一個匿名函數(shù)
                      console.log(localVar);
                  }
              }
              var nestedFun =  parentFun();//nestedFun引用parentFun函數(shù),把函數(shù)parentFun函數(shù)的返回值賦值給變量nestedFun
              nestedFun();   // 輸出itclan是個有溫度的公眾號,因為返回的函數(shù)可以通過作用域鏈訪問到localVar變量
產(chǎn)生閉包的根本原因是作用域鏈

在通過上面的了解變量的作用域和作用域鏈后,相信你理解閉包就不難了,如下代碼所示:

              * 閉包是由作用域鏈引用的
              *
              * @function expression countNum 匿名函數(shù) 
              * @local variable count
              * @return 匿名函數(shù)
              * 
              */
              var countNum = function(){
                  var count = 0;
                  return function(){  //調(diào)用countNum的時候返回嵌套的子函數(shù)
                     return ++count;// count在作用域鏈內(nèi)定義,父函數(shù)里
                  };

              }(); // 匿名函數(shù)的立即調(diào)用,返回嵌套函數(shù)
              // countNum(),上面的匿名函數(shù)后若不加括號調(diào)用,則返回的結(jié)果將是return 后面的函數(shù)的整體代碼
              console.log(countNum());   // 1
              console.log(countNum());   // 2
              console.log(countNum());   // 3
03產(chǎn)生閉包的原因是作用域鏈.png

當(dāng)每次調(diào)用countNum函數(shù)時,嵌套在該函數(shù)內(nèi)的匿名函數(shù)是可以訪問父函數(shù)(這里指的是countNum的)作用域的,其實這就是所謂的閉包,作用鏈就是閉包的橋梁,用來連接內(nèi)部函數(shù)與外部函數(shù)的關(guān)系,從而達(dá)到外部函數(shù)訪問內(nèi)部函數(shù)局部變量或者函數(shù)的目的,其中被嵌套函數(shù)就可以稱為是一個閉包
小結(jié)

  • 產(chǎn)生閉包的原因是由作用域鏈引起的
  • 函數(shù)嵌套函數(shù),被嵌套的函數(shù)就可以稱為閉包
  • 子函數(shù)可以使用父函數(shù)的變量(訪問其他函數(shù)內(nèi)部的局部變量)
  • 讓變量始終保存在內(nèi)存中,避免自動垃圾回收(其實上面的例子中就已經(jīng)用到了的)
  • 對外提供公有屬性和方法

總結(jié):

整篇文章從理解上文和作用域開始,以及什么是執(zhí)行壞境,其產(chǎn)生閉包的原因是作用域鏈,并知道在Es6之前是沒有塊級作用域的概念的,并且作用域是在函數(shù)定義時就確定的,而非函數(shù)調(diào)用確定,在我的理解中編程其實很大一部分就是對數(shù)據(jù)進行讀和寫的操作

其中讀可以理解對定義變量數(shù)據(jù)的訪問,而寫可以理解賦值,引用,變更,改寫操作,當(dāng)然js中不像其他后臺語言的存儲數(shù)據(jù)類型那般復(fù)雜,基本就是基本數(shù)據(jù)類型和對象了

理解作用域以及作用域鏈對理解閉包是相當(dāng)?shù)闹匾?對后續(xù)的原型鏈以及繼承都是相關(guān)聯(lián)的,其實也不必抓著什么執(zhí)行壞境和上下文這些相對抽象的概念不放,我們只有在平時的使用當(dāng)中,稍稍留意就行,在應(yīng)用中結(jié)合理論進行驗證,當(dāng)然閉包的內(nèi)容遠(yuǎn)不及此..

以下是本篇提點概要
  • 理解上下文和作用域,作用域是基于函數(shù)的,而上下文是基于對象的,雖然說函數(shù)也是對象,但是這里更多的是指對象直接量的表示法,上下文始終圍繞著this所代表的值,它是擁有控制當(dāng)前執(zhí)行代碼對象的引用
  • 變量的作用域,在Es6之前沒有塊級作用域,而Es6有了塊級作用域,也就是if,,while,switch,for,若使用let關(guān)鍵字,則具備塊級作用域,也就是說定義在雙大括號內(nèi)的變量,在雙大括號內(nèi)的才起作用,一旦離開該范圍,就不起作用了
  • 什么是執(zhí)行壞境,定義了變量或函數(shù)有訪問的其他數(shù)據(jù)的能力,它決定了各自的行為,它的側(cè)重點在于函數(shù)的作用域,而并不是所要糾結(jié)的上下文,分為創(chuàng)建壞境和執(zhí)行壞境
  • 作用域鏈(詞法作用域),當(dāng)查找與變量相關(guān)聯(lián)的值時,會遵循一定的規(guī)則,也就是沿著作用域鏈從當(dāng)前函數(shù)作用域內(nèi)逐級的向上查找,直到頂層全局作用域結(jié)束,若找到則返回該值,若無則返回undefined
  • javascript沒有塊級作用域,往往很多時候使用匿名函數(shù)自執(zhí)行來模擬塊級作用域
  • 作用域是在函數(shù)定義時就確定的,而非調(diào)用時確定,作用域就是根據(jù)函數(shù)定義時的位置確定的,而與該函數(shù)在哪里被調(diào)用無關(guān),其實就是詞法作用域
  • 產(chǎn)生閉包的根本原因是作用域鏈,見上小結(jié)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容