當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。閉包
function foo(){
var a = 2;
function bar(){
console.log(a); // 2
}
bar()
}
foo()
複製代碼
根據前面的定義,嚴格來講上述代碼並非閉包,最準確地用來解釋bar()對a的引用的方法是詞法做用域的查找規則,而這些規則只是閉包的一部分。app
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 ---- 這就是閉包的效果
複製代碼
上述代碼中,在foo()執行後,其返回值(也就是內部的bar()函數)賦值給變量baz並調用baz(),實際上只是經過不一樣的標識符引用調用了內部的函數bar()。異步
bar()顯然能夠被正常執行。可是在這個例子中,它在本身定義的詞法做用域之外的地方執行。ide
在foo()執行後,一般會期待foo()的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器來釋放再也不使用的內存空間。而閉包的「神奇」之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收,由於bar()自己在使用。函數
由於bar()所聲明的位置,它擁有涵蓋foo()內部做用域的閉包。使得該做用域一直存活,以供bar()在以後任什麼時候間進行引用。工具
bar()依然持有對該做用域的引用,而這個引用就叫做閉包。ui
閉包使得函數能夠繼續訪問定義時的詞法做用域。固然,不管使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時均可以觀察到閉包。spa
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
fn(); // 這就是閉包
}
複製代碼
把內部函數baz傳遞給bar,當調用這個內部函數時(如今叫做fn),它涵蓋的foo()內部做用域的閉包就能夠觀察到了,由於它可以訪問a。code
傳遞函數固然也能夠是間接的。對象
var fn ;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
fn = baz; // 將baz分配給全局變量
}
function bar(){
fn(); // 這就是閉包
}
複製代碼
不管經過何種手段將內部函數傳遞到所在的詞法做用域之外,它都會持有對原始定義做用域的引用,不管在何處執行這個函數都會使用閉包。
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000)
}
wait("Hello,closure!")
複製代碼
將一個內部函數(名爲timer)傳遞給setTimeout(..)。timer具備涵蓋wait(..)做用域的閉包,所以還保有對變量message的引用。
wait(..)執行1000毫秒後,它的內部做用域並不會消失,timer函數依然保有wait(..)做用域的閉包。
本質上,不管什麼時候何地,若是將(訪問它們各自詞法做用域的)函數看成第一級的值類型並處處傳遞,你就會看到閉包在這些函數中的做用。在定時器、事件監聽器、Ajax請求、跨窗口通訊、Web Workers或者任何其餘的異步(或者同步)任務中,只要使用了回調函數,實際上就是在使用閉包。
for(var i = 1;i <= 5; i++){
setTimeout(function timer(){
console.log(i); // 每秒一次的頻率輸出5次6
},i*1000)
}
複製代碼
根據做用域的工做原理,實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個i。
for(var i = 1; i <= 5; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},j*1000);
})(i)
}
複製代碼
在迭代內使用IIFE會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
},i*1000)
}
複製代碼
function foo(){
var something = "cool";
var another = [1,2,3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join(" ! ")
}
}
複製代碼
私有數據變量something和another,以及doSomething()和doAnother()兩個內部函數,它們的詞法做用域(而這就是閉包)也就是foo()的內部做用域。
function CoolModule(){
var something = "cool";
var another = [1,2,3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join("!");
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
複製代碼
這個模式在JavaScript中被稱爲模塊。最多見的實現模塊的方法一般被稱爲模塊暴露,這裏展現的是其變體。
首先,CoolModule()只是一個函數,必需要經過調用它來建立一個模塊實例。若是不執行外部函數,內部做用域和閉包都沒法被建立。
其次,CoolModule()返回一個用對象字面量語法 {key: value,...}來表示的對象。這個返回的對象中含有對內部函數而不是內部數據變量的引用。咱們保持內部數據變量是隱藏且私有的狀態。能夠將這個對象類型的返回值看做本質上是模塊的公共API。
模塊模式須要具有兩個必要條件。
1.必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會建立一個新的模塊實例)。
2.封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態。
一個從函數調用所返回的,只有數據屬性而沒有閉包函數的對象並非真正的模塊。
var foo = (function CoolModule(){
var something = "cool";
var another = [1,2,3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join("!");
}
return {
doSomething: doSomething,
doAnother: doAnother
}
})()
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
複製代碼
將模塊函數轉換成了IIFE,當即調用這個函數並將返回值直接賦值給單例的模塊實例標識符foo。
模塊也是普通的函數,所以能夠接收參數:
function CoolModule(id){
function identify(){
console.log(id);
}
return {
identify: identify
}
}
var foo1 = CoolModule("foo 1");
var foo2 = CoolModule("foo 2");
foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"
複製代碼
模塊模式的一個簡單但強大的用法是命名將要做爲公共API返回的對象:
var foo = (function CoolModule(id){
function change(){
// 修改公共API
publicAPI.identify = identify2;
}
function identify1(){
console.log(id);
}
function identify2(){
console.log(id.toUpperCase());
}
var publicAPI = {
change: change,
identify: identify1
}
return publicAPI;
}("foo mocule");
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
複製代碼
經過在模塊實例的內部保留對公共API對象的內部引用,能夠從內部對模塊實例進行修改,包括添加或刪除方法和屬性,以及修改它們的值。
大多數模塊依賴加載器/管理器本質上都是將這種模塊定義封裝進一個友好的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
}
})()
複製代碼
這段代碼的核心是modules[name]=impl.apply(impl,deps)。爲了模塊的定義引入了包裝函數(能夠傳入任何依賴),而且將返回值,也就是模塊的API,儲存在一個根據名字來管理的模塊列表中。
下面展現瞭如何使用上面的代碼來定義模塊:
MyModules.define("bar",[],function(){
function hello(who){
return "Let me introduce: "+ who
}
return {
hello: hello
}
})
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.awefome(); // LET ME INTRODUCE: HIPPO
複製代碼
"foo"和"bar"模塊都是經過一個返回公共API的函數來定義的。"foo"甚至接受"bar"的實例做爲依賴參數,並能相應地使用它。
模塊就是模塊,即便在它們外層加上一個友好的包裝工具也不會發生任何變化。
ES6中爲模塊增長了一級語法支持。在經過模塊系統進行加載時,ES6會將文件當作獨立的模塊來處理。每一個模塊均可以導入其餘模塊或特定的API成員,一樣也能夠導出本身的API成員。
基於函數的模塊並非一個能被靜態識別的模式,所以能夠在運行時修改一個模塊的API。 相比之下,ES6模塊API是靜態的,所以在編譯期就會檢查導入模塊的API成員的引用是否存在,若是不存在,編譯器會在編譯時就報錯,而不會等到運行期動態解析(而且報錯)。
ES6的模塊沒有「行內」格式,必須被定義在獨立的文件中。
// bar.js
function hello(who){
return "Let me introduce: " + who;
}
export hello;
// foo.js
// 僅從"bar"模塊導入hello()
import hello from "bar";
var hungry = "hippo";
function awesome(){
console.log(hello(hungry).toUpperCase());
}
export awesome;
// baz.js
// 導入完整的"foo"和"bar"模塊
module foo from "foo";
module bar from "bar";
console.log(bar.hello("rhino"));
foo.awesome();
複製代碼
import能夠將一個模塊中的一個或多個API導入到當前做用域中,並分別綁定在一個變量上。module會將整個模塊的API導入並綁定到一個變量上。export會將當前模塊的一個標識符(變量、函數)導出爲公共API。這些操做能夠在模塊定義中根據須要使用任意屢次。
咱們在詞法做用域的環境下寫代碼,而其中的函數也是值,能夠隨意傳來傳去。
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時就產生了閉包。
閉包是一個很是強大的工具,能夠用多種形式來實現模塊等模式。
模塊有兩個主要特徵:
關於腦子一熱
個人經歷告訴我,腦子一熱作的事情,多半會後悔,並且會很是後悔。可是怎麼去避免呢,方法我還沒找到,每次我遇到這樣的情緒,都會找各類理由去逃避,這是目前個人低級應對措施,至關低級。若是能從根源消除是最好不過的了,但是我尚未那麼大的控制力,因此只能慢慢去培養,儘可能減小這種上頭的次數了。