你不知道的JS之-做用域和閉包

做用域是什麼

理解做用域

  • 引擎
    • 從頭至尾負責整個JavaScript程序的編譯和執行過程
  • 編譯器
    • 負責語法分析及代碼生成
  • 做用域
    • 負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符有訪問權限。

做用域嵌套

當一個塊或者函數嵌套在另外一個函數或函數中時,就發生了做用域嵌套。瀏覽器

遍歷嵌套做用域規則:引擎從當前的執行做用域開始查找變量,若是找不到,就向上一級繼續查找。直到抵達最外層的全局做用域, 不管找到仍是沒找到,查找過程都會中止。bash

小結

做用域是一套規則,用於肯定在何處以及如何查找變量(標誌符)。 若是查找目的是對變量進行賦值,就是執行LHS查詢 若是查找目的是獲取變量的值,就是執行RHS查詢閉包

詞法做用域

做用域主要兩種工做模式:詞法做用域和動態做用域app

詞法階段

  • 大部分標準語言編譯器的第一個工做階段叫作詞法化(也叫單詞化)。
  • 簡單的說, 詞法做用域就是定義在詞法階段的做用域。換句話說,詞法做用域是由你在寫代碼的時候將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變。
  • 做用域查找會在找到第一個匹配的標識符時中止。在多層的嵌套做用域中能夠定義同名的標識符,叫作「遮蔽效應」
  • 做用域查找始終是從運行時所處的最內部做用域開始,逐級向外或者向上查找, 知道碰見第一個匹配的標識符爲止。
  • 全局變量會自動成爲全局對象(例如瀏覽器中的window對象)的屬性,所以能夠不直接經過全局對象的詞法名稱, 而是間接的經過對全局對象屬性的引用來對其進行訪問。 例如window.a。經過這種技術能夠訪問那些被同名變量鎖遮蔽的全局變量。但非全局變量若是被遮蔽了,不管如何都沒法被訪問到。
  • 不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處的位置決定。

小結

詞法做用域意味着做用域是由代碼書寫時候函數聲明的位置來決定的。函數

函數做用域和塊做用域

函數中的做用域

函數做用域是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用以及複用(事實上在嵌套的做用域中也可使用)。ui

隱藏內部實現

不該該這樣:spa

function doSomething(a) {
 b = a + doSomethingElse(a * 2);

 console.log(b * 3);
}

function doSomethingElse(a) {
 return a - 1;
}

var b;

doSomething(2);
複製代碼

而是應該這樣, 隱藏變量:調試

function doSomething(a) {
 function doSomethingElse(a) {
   return a - 1;
 }
 var b;

 b = a + doSomethingElse(a * 2);

 console.log(b * 3);
}

doSomething(2);
複製代碼

規避衝突

「隱藏」做用域中的變量和函數所帶來的另外一個好處,是能夠避免同名標識符之間的衝突,兩個標識符可能具備相同的名字可是用途卻不同,無心間可能形成命名衝突。 衝突會致使變量的值被意外覆蓋。code

函數做用域

匿名和具名

例如以下函數:cdn

setTimeout(function() {
 console.log('I waited 1 second');
 
}, 1000);
複製代碼

這叫作匿名函數表達式。 匿名函數表達式書寫起來簡單快捷,可是有幾個缺點:

  1. 匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難
  2. 若是沒有函數名,當函數須要引用自身時只能使用已通過期的arguments.callee引用。
  3. 匿名函數省略了對於代碼可讀性/可理解性很重要的函數名。

行內函數表達式很是強大且有用----匿名和具名之間的區別並不會對這一點有任何影響。給函數表達式指定一個函數名能夠有效解決以上問題。因此,最好始終給函數表達式命名。

setTimeout(function timeoutHandler() { // 有名字了
  console.log('I waited 1 second');
  
}, 1000);
複製代碼

當即執行函數表達式

(function(){})()(function(){}())

提高

  • 函數會首先別提高,而後纔是變量。
  • 出如今後面的函數聲明仍是能夠覆蓋前面的。
  • 一個普通塊內部的函數聲明一般會被提高到所在做用域的頂部。

總結

  • 全部的聲明(變量和函數)都會被「移動」到各自做用域的最頂端, 這個過程被稱爲 提高。
  • 聲明自己會被提高,而包含函數表達式的賦值在內的賦值操做並不會被提高。
  • 要注意避免重複聲明,特別是當普通的var聲明和函數聲明混合在一塊兒的時候, 不然會引發不少危險的問題。

做用域閉包

定義

當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在所在詞法做用域之外被執行,這個引用,就叫作閉包。

  • 不管經過何種手段將內部函數傳遞到所在詞法做用域之外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包
  • 本質上講,不管什麼時候何地,若是將函數看成第一級的值類型並處處傳遞,你就會看到閉包在這些函數中的應用。
  • 例如在一些定時器、事件監聽器、Ajax請求等,只要使用了回調函數,實際上就是在使用閉包

循環和閉包

  • let聲明能夠用來劫持塊做用域,而且在這個做用域中聲明一個變量。
  • for循環頭部的let聲明還會有一個特殊的行爲。這個行爲指出變量在循環過程當中不止被聲明一次,每次迭代都會聲明。隨後每一個迭代都會使用上一個迭代結束時的值來初始化這個變量。

模塊

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

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

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

現代的模塊機制

大多數模塊依賴加載器/管理器本質上都是將這種模塊定義封裝進一個友好的API。

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
  };
})();

MyModules.define('bar', [], function() {
  function hello(who) {
    return 'let me introduce: ' + who;
  }

  return {
    hello: hello
  };
});

MyModules.define('foo', ['bar'], function(bar) {
  var hungry = 'xiaofan';

  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('xiaofan'));

foo.awesome();
複製代碼

foobar模塊都是經過一個返回公共API的函數來定義的。foo甚至接受bar的實例做爲依賴參數,並能響相應的使用它。

總結

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

模塊有兩個主要特徵:

  1. 爲建立內部做用域而調用了一個包裝函數
  2. 包裝函數的返回值必須包含至少一個對內部函數的引用,這樣就會建立涵蓋整個包裝函數內部做用域的閉包
相關文章
相關標籤/搜索