函數(shù)作用域與閉包

函數(shù)作用域

要理解閉包,必須從理解函數(shù)被調(diào)用時都會發(fā)生什么入手。

我們知道,每個javascript函數(shù)都是一個對象,其中有一些屬性我們可以訪問到,有一些不可以訪問,這些屬性僅供JavaScript引擎存取,是隱式屬性。[[scope]]就是其中一個。
[[scope]]就是我們所說的作用域,其中存儲了執(zhí)行期上下文的集合。由于這個集合呈鏈式鏈接,我們把這種鏈式鏈接叫做作用域鏈。

當(dāng)函數(shù)被定義(創(chuàng)建)時有一個自己所在環(huán)境的作用域(GO全局作用域 ,若是在函數(shù)內(nèi)部,就是引用別人的作用域),當(dāng)函數(shù)被執(zhí)行時,會將自己的獨一無二的AO(活動對象,是使用arguments和該函數(shù)內(nèi)部的變量值初始化的活動對象)執(zhí)行上下文放在前端,形成一個作用域鏈;當(dāng)該函數(shù)執(zhí)行完,自己的AO會被干掉,回到被定義時的狀態(tài)。

另外,變量的查找,就是找所在函數(shù)的作用域,首先從作用域的頂端開始查找,找不到的情況下,會查找外部函數(shù)的活動對象,依次向下查找,直到到達作為作用域鏈終點的全局執(zhí)行環(huán)境。

下面看幾個查找變量例子,深入理解函數(shù)作用域及作用域鏈。

function a(){
   function b(){
      var b=2223;  
   }
   var a=78;
}
a()
b()
console.log(b)

輸出結(jié)果: error: b is not defined
當(dāng)函數(shù)a執(zhí)行完畢后,該函數(shù)內(nèi)部的活動對象AO就會被銷毀。所以函數(shù)外部是訪問不到函數(shù)內(nèi)部的變量的。


function outer(){
   function inner(){
      var b=2223;   
      a=0
   }
   var a=78;
   inner() //①
   console.log(a) 
   console.log(b)
}
outer() 

輸出結(jié)果: 0 , error: b is not defined

當(dāng)函數(shù)inner在被定義的階段,就會擁有(引用)函數(shù)outer的作用域(包括函數(shù)outer自己局部的活動對象AO和全局作用域);當(dāng)函數(shù)inner()被執(zhí)行的時候,會再創(chuàng)建一個自己的活動對象AO并被推入執(zhí)行環(huán)境作用域鏈的前端。
inner()函數(shù)在被執(zhí)行的時候,由于變量a在outer()函數(shù)中已經(jīng)存在并被inner()引用,所以inner()函數(shù)內(nèi)部的變量a會修改掉外部函數(shù)變量a的值,并且可以不用聲明。當(dāng)inner()函數(shù)執(zhí)行完畢后(執(zhí)行到①處),inner()函數(shù)局部的AO會被銷毀,下面就訪問不到變量b了,而且這時候變量a的值將是被inner()函數(shù)修改過的值。


function a(){
  function b(){
     var b=2223;   
     a=0
  }
  var a=78;
  b=1
  b()
  console.log(b)
}
a()

輸出結(jié)果: 1


var x=10;
function a(){
    console.log(x);
}
function b(){
    var x=20;
    a();
}
a();//10
b();//還是10;

總之:函數(shù)在被定義階段,會引用著其所在環(huán)境的作用域;執(zhí)行階段,會創(chuàng)建一個自己獨一無二的活動對象AO,并推入執(zhí)行環(huán)境作用域鏈的前端;函數(shù)執(zhí)行完畢之后,自己的執(zhí)行上下文AO會被銷毀,所以,這時候訪問其內(nèi)部的變量是訪問不到的。但是,閉包的情況又有不同。

簡述什么是閉包

閉包:有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。

從理論的角度上,所有的JavaScript函數(shù)都是閉包,因為函數(shù)在被定義階段就會存儲一個自己所在環(huán)境的作用域,可以訪問這個作用域中的所有變量。可以說,閉包是 JS 函數(shù)作用域的副產(chǎn)品。理解js作用域,自然就明白了閉包,即使你不知道那是閉包。

從技術(shù)實踐的角度,以下函數(shù)才算閉包:

  1. 定義該函數(shù)的執(zhí)行環(huán)境的作用域(執(zhí)行上下文)即使被銷毀 ,它的活動對象(AO)仍然會留在內(nèi)存中,被該函數(shù)引用著。
  2. 引用了函數(shù)體外部的變量。

創(chuàng)建閉包常見方式,就是在一個函數(shù)A內(nèi)部創(chuàng)建另一個函數(shù)B,然后通過return這個函數(shù)B以便在外部使用,這個函數(shù)B就是一個閉包。

舉個例子:

//也算閉包
var a = 1;
function foo() {
    console.log(a);
}
foo();

//函數(shù)內(nèi)部定義函數(shù)
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()()  //這里相當(dāng)于:
//var foo = checkscope();
//foo();

輸出結(jié)果:local scope
f()函數(shù)在被定義階段就被保存到了外部,這個時候就相當(dāng)于外部的函數(shù)可以訪問另一個函數(shù)內(nèi)部的變量,f()函數(shù)會形成一個閉包。

