閉包的相關解釋合集(來源於知乎)

作者:wyatt-pan
鏈接:https://www.zhihu.com/question/19554716/answer/12763637
來源:知乎
著作權歸作者所有,轉載請聯繫作者獲得授權。

JavaScript 中的閉包與其 Scope Chain 特性真是密不可分的.
JavaScript 中的閉包:
例子:
def foo() {
var a = 1;
def bar() {
a = a + 1;
alert(a);
}
return bar;
}

var closure = foo(); // 這個時候返回的是 bar() 這個函數外加其包上的變量 a;
var closure2 = foo(); // 這裏同樣生成了另外一個閉包(實例)
closure(); // 2
closure2(); // 2 , 綁定了另外一份變量 a
closure(); // 3

對於常規的 foo() 方法來說, 在其內部的變量 a 的存在應該在 foo() 方法執行完畢以後就消失了, 但是 foo() 方法返回了一個新的方法 bar(), 而這個方法卻訪問到了 foo() 方法的變量 a (JavaScript 通過 Scope Chain 訪問到父級屬性), 而方法 bar() 的存在延長了變量 a 的存在時間, 類似與將變量 a 關閉在了自己的作用域範圍內一樣, 只要方法 bar() 沒有失效, 那麼變量 a 則會一直伴隨着方法 bar() 存在, 而變量 a 與方法 bar() 的這樣存在形式被稱爲閉包;

閉包的應用:
在我看來, 雖然時常聽到閉包這個概念, 但真正的應用還真不是很多, 也沒看到或者想到比較經典的應用. 在 JavaScript 中, 使用得最多的, 恐怕還是將 function 作爲 first class 的情況使用, 就好比你可以 var a = new Number(0); 可以將 a 當作函數的參數不斷的進行傳遞, 你也可以 var f = new Function(arg1, arg2...., functionBody) [new Function("x", "y", "return (x + y)/2")] 來定義函數, 然後將 f 作爲參數在函數中不斷的傳遞; 但我一般不會讓這個 function 產生閉包來進行傳遞, 而是會傳遞一個獨立的 function 避免寫多了自己都忘記是哪個了.

JavaScript 閉包的實現:
如開篇所說, JavaScript 中的閉包實現與 JavaScript 的 Scope Chain 是密不可分的. 首先在 JavaScript 的執行中會一直存在一個 Execute Context Stack (想想 JavaScript 解釋器在看到一個 alert(x) 的時候, 如果沒有上下文他怎麼知道這個 x 是什麼?), Execute Context Stack 中最下面一個一定是 GlobalContext, 而在每一個函數的執行開始就會向這個 stack 中壓入一個此 Function 的 Execution Context; 而一個 Execution Context 的組成分爲三部分:
1. Variable Object: 存儲方法內的變量 vars, 方法傳入的參數, 函數內定義的函數等等(函數表達式不保存), Variable Object 在任何時候是不可以被直接訪問到的, 當然不同的 JS 引擎提供了訪問接口就說不定了;
2. Scope Chain: 這個函數執行的時候用以尋找值的 Scope Chain, 這個 Scope Chain 由 Variable Object + All Parent Scopes 組成, Variable Object 會放在這個 Scope Chain 的最前面, 這也是爲什麼函數內的變量會被最先找到;
3. thisValue, 函數被調用的時候的 this 對象, 存儲的就是函數的調用者(caller)的引用;

對於 Variable Object 在不同的情況下會有不同的定義, 例如在全局的時候被稱爲 Global Object, 而在函數中則被稱爲 Activation Object 激活對象;

正是由於有了 Execution Context 中的 Scope Chain, JavaScript 才能夠使得在方法 bar()

的內部訪問到方法 foo() 中的變量 a, 才能夠使方法 bar() 將變量 a 關閉在自己的作用範圍內不讓他隨 foo() 方法的執行完畢而銷燬;


說個應用場景。
利用閉包可以給對象設置私有屬性並利用特權(Privileged)方法訪問私有屬性。
  var Foo = function(){
      var name = 'fooname';
      var age = 12;
      this.getName = function(){
          return name;
      };
      this.getAge = function(){
          return age;
      };
  };
  var foo = new Foo();

  foo.name;        //  => undefined
  foo.age;         //  => undefined
  foo.getName();   //  => 'fooname'
  foo.getAge();    //  => 12


作者:甘洪翔
鏈接:https://www.zhihu.com/question/19554716/answer/24426087
來源:知乎
著作權歸作者所有,轉載請聯繫作者獲得授權。


