從閉包到做用域再到具體的應用

前言

該篇文章是我我的《JavaScript高級程序設計》精讀筆記系列中節選的文章,感受此次系統基礎的學習JS基礎知識確實學到了不少,特此來跟你們分享,勿怪,勿笑 ╰( ̄▽ ̄)╭ ,有錯誤或者是建議的小夥伴請踊躍發言,謝謝你們!前端

那麼,本篇文章你能學到什麼?webpack

  • 閉包的概念
  • 詞法做用域/靜態做用域/動態做用域
  • 做用域與做用域鏈
  • 標識符在做用域鏈上的解析
  • 換個角度,經過做用域與做用域鏈來看待 this 指向和函數的 arguments
  • 經過閉包實現對象的私有成員變量
  • 經過閉包實現模塊模式

閉包

「閉包(closure)」實際上就是被返回的函數有權訪問其包含函數中定義的變量和標識符。 「閉包」能夠訪問其包含函數中的變量標識符,原理在於閉包函數的做用域鏈中引用了包含函數的做用域。web

function debounce(fn, wait) {
  var timeout = null;
  return function() {
    var args = arguments;
    var that = this;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(function() {
      fun.apply(that, args);
    }, wait);
  };
}
複製代碼

在這個「防抖」的示例中,debounce 就是「包含函數」,而 debounce 函數內部經過 return 語句返回的匿名函數就是前面咱們所說的 —— 「閉包函數」。 對於「包含函數」、「匿名閉包函數」 以及全局對象 window,它們之間的包含關係,相信很快就會聯想到下圖。express

在瞭解了關於「閉包」的大體概念後,如今咱們來繼續瞭解「做用域」、「做用域鏈」的相關知識,由於這些纔是閉包的底層原理。 首先、「做用域」指的是變量的訪問範圍,由於 JS 是「詞法做用域」(lexical scoping),即「靜態做用域」而非「動態做用域」,因此函數的做用域範圍在函數定義的時候便已經確立,在 JS 中每一個函數都會有獨立的局部做用域,因此咱們要說的「做用域鏈」實際上就是多個函數做用域的串聯,這一點很是相似與對象繼承關係的「原型鏈」。但與原型鏈中原型屬性 [[Prototype]] 屬性與原型對象 prorotype 的關聯關係又有所不一樣,「做用域鏈」其實是多層級函數嵌套的過程,即做用域鏈內的每個環節(做用域)都對應者嵌套的每一層函數,關於這點的理解的圖示,可見讀書筆記的第四章。瀏覽器

「做用域」與「做用域鏈」聽上去很虛,難以理解,那麼咱們就尋找它們存在的實物而後參照着去理解,首先咱們是否還記得第五章函數類型時咱們分析過,當一個函數被執行的時候,首先會建立該函數的執行上下文 EC,而後將該上下文加入執行棧中等待執行,實際中當函數的執行上下文建立好後,分別會使用 this 以及函數的 arguments 和其它函數的形參來初始化執行環境的 「變量對象」,因爲這個變量對象只在函數執行的時候才被建立,所以也稱之爲「活動對象」,同時初始化的還有「做用域鏈」對象並將其保存在函數內部的 [[scope]] 屬性中。緩存

function fn() {}
console.dir(fn);
/* arguments: null caller: null length: 0 name: "fn" prototype: {constructor: ƒ} __proto__: ƒ () [[Scopes]]: Scopes[1] */
複製代碼

拋開「原型鏈」對象,咱們知道「變量對象」其實是一個 MAP 表結構,存放了當前函數內的全部標識符(私有變量、函數的形參、arguments),當在函數內部去訪問這些私有的標識符的時候,都會前往該函數的活動對象中去查找,所以「活動對象」就決定了某個標識符是否能解析獲取成功,即「變量對象」就是「做用域」,由於它決定了當前函數內的標識符解析。安全