按照函數(shù)作用域的概念,當(dāng)checkscope()執(zhí)行完畢后,其局部的活動對象AO會被銷毀;但是由于checkscope()函數(shù)執(zhí)行完畢后返回一個函數(shù),根據(jù)函數(shù)在被定義階段會引用該函數(shù)所在執(zhí)行環(huán)境的執(zhí)行上下文,被返回的函數(shù)f()即使被保存到了外部依然引用著checkscope()函數(shù)的執(zhí)行期上下文,直到函數(shù)f()執(zhí)行完畢,checkscope()函數(shù)的執(zhí)行上下文才會被銷毀。

也就是說被嵌套的函數(shù)f()無論在什么地方執(zhí)行,都會包含著外部函數(shù)(定義該函數(shù))的活動對象。所以,即使f()被保存到外部,也可以訪問到另一個函數(shù)checkscope()中定義的變量。

無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外, 它都會持有對原始定義作用域的引用, 無論在何處執(zhí)行這個函數(shù)都會使用閉包。

由此看來,閉包可能會導(dǎo)致一個問題:導(dǎo)致原有作用域鏈不釋放,造成內(nèi)存泄漏(內(nèi)存空間越來越少)。可以通過手動將被引用的函數(shù)設(shè)為null,來解除對該函數(shù)的引用,以便釋放內(nèi)存。

閉包的作用

  1. 實現(xiàn)公有變量。
function a(){
   var num=100;
   function b(){
      num++;
      console.log(num)
   }
   return b;
}

var demo=a();
demo();//101
demo();//102
function test(){
   var num=100;
   function a(){
      num++;
   }
   function b(){
      num--;
   }
   return [a,b]

}

var demo=test()
demo[0]();//101
demo[1]();//100
//函數(shù)a和函數(shù)b引用的是同一個作用域。
  1. 實現(xiàn)私有變量。

閉包通常用來創(chuàng)建內(nèi)部變量,使得這些變量不能被外部隨意修改,同時又可以通過指定的函數(shù)接口來操作。
通過在立即執(zhí)行函數(shù)中return 將方法保存到外部等待調(diào)用,內(nèi)部的變量由于是私有的,外部訪問不到,可防止污染全局變量,利于模塊化開發(fā)。

var foo = ( function() { 
   var secret = 'secret'; 
   // “閉包”內(nèi)的函數(shù)可以訪問 secret 變量,而secret變量對于外部卻是隱藏的 
   return { 
      get_secret: function () { 
         // 通過定義的接口來訪問 secret 
            return secret; 
      }, 
      new_secret: function ( new_secret ) { 
         // 通過定義的接口來修改 secret 
         secret = new_secret; 
      } 
   }; 
} () ); 
foo.get_secret (); // 得到 'secret' 
foo.secret; // undefined,訪問不能 
foo.new_secret ('a new secret'); // 通過函數(shù)接口,我們訪問并修改了secret 變量 
foo.get_secret (); // 得到 'a new secret'
var name='bcd';
var init=(function (){
   var name='abc';
   function callName(){
      console.log(name);
   }

   //其他方法

   return function () {
      callName();
      //其他方法
   }
}())

init () //abc

閉包經(jīng)典題

function createFunctions(){
  var result = new Array();
  for (var i=0; i < 10; i++){
    result[i] = function(){
      console.log(i);
     };
  }
return result;
}

var fun = createFunctions();
for(var i=0;i<10;i++){
    fun[i]();
}

輸出結(jié)果:打印十個10
數(shù)組每個值都是一個函數(shù),每個函數(shù)對createFunctions()形成一個閉包,此時i都是引用createFunctions()中同一個i變量。

function test(){
   var arr=[];
   for(var i=0;i<10;i++){
      (function(j){
         arr[j]=function(){
            console.log(j);
         }
      }(i))
   }
   console.log(i)//10 ,i還是10
   return arr
}
var myArr=test();
for(var i=0;i<10;i++){
   myArr[i]()
}

輸出結(jié)果:從0到9
這次依然把數(shù)組每個值賦為函數(shù),不同的是循環(huán)十次立即執(zhí)行函數(shù),并將當(dāng)前循環(huán)的i作為參數(shù)傳進立即執(zhí)行函數(shù),由于參數(shù)是按值傳遞的,這樣就把當(dāng)前循環(huán)的i保存下來了。

閉包中的this對象

在閉包中使用this對象可能會導(dǎo)致一些問題,結(jié)果往往不是預(yù)想的輸出結(jié)果。
看個例子:

  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());

輸出結(jié)果:The Window

this對象是在運行時基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中,this等于window,而當(dāng)函數(shù)被作為某個對象的方法調(diào)用時,this等于那個對象。匿名函數(shù)往往具有全局性,這里可以這樣理解,沒有任何對象調(diào)用這個匿名函數(shù),雖然這個匿名函數(shù)擁有getNameFunc()的執(zhí)行上下文。

因為這個匿名函數(shù)擁有getNameFunc()的執(zhí)行上下文,通過把外部函數(shù)getNameFunc()作用域中的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問到該對象了。

  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());

輸出結(jié)果:My Object

理解到這里,基本上就搞定了閉包了。

學(xué)習(xí)資料

JavaScript 里的閉包是什么?應(yīng)用場景有哪些?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,604評論 2 380

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