重讀你不知道的JS (上) 第一節五章

你不知道的JS(上卷)筆記

你不知道的 JavaScript閉包

JavaScript 既是一門充滿吸引力、簡單易用的語言,又是一門具備許多複雜微妙技術的語言,即便是經驗豐富的 JavaScript 開發者,若是沒有認真學習的話也沒法真正理解它們.app

上捲包括倆節:異步

  • 做用域和閉包
  • this 和對象原型

做用域和閉包

但願 Kyle 對 JavaScript 工做原理每個細節的批判性思 考會滲透到你的思考過程和平常工做中。知其然,也要知其因此然。編輯器

做用域閉包

啓示

祕訣: JavaScript中閉包無處不在,你只須要可以識別並擁抱它。函數

閉包是基於詞法做用域書寫代碼時所產生的天然結果,你甚至不須要爲了利用它們而有意識的建立閉包。工具

閉包的建立和使用在你的代碼中隨處可見。
你缺乏的是根據你本身的意願來識別、擁抱和影響閉包的思惟環境。學習

實質問題

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。this

詞法做用域的查找規則是閉包的一部分。code

function foo() {
  var a = 2;
  function bar() {
    console.log( a ); // 2
  }
  bar();
}
foo();

純學術的角度上說,上述代碼片斷中,函數 bar() 具備一個涵蓋 foo() 做用域的閉包 (事實上,涵蓋了它能訪問的全部做用域,好比全局做用域)。也能夠認爲 bar() 被封閉在了 foo() 的做用域中。爲何呢?緣由簡單明瞭,由於 bar() 嵌套在 foo() 內部。對象

閉包使得函數能夠繼續訪問定義時的詞法做用域。

不管經過何種手段將內部函數傳遞到所在的詞法做用域之外,它都會持有對原始定義做用 域的引用,不管在何處執行這個函數都會使用閉包。

思考:

function foo() {
  var a = 2;
  function baz() {
    console.log( a, b ); // 2 , b能獲取到1嗎?
  }
  bar( baz );
}
function bar(fn) {
  var b = 1;
  fn(); // 媽媽快看呀,這就是閉包!
}

常見的閉包場景

function wait(message) {
  setTimeout( function timer() { // timer函數由引擎調用,可是已經超出了wait做用域,因此存在閉包
      console.log( message );
  }, 1000 );
}
wait( "Hello, closure!" );

在定時器、事件監聽器、 Ajax 請求、跨窗口通訊、Web Workers 或者任何其餘的異步(或者同步)任務中,只要使 用了回調函數,實際上就是在使用閉包!

IIFE閉包
var a = 2;
(function IIFE() {
  console.log( a );
})();

儘管 IIFE 自己並非觀察閉包的恰當例子,但它的確建立了閉包,而且也是最經常使用來建立 能夠被封閉起來的閉包的工具。所以 IIFE 的確同閉包息息相關,即便自己並不會真的使用 閉包。

循環和閉包
for (var i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

延遲函數的回調會在循環結束時才執行。事實上, 當定時器運行時即便每一個迭代中執行的是setTimeout(.., 0),全部的回調函數依然是在循 環結束後纔會被執行,所以會每次輸出一個 6 出來。

缺陷是咱們試圖假設循環中的每一個迭代在運行時都會給本身「捕獲」一個 i 的副本。可是 根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的, 可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個 i。
這樣說的話,固然全部函數共享一個 i 的引用。

它須要有本身的變量,用來在每一個迭代中儲存 i 的值:

for (var i=1; i<=5; i++) { 
  (function() { // IIFE 每次執行都會當即建立一個詞法上的函數做用域
    var j = i; // 閉包做用域的變量j, 當即獲得i的值 
    setTimeout( function timer() {
                 console.log( j ); // 訪問閉包做用域的變量j
             }, j*1000 );
  })();
}

變體:

for (var i=1; i<=5; i++) {
  (function(j) { // IIFE 每次執行都會當即建立一個詞法上的函數做用域
    // 閉包做用域的變量j, 參數傳遞當即獲得i的值
    setTimeout( function timer() {
                 console.log( j ); // 訪問閉包做用域的變量j
             }, j*1000 );
  })(i);
}
塊做用域和閉包

let 聲明,能夠用來劫 持塊做用域,而且在這個塊做用域中聲明一個變量。

而上面的IIFE建立一個閉包,本質上這是將一個塊轉換成一個能夠被關閉的做用域。

結合塊級做用域與閉包:

for (let i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

模塊

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
  function doSomething() { 
    console.log( something );
  }
  function doAnother() {
    console.log( another.join( " ! " ) );
  }
  return {
    doSomething1: doSomething,
    doAnother: doAnother
  };
}
var foo = CoolModule(); 
foo.doSomething1(); // cool
foo.doAnother(); // 1 ! 2 ! 3

這個模式在 JavaScript 中被稱爲模塊。最多見的實現模塊模式的方法一般被稱爲模塊暴露, 這裏展現的是其變體。
doSomething() 和 doAnother() 函數具備涵蓋模塊實例內部做用域的閉包(經過調用 CoolModule() 實現)。
當經過返回一個含有屬性引用的對象的方式來將函數傳遞到詞法做 用域外部時,咱們已經創造了能夠觀察和實踐閉包的條件。

模塊模式須要具有兩個必要條件。

  1. 必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會建立一個新的模塊 實例)。
  2. 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,並 且能夠訪問或者修改私有的狀態。

