閉包理解

面試必問題目,但總以爲理解得不深刻,索性寫一篇文章慢慢梳理吧。面試

什麼是閉包

紅寶書上給出的定義是:閉包是指有權訪問另外一個函數做用域中的變量的函數,看到另一個理解是:函數和函數內部能訪問到的變量(或者環境)的總合,就是一個閉包。建立一個閉包最多見的方式就是在一個函數內部建立另外一個函數。下面寫一個例子:閉包

function f1() {
  var a = 1;
  function closure() {
    console.log(++a);
  } 
  return closure;
}

上面例子中,f1 內部的匿名函數以及它可以訪問到的外部函數的變量 a 合在一塊兒,就造成了一個閉包。使用 return 將閉包返回的目的是讓它能夠被外部訪問。下面看看它怎麼使用:函數

var f2 = f1();   // 執行外部函數,返回閉包
f2();     // 2
f2();     // 3
f2();     // 4

第一句執行函數 f1() 後,閉包被返回並賦值給了一個全局變量 f2,之後每次調用 f2(),變量 a 的值就會加 1。一般函數執行完畢後,其做用域鏈和活動對象都會被銷燬,爲何這裏 a 並無被銷燬而且每次執行 f2() 還會被遞增?緣由是閉包有權訪問外部函數的變量,進一步說,閉包的做用域鏈會引用外部函數的活動對象,因此 f2() 在執行時,其做用域鏈其實是:性能

  1. 自身的活動對象;
  2. f1() 的活動對象;
  3. 全局變量對象。

因此 f1() 執行完後,其執行環境的做用域鏈會被銷燬,但活動對象仍然會留在內存中,由於閉包做用域鏈在引用這個活動對象(說白了就是閉包還須要使用外層函數的變量,不容許它們被銷燬),直到閉包被銷燬後,f1() 的活動對象纔會被銷燬。this

上面例子中,是將返回的閉包賦值給了一個全局變量 f2var f2 = f1();f2 是不會被銷燬的,每次執行完 f2(),閉包的做用域鏈不會被銷燬,因此就會出現每次執行 f2()a 遞增。code

可是換一種閉包的調用方式,狀況會不一樣:對象

f1()();   // 2
f1()();   // 2

由於沒有把閉包賦值給一個全局變量,閉包執行完後,其執行域鏈與活動對象都銷燬了。接口

閉包的做用

建立用於訪問私有變量的公有方法

其實構造函數中定義的實例方法,就是閉包:內存

function Person(){
  var name = 'Leon';
  function sayHi() {
    alert('Hi!');
  }
  this.publicMethod = function() {
    alert(name);
    return sayHi();
  }
}

構造函數 Person 中定義實例方法 publicMethod() 就是一個閉包,它能夠訪問外部函數的變量 name 和 函數 sayHi(),爲何要這麼作呢?由於咱們想在構造函數中定義一些私有變量,讓外部不能直接訪問,只能經過定義好的公有方法訪問,從而達到保護變量,收斂外部權限的目的。作用域

而在普通函數中,把閉包 return 出去供外部使用,其實目的也就是:讓函數內部的變量始終保持在內存中,同時保護這些變量,讓它們不能被直接訪問。

function person(){
  var name = 'Leon';
  function sayHi() {
    alert('Hi!');
  }
  function publicMethod() {
    alert(name);
    return sayHi();
  }
  return publicMethod;
}

閉包用於建立單例

所謂單例,就是隻有一個實例的對象。單例模式的好處在於:

  • 保證一個類只有一個實例,避免了一個在全局範圍內使用的實例頻繁建立與銷燬。

    • 好比網頁中的彈窗,點擊 a 按鈕彈出,點擊 b 按鈕隱藏,若是彈窗每一次彈出都須要新建一個對象,將會形成性能的浪費,更好的辦法就是隻實例化一個對象,一直使用。
  • 劃分了命名空間,避免了與全局命名空間的衝突。

    • 好比在一個單例中能夠定義不少方法,經過單例.方法來使用,避免了在全局環境中定義函數,形成函數名衝突。

下面逐步介紹下單例的建立方式,後兩種方式將用到閉包。

1. 對象字面量建立單例

var singleton = {
  attr1: 1,
  attr2: 2,
  method: function () {
    return this.attr1 + this.attr2;
  }
}
var s1 = singleton;
var s2 = singleton;
console.log(s1 == s2)  // true

上面用字面量形式建立了一個單例,能夠看到 s1s2 是等同的。這種方式的問題在於外部能夠直接訪問單例的內部變量並加以修改,若是想讓單例擁有私有變量,就須要使用模塊模式,模塊模式就是用了閉包。

2. 模塊模式

JS 中的模塊模式的做用是:爲單例添加私有變量和公有方法。它使用當即執行函數和閉包來達到目的。

var singleton = (function(){
  // 建立私有變量
  var privateNum = 1;
  // 建立私有函數
  function privateFunc(){
    console.log(++privateNum);
  }
  // 返回一個對象包含公有方法
  return {
      publicMethod: function(){
        console.log(privateNum)
        return privateFunc()
      }
  };
})();

singleton.publicMethod();
// 1
// 2

這裏首先定義了一個當即執行函數,它返回一個對象,該對象中有一個閉包 publicMethod(), 它能夠訪問外部函數的私有變量。從而這個被返回的對象就成爲了單例的公共接口,外部能夠經過它的公有方法訪問私有變量而無權直接修改。總結一下就是兩點:

  • 當即執行函數能夠建立一個塊級做用域, 避免在全局環境中添加變量。
  • 閉包能夠訪問外層函數中的變量。

3. 構造函數+閉包

上面提到的對象字面是用來建立單例的方法之一,既然單例只能被實例化一次,不難想到,在使用構造函數新建實例時,先判斷實例是否已被新建,未被新建則新建實例,不然直接返回已被新建的實例。

var Singleton = function(name){
  this.name = name;
};

// 獲取實例對象
var getInstance = (function() {
  var instance = null;
  return function(name) {
      if(!instance) {
          instance = new Singleton(name);
      }
      return instance;
  }
})();

var a = getInstance('1');
console.log(a);  // {name: "1"}
var b = getInstance('2');
console.log(b);  // {name: "1"}

這裏將構造函數和實例化過程進行了分離, getInstance()中存在一個閉包,它能夠訪問到外部變量 instance,第一次 instance = null,則經過 new Singleton(name) 新建實例,並將這個實例保存在instance 中,以後再想新建實例,由於閉包訪問到的instance已經有值了,就會直接返回以前實例化的對象。

相關文章
相關標籤/搜索