轉:對JavaScript中閉包的理解

關於 const     let      var 

總結:javascript

建議使用 let  ,而不使用var,若是要聲明常量,則用const。php

ES6(ES2015)出現以前,JavaScript中聲明變量只有經過 var 關鍵字,函數聲明經過 function 關鍵字,html

ES6以後,聲明的方式有 var 、 let 、 const 、 function 、 class 。前端

const 用來定義常量,使用時必須初始化(即必須賦值),只能在塊做用域裏訪問,並且不能修改。java

let      let是相對var的,優勢是let 聲明的變量不存在變量提高。nginx

var     存在變量提高,變量提高是指不管 var 出如今一個做用域的哪一個位置,這個聲明都屬於當前的整個做用域,在其中處處均可以訪問到。面試

參考連接: JavaScript中var、let和const的區別   js中 let var const 的差別和使用場景  JS中的塊級做用域,var、let、const三者的區別docker

 

關於 局部變量 和 全局變量

js的變量有兩種做用域:全局變量和局部變量。瀏覽器

沒有使用 var 聲明的變量和在function以外聲明的變量都是全局變量,是window屬性之一;緩存

使用 var 聲明的變量屬於所在函數,無論在函數的哪一個位置出現,等價於在函數一開始聲明

局部變量比同名全局變量的優先級高,因此局部變量會隱藏同名的全局變量。要想訪問被隱藏的全局變量就加上 window. 前綴。

js(ES6以前)沒有塊做用域,在語句塊以後,在函數結束以前,語句塊定義的變量都是能夠訪問的。

好比:for(var idx =0; idx<2;idx++){} alert(idx); 結果顯示爲2。ES6出現後,有了let ,這裏代碼若是爲for(let idx =0; idx<2;idx++){} alert(idx);  則會運行時報錯:idx is not difined;

關於局部變量和全局變量的示例:

<script type="text/javascript">
var a = 1;
function hehe()
{
         window.alert(a);//undifined
         var a = 2; //聲明瞭局部變量
         window.alert(a);//2
}
hehe();
</script>
若是上面這個例子中把 var 換成 let ,則會報錯,由於 let 在同一個範圍內不能重複定義。 
<script type="text/javascript">
var a = 1;
function hehe()
{
         window.alert(a);//1
         a = 2; // 修改的是全局變量 a
         window.alert(a);//2
}
hehe();
</script>
<script type="text/javascript">
var a = 1;
function hehe()
{
     window.alert(this.a);//1 //訪問全局變量  this.a 換成 window.a 也能夠  關於 this 的指向,就一句話:誰最終調用函數,this指向誰!!! 參考 this指向
     var a = 2;
     window.alert(a);//2
}
hehe();
</script> 

參考連接:JavaScript局部變量和全局變量的理解

 

關於 js 閉包

JavaScript 變量屬於本地或全局做用域。全局變量可以經過閉包實現局部(私有)。

Javascript容許使用內部函數---即函數定義和函數表達式位於另外一個函數的函數體內。

這些內部函數能夠訪問它們所在的外部函數中聲明的全部局部變量、參數和聲明的其餘內部函數。

當其中一個這樣的內部函數在包含它們的外部函數以外被調用時,就會造成閉包。

詳見下文。

參考:W3school JS 閉包

原文連接:http://www.javashuo.com/article/p-pjavktlw-z.html

在前端開發中閉包是一個很重要的知識點,是面試中必定會被問到的內容。以前我對閉包的理解主要是"經過閉包能夠在函數外部能訪問到函數內部的變量",對閉包運用的也不多,甚至本身寫過閉包本身都不太清楚,只知道這樣寫能夠解決問題。最近在梳理本身的js知識點,發現本身對js閉包理解的很不透徹,因而想全面的分析一下閉包,特別是閉包的造成緣由和閉包的使用場景。

閉包的定義

閉包是指有權訪問另外一個函數做用域中的變量的函數 --《JavaScript高級程序設計》

函數對象能夠經過做用域關聯起來,函數體內的變量均可以保存在函數做用域內,這在計算機科學文獻中稱爲「閉包」,全部的javascirpt函數都是閉包 --《Javascript權威指南》

看完這些專業的定義是否是感受一頭霧水,不要緊,我和同樣也沒明白這些定義說的是啥,咱接着往下分析。

在認識閉包原理以前咱們先必須對做用域、執行上下文、執行上下文堆棧、變量對象、活動對象、做用域鏈有着全面的認識

做用域 Scope