一個具備函數屬性的對象自己並非真正的模塊。從方便觀察的角度看,一個從函數調用 所返回的,只有數據屬性而沒有閉包函數的對象並非真正的模塊。

模塊模式另外一個簡單但強大的變化用法是,命名將要做爲公共 API 返回的對象:
在上述模塊中,dosomething1被做爲模塊內部dosomething的公開訪問名。

現代的模塊機制
var MyModules = (function Manager() { // 模塊 管理器/依賴加載器
  var modules = {};
  function define(name, deps, impl) {
    for (var i=0; i<deps.length; i++) {
      deps[i] = modules[deps[i]];
    }
    modules[name] = impl.apply( impl, deps );
  }
  function get(name) { 
    return modules[name];
  }
  return {
    define: define,
    get: get 
  };
})();


// module bar 定義
MyModules.define( "bar", [], function() { 
  function hello(who) {
    return "Let me introduce: " + who; 
  }
  return {
    hello: hello
  };
});

// module foo 定義 依賴 bar 模塊
MyModules.define( "foo", ["bar"], function(bar) {
  var hungry = "hippo";
  function awesome() {
    console.log( bar.hello( hungry ).toUpperCase() );
  }
  return {
    awesome: awesome
  };
});
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(bar.hello( "hippo" )); // Let me introduce: hippo 
foo.awesome(); // LET ME INTRODUCE: HIPPO

將來的模塊機制

基於函數的模塊並非一個能被穩定識別的模式(編譯器沒法識別),它們 的 API 語義只有在運行時纔會被考慮進來。所以能夠在運行時修改一個模塊 的 API。
相比之下,ES6 模塊 API 更加穩定(API 不會在運行時改變)。因爲編輯器知 道這一點,所以能夠在(的確也這樣作了)編譯期檢查對導入模塊的 API 成 員的引用是否真實存在。若是 API 引用並不存在,編譯器會在運行時拋出一 個或多個「早期」錯誤,而不會像往常同樣在運行期採用動態的解決方案。

ES6 的模塊沒有「行內」格式,必須被定義在獨立的文件中(一個文件一個模塊)。瀏覽 器或引擎有一個默認的「模塊加載器」(能夠被重載,但這遠超出了咱們的討論範圍)可 以在導入模塊時異步地加載模塊文件。

小結

閉包就好像從 JavaScript 中分離出來的一個充滿神祕色彩的未開化世界,只有最勇敢的人 纔可以到達那裏。但實際上它只是一個標準,顯然就是關於如何在函數做爲值按需傳遞的 詞法環境中書寫代碼的。
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時 就產生了閉包。
若是沒能認出閉包,也不瞭解它的工做原理,在使用它的過程當中就很容易犯錯,好比在循 環中。但同時閉包也是一個很是強大的工具,能夠用多種形式來實現模塊等模式。
模塊有兩個主要特徵:

(1)爲建立內部做用域而調用了一個包裝函數;
(2)包裝函數的返回 值必須至少包括一個對內部函數的引用,這樣就會建立涵蓋整個包裝函數內部做用域的閉 包。

如今咱們會發現代碼中處處都有閉包存在,而且咱們可以識別閉包而後用它來作一些有用 的事!

相關文章
相關標籤/搜索