每一個函數都有本身的」活動對象「,因此每一個函數都有本身的獨立做用域,而「做用域鏈」則是指向這些變量對象的指針列表,它只引用但不實際包含變量對象,所以每一個函數的做用域鏈對象中都會有一個引用自身的「變量對象」,若是這個函數還被嵌套在其它函數中,那麼其做用域鏈表的第二個位置就是其嵌套函數「活動對象」,依次類推做用域鏈表的最頂端必定是全局環境 window 的「變量對象」 —— 即全局做用域,由於全部的局部代碼都是嵌套在全局代碼內的,這也很好的說明了爲何咱們不論在那裏編寫代碼,總能訪問到全局變量。閉包

總結下,「變量對象」或者是「活動對象」就是「做用域」,它們決定了某個變量或標識符可否查詢到,而「做用域鏈」對象則是保存了一張做用域鏈表,這張表裏除了保存當前做用域的引用,還保存了當前做用域上層以及上上層等做用域的引用(若是有的話),最終到達「全局做用域」。app

與原型鏈中原型屬性的訪問機制相同,若是要解析的標識符沒有在當前的做用域對象中查詢到,則會沿着做用域鏈表的順序依次在每一個做用域中進行查找,若是查詢到了則會中止解析,哪怕再上層具備同名的標識符也不會去解析 —— 這就是標識符在做用域鏈中的解析過程。異步

閉包與變量

因爲「閉包函數」是在上一層執行環境執行完成後返回的,因此若是閉包函數內部經過做用域鏈去查詢上層活動對象中的標識符,那麼只會得到上層活動對象中全部變量的最後階段的值。

function getIndex() {
  var clouser = [];
  for (var i = 0; i < 10; i++) {
    clouser.push(function() {
      console.log(i); //每次都會是10
    });
  }
  return clouser;
}
複製代碼

緣由很簡單,那就是閉包返回的時候上層函數 getIndex 已經執行完了,其活動對象中保存的 i 變量的值已是 10,此時閉包再去獲取總會固定返回 10。 若是想每次執行閉包都返回對應的索引,咱們能夠在閉包函數外面再套一層「當即執行函數表達式」,套的緣由在於每次循環的時候都會建立一個匿名函數,而後把循環的索引值做爲參數傳遞這個匿名函數,讓每次經過循環生成的當即執行函數使用本身的活動對象來保存每一個階段循環的索引值,而後當即執行函數中再返回閉包,這樣閉包就能夠經過上層當即執行函數的活動對象來讀取循環的每一個階段的值,返回的也總會是每次循環的索引值。

function getIndex() {
  var clouser = [];
  for (var i = 0; i < 10; i++) {
    clouser.push(
      (function(index) {
        return function() {
          console.log(index); //每次都會是10
        };
      })(i)
    );
  }
  return clouser;
}
複製代碼

而後調用每次循環生成的函數

getIndex()[0](); //0
getIndex()[1](); //1
//....
getIndex()[9](); //9
複製代碼

閉包與 this

其實應該說的是「活動對象」與 this ,因爲每一個活動對象都會存在 this 對象,而且當函數或方法不屬於某個對象的成員屬性,那麼其活動對象中 this 默認指向的都是 window,所以就會形成內部函數每次搜索 this 的時候,只會搜索到當前活動對象爲止。即永遠不會再訪問外部函數活動對象的 this,並且固定的指向 window

若是內部函數想引用外部函數的 this 其實很簡單,只須要在外部函數的做用域中在定義一個私有變量,而後保存當前活動對象中的 this,那麼,當內部函數去解析這個變量標識符的時候,便會沿着做用域鏈到上層的做用域對象中去取這個變量所引用的 this

function Fn() {
  var that = this;
  return function() {
    console.log(that);
  };
}
複製代碼

閉包與 arguments

this 的原理相同,由於每一個函數的「活動對象」中都存在 arguments 對象,因此當內部函數每次搜素 arguments 的時候,都只會搜索到當前的活動對象爲止,即不會超出當前做用域的範圍。

閉包回收