作者:gyfnice
鏈接:https://www.zhihu.com/question/19554716/answer/40356052
來源:知乎
著作權歸作者所有,轉載請聯繫作者獲得授權。

從前:
有一位公主......
function princess() {
她生活在一個充滿奇幻冒險的世界裏, 她遇到了她的白馬王子, 帶着他騎着獨角獸開始周遊這個世界,與巨龍戰鬥,巧遇會說話的動物,還有其他一些新奇的事物。
    var adventures = [];

    function princeCharming() { /* ... */ } //白馬王子

    var unicorn = { /* ... */ },          //獨角獸
        dragons = [ /* ... */ ],         //龍
        squirrel = "Hello!";            //松鼠

    adventures.push(unicorn, dragons, squirrel, ....);
但是她不得不回到她的王國裏,面對那些年老的大臣。
return {
她會經常給那些大臣們分享她作爲公主最近在外面充滿奇幻的冒險經歷。
        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}
但是在大臣們的眼裏,總是認爲她只是個小女孩......
var littleGirl = princess();
....講的是一些不切實際,充滿想象的故事
littleGirl.story();
即便所有大臣們知道他們眼前的小女孩是真的公主,但是他們卻不會相信有巨龍或獨角獸,因爲他們自己從來沒有見到過。大臣們只會覺得它們只存在於小女孩的想象之中。

但是我們卻知道小女孩述說的是事實.......




作者:倪雲建
鏈接:https://www.zhihu.com/question/19554716/answer/98106832
來源:知乎
著作權歸作者所有,轉載請聯繫作者獲得授權。

先說結論。閉包,簡單說是指:函數對象本身和這個函數關聯的作用域鏈的結合

上面的定義可能聽不懂,爲了詳細說明這個概念,需要先解釋清楚JavaScript中作用域相關的概念。

變量作用域

變量作用域是一個變量在定義後在源碼中可見的範圍。

JavaScript中存在兩種變量作用域:

全局作用域:全局變量具有全局作用域,在整個JavaScript代碼中都可見

本地作用域:
  • 在函數中定義的變量就是本地變量,這些變量具有本地作用域,他們在其被定義的函數體內部完全可見,函數的參數也算作該函數的本地變量。
  • 本地變量可以覆蓋同名的全局變量
  • 函數可以嵌套,且每一個函數都具有自己的本地作用域環境。

例子:

/**
 * 全局變量,全局可見
 */
var globalVar = 'global Scope';
var anotherGlobalVar = 'another global var'

function fn(){
     var localVar = 'local Scope';
     var anotherGlobalVar = 'global override';
     console.log( localVar );
     console.log( globalVar );
     console.log( anotherGlobalVar );
}

fn(); // 將輸出:   'local Scope'  'globalScope' 'global override'

console.log( localVar ); // 報錯, localVar 不存在

函數作用域

JavaScript使用函數作用域規則來決定變量的作用域範圍。這意味着,一個在函數中被定義的變量,在這個函數體中,以及該函數體中嵌套定義的所有函數內部都可見。另外和同名本地變量覆蓋全局變量同理,嵌套函數中的同名本地變量也會覆蓋其上層函數中的本地變量。

例子:
function fn(){
     var localVar = 'fn var';

     function nestedFn(){
          var localVar = 'nested fn var';
          console.log( localVar );          // ==> 'nested fn var'
     }

     nestedFn();
}

fn();

作爲對象屬性的變量

變量的本質是通過對象來組織到一起的屬性集合。

當你定義了一個全局變量,你是在全局對象上定義了一個屬性(這個全局對象根據宿主環境不一樣而不一樣,比如在瀏覽器裏面就是window)

var a = {};
console.log( window.a === a ); // true

對於本地變量,你可以同樣的將他們視爲某個對象的屬性,只是在JavaScript中,你並沒有方法獲取到這個對象。在ES3中,這個對象被稱爲“調用對象”(Call Object),在ES5中被稱爲“聲明上下文”(declarative enviroment record),以下面這個函數定義爲例:
function fn( parameter ){
     var localVar = 'local var';
}

若我們能找到辦法獲取到這個“調用對象”的對象引用,即爲A,則A將具有如下的屬性:
A.parameter
A.localVar

作用域鏈

結合上面的所有信息,在JS的實際執行中,爲了準確的定位到一個標示符具體指向的是哪個變量,我們需要了解作用域鏈的概念。爲什麼需要這個聽起來很複雜的東西?先看看下面的代碼:
var neekey = { name: 'Neekey' };

function run( name ){

     var originName = myName;
     var myName = name;

     function sayMyCurrentName(){
          console.log( myName );
     }

     function changeMyName( name ){
          originName = name;
          neekey.name = name;
     }

     function sayMyOriginName(){
          console.log( originName );
     }

     sayMyCurrentName();     // Neekey
     changeMyName( 'Nick' );
     sayMyOriginName();      // Nick
}

run( neekey.name );

console.log( neekey.name ); // Nick

這個例子其實不算複雜,但是你在看的時候還是要仔細注意這些個`name`,`myName`,`originName` 都實際指向的是哪個變量。

首先作用域鏈是什麼呢?JavaScript中的每一塊代碼都會與一個作用域鏈相關聯,這個鏈是一個對象列表,對於每一個標示符,都依次從這個鏈的對象中查找具有同樣標示符的屬性。而作用域鏈就是由全局變量和不定數量的由函數調用產生的調用對象構成的列表。

看下面的例子:
var x = 1;
var y = 2;

// 代碼執行到此,爲了確定x和y,查詢其作用域鏈: [ window ],從 window.x 和 window.y 中取出了值
console.log( x + y );

function fnA(){
     // 函數被調用,產生了fnA的調用對象(即爲a),用於查詢變量的作用域鏈爲:[ a, window ]
     var x = 2;
     // a.x 定位到x,a.y不存在,繼續從 window.y 中定位到y
     console.log( x + y );
}

fn();

需要注意,在一個函數剛剛被定義時,還沒有調用對象存在,只有當這個函數開始被調用執行時,纔會有一個調用對象被創建用於儲存其中的本地變量。也就是說,相同的一個函數,在每一次調用都會產生一個獨立的調用對象。

另外,在函數剛剛被定義時,除了新建了一個函數對象(Function Object)外,還將當前的作用域鏈和這個函數對象關聯在了一起:
var x = 1;
var y = 2;

// 此時的作用域鏈: [ window ]
// fnA 函數定義後,當前作用域鏈和fnA 關聯了起來,我們可以假設存在類似 fnA.__scope_chain = [ window ] 這樣的東西存在
function fnA(){
     // 函數調用開始,新建了一個調用對象a,和之前關聯起來的作用域鏈結合,生成了屬於該函數調用過程的作用域鏈: [ a, window ]
     var x = 2;
     console.log( x + y );
}

fn();

所以當函數嵌套很多的時候,作用域鏈會很長:
var x = 1;
var y = 2;

// [ window ]
// fnA.__scope_chain = [ window ]
function fnA(){

     // [ a, window ]
     // fB.__scope_chain = [ a, window ]
     function fnB(){

          // [ b, a, window ]
          // fnC.__scope.chain = [ b, a, window ]
          function fnC(){

                // N次嵌套後

               // [ n … b, a, window ]
               // fnN.__scope.chain = [ n …. b, a, window ]
               function fnN() {

                    // [ n+1, n, … b, a, window ]
               }
          }
     }
}

fn();

詞法作用域

詞法作用域(lexical scoping)是指,函數在執行時,使用的是它被定義時的作用域,而不是這個函數被調用時的作用域。

有點抽象,我們看下面的例子:
var scope = 'global scope';
function checkScope(){
     var scope = 'local scope';

     return function(){
         console.log( scope );
     }
}

var f = checkScope();
f();      // local scope

上面的例子中,核心是嵌套定義的這個函數:

return function(){
     console.log( scope );
}

雖然實際該函數在執行時`fn()`時作用域中的`scope`是 `global scope`,但是實際執行使用的是定義函數時的那個 ‘local scope’,集合上面作用域鏈的知識,所謂的詞法作用域規定了:

函數在執行時使用的作用域鏈是它在定義時綁定的那個作用域鏈(準確地說是使用當時綁定的那個作用域鏈加上新建的調用對象組成的新的作用域鏈),而不是函數調用時所處上下文的作用域鏈。

回到閉包上來

上面講了這麼多概念後,回到一開始的答案:

函數對象本身和這個函數關聯的作用域鏈的結合。

也即是:fn + fn.__scope_chain (並不存在__scope_chain,只是爲了讓這個關聯更加形象)

其實,由於作用域鏈本身的特性,以及函數在定義時就能和作用域鏈關聯起來的自然特性,可以簡單說:在JavaScript中,每個函數都是閉包。

理解了閉包本身後,你會發現,閉包的強大之處其實在於:JavaScript中的函數,通過作用域鏈和詞法作用域兩者的特性,將該函數定義時的所處的作用域中的相關函數進行了捕獲和保存,從而可以在完全不同的上下文中進行引用。

發佈了10 篇原創文章 · 獲贊 9 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章