做用域是一套規則,用於肯定在何處以及如何查找變量(標識符)
做用域共有兩種主要的工做模型:

  • 詞法做用域:做用域是在編寫代碼的時候肯定的
  • 動態做用域:做用域是在代碼運行的時候肯定的

咱們知道javascript使用的是詞法做用域

執行上下文 Execution Contexts

Javascript中代碼的執行上下文分爲如下三種:

  • 全局級別的代碼 – 這個是默認的代碼運行環境,一旦代碼被載入,引擎最早進入的就是這個環境。
  • 函數級別的代碼 – 當執行一個函數時,運行函數體中的代碼。
  • Eval的代碼 – 在Eval函數內運行的代碼。

一個執行的上下文能夠抽象的理解爲一個對象。每個執行的上下文都有一系列的屬性(變量對象(variable object),this指針(this value),做用域鏈(scope chain) )

Execution Contexts = { variable object:變量對象; this value: this指針; scope chain:做用域鏈; }

執行上下文堆棧 Execution Contexts Stack

活動的執行上下文在邏輯上組成一個堆棧。堆棧底部永遠都是全局上下文(globalContext),而頂部就是當前(活動的)執行上下文。

<script> function add(num){ var sum = 5; return sum + num; } var sum = add(4); </script>

當add函數被調用時,add函數執行上下文被壓入執行上下文堆棧的頂端,此時執行上下文堆棧可表示爲:

EC Stack = [
  <add> functionContext globalContext ];

add函數執行完畢後,其執行上下文將會從執行上下文堆棧頂端彈出並被銷燬。全局執行上下文只有在瀏覽器關閉時纔會從執行上下文堆棧中銷燬

變量對象 Variable Object

若是變量與執行上下文相關,那變量本身應該知道它的數據存儲在哪裏,而且知道如何訪問。這種機制稱爲變量對象(variable object)。
能夠說變量對象是與執行上下文相關的數據做用域(scope of data) 。它是與執行上下文關聯的特殊對象,用於存儲被定義在執行上下文中的變量(variables)、函數聲明(function declarations) 。
當進入全局上下文時,全局上下文的變量對象可表示爲:

VO = {  add: <reference to function>,  sum: undefined,  Math: <...>,  String: <...> ...  window: global //引用自身 }

活動對象 Activation Object

當函數被調用者激活時,這個特殊的活動對象(activation object) 就被建立了。它包含普通參數(formal parameters) 與特殊參數(arguments)對象(具備索引屬性的參數映射表)。活動對象在函數上下文中做爲變量對象使用。
當add函數被調用時,add函數執行上下文被壓入執行上下文堆棧的頂端,add函數執行上下文中活動對象可表示爲

AO = {
    num: 4,
    sum :5,
    arguments:{0:4}
}

做用域鏈 Scope Chain

函數上下文的做用域鏈在函數調用時建立的,包含活動對象AO和這個函數內部的[[scope]]屬性。

var x = 10; function foo() { var y = 20; function bar() { var z = 30; alert(x + y + z); } bar(); } foo(); 

在這段代碼中咱們看到變量"y"在函數"foo"中定義(意味着它在foo上下文的AO中)"z"在函數"bar"中定義,可是變量"x"並未在"bar"上下文中定義,相應地,它也不會添加到"bar"的AO中。乍一看,變量"x"相對於函數"bar"根本就不存在;
函數"bar"如何訪問到變量"x"?理論上函數應該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機制是經過函數內部的[[scope]]屬性來實現的。
[[scope]]是全部父級變量對象的層級鏈,處於當前函數上下文之上,在函數建立時存於其中。

注意: [[scope]]在函數建立時被存儲是靜態的(不變的),直至函數銷燬。即:函數能夠永不調用,但[[scope]]屬性已經寫入,並存儲在函數對象中。
在這裏咱們逐步分析下
全局上下文的變量對象是:

globalContext.VO === Global = { x: 10 foo: <reference to function> };

在"foo"建立時,"foo"的[[scope]]屬性是:

foo.[[Scope]] = [ globalContext.VO ];

在"foo"激活時(進入上下文),"foo"上下文的活動對象是:

fooContext.AO = {
  y: 20, bar: <reference to function> };

"foo"上下文的做用域鏈爲:

fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

內部函數"bar"建立時,其[[scope]]爲:

bar.[[Scope]] = [ fooContext.AO, globalContext.VO ];

在"bar"激活時,"bar"上下文的活動對象爲:

barContext.AO = {  z: 30 };

"bar"上下文的做用域鏈爲:

bar.Scope= [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

介紹了一大堆內容,是否是有點暈忽忽的?堅持一下,下面是重點

閉包的原理

咱們經過一個閉包的例子來分析一下閉包的造成原理

function add(){ var sum =5; var func = function () { console.log(sum); } return func; } var addFunc = add(); addFunc(); //5

js執行流進入全局執行上下文環境時,全局執行上下文可表示爲:

globalContext = {  VO: {  add: <reference to function>,  addFunc: undefined },  this: window, scope chain: window }

當add函數被調用時,add函數執行上下文可表示爲:

addContext = {
    AO: { sum: undefined //代碼進入執行階段時此處被賦值爲5 func: undefined //代碼進入執行階段時此處被賦值爲function (){console.log(sum);} }, this: window, scope chain: addContext.AO + globalContext.VO }

add函數執行完畢後,js執行流回到全局上下文環境中,將add函數的返回值賦值給addFunc。

因爲addFunc仍保存着func函數的引用,因此add函數執行上下文從執行上下文堆棧頂端彈出後並未被銷燬而是保存在內存中。

當addFunc()執行時,func函數被調用,此時func函數執行上下文可表示爲:

funcContext = {
    this: window, scope chain: addContext.AO + globalContext.VO }

當要訪問變量sum時,func的活動對象中未能找到,則會沿着做用域鏈查找,因爲js遵循詞法做用域,做用域在函數建立階段就被肯定,在add函數的活動對象中找到sum = 5;

介紹到這裏你明白造成閉包的緣由了嗎?
Javascript容許使用內部函數---即函數定義和函數表達式位於另外一個函數的函數體內。並且,這些內部函數能夠訪問它們所在的外部函數中聲明的全部局部變量、參數和聲明的其餘內部函數。當其中一個這樣的內部函數在包含它們的外部函數以外被調用時,就會造成閉包。

閉包的用途

閉包能夠用在許多地方。它的最大用處有兩個,一個是前面提到的能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中。

1. 保護變量的安全實現JS私有屬性和私有方法

利用閉包能夠讀取函數內部的變量,變量在函數外部不能直接讀取到,從而達到保護變量安全的做用。由於私有方法在函數內部都能被訪問到,從而實現了私有屬性和方法的共享。
常見的模塊模式就是利用閉包的這種特性創建的

var Counter = (function() { //私有屬性 var privateCounter = 0; //私有方法 function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(privateCounter); //privateCounter is not defined console.log(Counter.value()); // 0 Counter.increment(); Counter.increment(); console.log(Counter.value()); // 2 Counter.decrement(); console.log(Counter.value()); // 1

在jQuery框架的私有方法和變量也是這麼設計的

var $ = jQuery = function(){ return jQuery.fn.init(); } jQuery.fn = jQuery.prototype = { init:function(){ return this; //this指向jQuery.prototype }, length: 1, size: function(){ return this.length; } } console.log($().size()); // 1

2. 將處理結果緩存

var mult = (function(){ var cache = {}; var calculate = function(){ var a = 1; for(vari=0,l=arguments.length;i<l;i++){ a = a*arguments[i]; } return a; }; return function(){ var args = Array.prototype.join.call(arguments,','); if(args in cache){ return cache[args]; } return cache[args] = calculate.apply(null,arguments); } })();

這樣咱們在第二次調用的時候,就會從緩存中讀取到該對象。

理解了閉包的原理咱們發現閉包的這些用途都是利用了閉包保存了當前函數的活動對象的特色,這樣閉包函數在做用域以外被調用時依然可以訪問其建立時的做用域

閉包的缺點

  • 閉包將函數的活動對象維持在內存中,過分使用閉包會致使內存佔用過多,因此在使用完後須要將保存在內存中的活動對象解除引用;
  • 閉包只能取得外部函數中任何變量的最後一個值,在使用循環且返回的函數中帶有循環變量時會獲得錯誤結果;
  • 當返回的函數爲匿名函數時,注意匿名函數中的this指的是window對象。

這裏僅僅是我對閉包的一些看法,如有錯誤的地方,還望你們提出,一塊兒交流共同進步!
參考文獻

若是您對本文有什麼疑問,歡迎提出我的看法,若您以爲本文對你有用,不妨幫忙點個贊,或者在評論裏給我一句讚美,小小成就都是從此繼續爲你們編寫優質文章的動力, 歡迎您持續關注個人博客:)

做者:Jesse131

出處:http://www.cnblogs.com/jesse131/

關於做者:專一前端開發。若有問題或建議,請多多賜教!

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。

相關文章
相關標籤/搜索