對於主流瀏覽器而言閉包的回收很簡單,只須要將引用閉包的變量置爲 null 便可,但值得咱們注意的一點是,若是一個閉包沒有被回收,並且這個閉包內部還訪問了其它做用域中的標識符,那麼保存這些標識符的執行環境以及執行環境中的做用域鏈對象在執行完成後確實會被銷燬,可是這些執行環境中的「活動對象」卻永遠不會被銷燬,由於閉包的做用域鏈還對這些活動對象具備引用關係。

固然銷燬閉包等於銷燬一切,但這只是針對採用「標記清除」垃圾回收機制的現代瀏覽器而言的,對於採用「引用計數」機制的老版本 IE(IE8-),咱們在編碼的時候就須要具體的考量,以免循環引用的狀況發生。

var handleClick = function() {
  var elem = document.getElementById("btn");
  elem.onclick = function() {
    return elem.id;
  };
};
複製代碼

本來的本意是定義一個 handleClick 的函數用於封裝 btn 元素的點擊事件,可是因爲 elem 這個 DOM 對象經過 onclick 引用了一個匿名函數,而這個匿名函數中又由於返回 elem.id 的值,再次引用了 elem DOM 元素,所以就產生了循環引用的狀況,若是在老版本的 IE,那麼這裏的標識符將沒法被正常的回收。

此時,咱們就須要在編碼的過程當中仔細的考量,謹慎的定義多個對象之間的引用關係。

var handleClick = function() {
  var elem = document.getElementById("btn");
  var id = elem.id;

  elem.onclick = function() {
    return id;
  };

  elem = null;
};
複製代碼

對象的私有變量

對象不像函數,具備獨立的做用域且能夠定義私有的變量或方法,對象的成員屬性具備透明性,即全部的方法均可以訪問對象中的任何成員屬性或成員方法,對沒有進行不可擴展、密封、凍結的對象還能夠進行添加、修改、刪除該對象成員的操做。 若是要讓對象也具備函數私有變量同樣性質的私有成員,解決的辦法就是經過「對象」與「閉包」來模擬實現,爲何說是「模擬」由於咱們所認爲的私有成員實際上仍是閉包方法的上一層做用域的私有變量,只是該私有變量能夠被咱們所建立的對象進行引用,並且還只能經過對象上特定的方法才能訪問,但本質上該變量並不是是對象的實際成員。

咱們先從一個簡單的全局變量入手,咱們定義一個全局的變量 _g,而後在一個當即執行函數中爲這個全局變量賦值一個匿名函數表達式,這個匿名函數中存在着對上層做用域的變量進行引用。

var _g;

(function() {
  var value = "IIFE";
  _g = function() {
    return value;
  };
})();

_g();
複製代碼

在這個簡單的示例中咱們要學會兩個概念的定義,例如當即執行函數中的 value私有的變量,可以訪問這個私有的變量只有 _g 這個方法,所以咱們把有權訪問私有變量和私有函數的公有方法稱之爲 特權方法。 繼續深刻,如今若是如今這個全局變量 _g 是一個對象會怎麼樣呢?

var _g = {};

(function() {
  var value = "function expression";
  _g.getValue = function() {
    return value;
  };
  _g.setValue = function(v) {
    value = v;
  };
})();

_g.getValue(); //function expression
複製代碼

如今咱們就爲變量 _g 定義了一個模擬的私有成員屬性 value,這個屬性直接經過對象是沒法獲取到的,只有使用兩個特權方法 getValuesetVlaue 纔可以獲取以及操做。 可是示例的方式都是經過全局變量來引用當即執行函數中的私有函數,並且全局變量與當即執行函數也是分開定義的,缺乏總體性與封裝性,所以咱們還能夠繼續更一步的改進,即在對象建立的時候就定義私有成員以及特權方法。

對象的靜態私有變量

「對象的靜態私有變量」指的是在對象的構造函數中定義私有變量和特權方法,而後將特權方法做爲對象的實例成員隨同返回。

function Person(name) {
  var name = "blob";
  this.getName = function() {
    return name;
  };
  this.setName = function(n) {
    name = n;
  };
}
複製代碼

根據構造函數的特性,每次執行構造函數返回的實例對象都具備獨立的私有變量與特權方法,多個實例對象同時對 name 進行設置與獲取,都是獨立的操做。

對象的共享私有變量

「對象的共享私有變量」 這種模式下對象的私有成員其實是一個當即執行函數的私有變量,而後特權方法則定義在構造函數的原型上,這樣全部該構造函數的實例對象均可以操做讀取這個公共的私有成員屬性。

(function() {
  var name = "";
  Person = function(n) {
    name = n;
  };
  Person.prototype.getName = function() {
    return name;
  };
  Person.prototype.setName = function(n) {
    name = n;
  };
})();
複製代碼

注意:只要沒有使用關鍵字聲明的變量都是全局變量。

在這種模式下,咱們 setNamegetName 實際上都是對同一個私有變量進行操做。

小結

「對象的靜態私有變量」 與 「對象的共享私有變量」的優勢 相比普通離散的方式,「對象的靜態私有變量」 與 「對象的共享私有變量」更具備可封裝性,同時還能夠定義特定引用類型且具備私有成員變量的對象。

「對象私有成員變量」的做用? 例如,咱們須要對象的某個成員屬性做爲緩存空間,來保存特權方法處理的結果,可是又不想讓這個成員屬性直接暴漏出來,能夠被任何的方法進行修改讀取等。

思考 我的認爲使用對象的擴展性、密封性、凍結性等徹底能夠做爲「對象私有成員變量」的替代方案。

模塊模式

「模塊模式」 中的模塊實際上就是指函數做用域的封閉性與獨立性。 「模塊模式」 實際上就是對閉包的一種運用,它定義了一個封閉的空間,而後暴漏出一個接口,只有經過這個接口,才能訪問這個模塊的內部的私有變量和標識符。

模塊模式的做用

上面都是對「模塊模式」的定義以及模塊模式的性質進行說明,而實際運用上,模塊模式的最大價值在於保持代碼塊之間相互獨立,清晰地分離和組織項目中的代碼單元,而這一點的具體體現就是知名的前端打包工具 webpack ,它就是利用了 JS 函數具備模塊的封閉性來模擬其它面嚮對象語言中的 Package 概念,除此以外,AMD 模式、CommonJS 模塊、IEFF 模塊等都是模塊模式的典型運用。

基本模塊模式

就是具備獨立做用域的普通聲明函數和函數表達式。

function applaction() {
    return {}
}

var applaction = function(){ return {}}
複製代碼

經典模塊模式

var applaction = function (component) {
    var components = [];
    if (typeof components === 'object') {
        components.push(component)
    }
    return {
        getComponentCount: function () {
            return components.length;
        },
        registerComponent: function (component) {
            if (typeof components === 'object') {
                components.push(component)
            }
        }
    }
}
複製代碼

實際到了這裏咱們「面向對象程序設計 - 建立對象」中所說的「穩妥構造函數模式」(我我的喜歡稱呼的簡單的安全工廠模式)就是對「模塊模式」結合對象的建立返回。

當即執行的模塊模式(IEFF)

var applaction = (function() {
  return {};
})();
複製代碼

可擴展的模塊模式

(function($) {
  // 獲取 jQuery
})(jQuery);
複製代碼

高級的可擴展模塊模式

var applaction = (function(module) {
  //add method or add property
  return module;
})(module || {});
複製代碼

這種方式的優勢在於能夠進行異步腳本的執行,好比這個 module 對象尚未獲取到的狀況。

Sub-modules

能夠基於 Module 創建 Sub Module

applaction.sub = (function() {
  return {};
})();
複製代碼

PS: 最後自薦下個人 《JavaScript 高級程序設計》讀書筆記

相關文章
相關標籤/搜索