## 1.5 作用域閉包
當函數可以**記住并訪問**所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行。
~~~
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2
~~~
無論使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時都可以觀察到閉包。
~~~
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 這就是閉包!
}
~~~
把內部函數baz 傳遞給bar,當調用這個內部函數時(現在叫作fn),它涵蓋的foo() 內部作用域的閉包就可以觀察到了,因為它能夠訪問a。
傳遞函數當然也可以是間接的。
~~~
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 將baz 分配給全局變量
}
function bar() {
fn(); //這就是閉包!
}
foo();
bar(); // 2
~~~
無論通過何種手段將內部函數傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執行這個函數都會使用閉包。
### 1.5.1 深入理解
~~~
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 或者任何其他的異步(或者同步)任務中,只要使用了回調函數,實際上就是在使用閉包!**
通常認為IIFE 是典型的閉包例子,但根據先前對閉包的定義,這不是嚴格意義上的閉包。
~~~
var a = 2;
(function IIFE() {
console.log( a );
})();
~~~
雖然這段代碼可以正常工作,但嚴格來講它并不是閉包。為什么?因為函數(示例代碼中的IIFE)并不是在它本身的詞法作用域以外執行的。它在定義時所在的作用域中執行(而外部作用域,也就是全局作用域也持有a)。a 是通過普通的詞法作用域查找而非閉包被發現的。
### 1.5.2 循環與閉包
要說明閉包,for 循環是最常見的例子。
~~~
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
~~~
正常情況下,我們對這段代碼行為的預期是分別輸出數字1~5,每秒一次,每次一個。但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次6。
改進:
~~~
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
~~~
IIFE 也不過就是函數,因此我們可以將i 傳遞進去,如果愿意的話可以將變量名定為j,當然也可以還叫作i。無論如何這段代碼現在可以工作了。
在迭代內使用IIFE 會為每個迭代都生成一個新的作用域,使得延遲函數的回調可以將新的作用域封閉在每個迭代內部,每個迭代中都會含有一個具有正確值的變量供我們訪問。
#### 重返塊作用域
1.3節中說過,let 聲明,可以用來劫持塊作用域,并且在這個塊作用域中聲明一個變量。
本質上這是將一個塊轉換成一個可以被關閉的作用域。
~~~
for (var i=1; i<=5; i++) {
let j = i; // 是的,閉包的塊作用域!
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}
~~~
上面代碼還不完美,進一步改進
~~~
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
~~~
for 循環頭部的let 聲明還會有一個特殊的行為。這個行為指出變量在循環過程中不止被聲明一次,每次迭代都會聲明。隨
后的每個迭代都會使用上一個迭代結束時的值來初始化這個變量。
### 1.5.3 模塊
考慮以下代碼:
~~~
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 中被稱為模塊。最常見的實現模塊模式的方法通常被稱為模塊暴露,這里展示的是其變體。
**模塊模式需要具備兩個必要條件:**
* 必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會創建一個新的模塊實例)。
* 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態。
一個具有函數屬性的對象本身并不是真正的模塊。一個從函數調用所返回的,只有數據屬性而沒有閉包函數的對象也并不是真正的模塊。
模塊也是普通的函數,因此可以接受參數:
~~~
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 module");
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
~~~
通過在模塊實例的內部保留對公共API 對象的內部引用,可以從內部對模塊實例進行修改,包括添加或刪除方法和屬性,以及修改它們的值。
**1. 現代的模塊機制**
大多數模塊依賴加載器/ 管理器本質上都是將這種模塊定義封裝進一個友好的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.awesome(); // LET ME INTRODUCE: HIPPO
~~~
"foo" 和"bar" 模塊都是通過一個返回公共API 的函數來定義的。"foo" 甚至接受"bar" 的示例作為依賴參數,并能相應地使用它。
**2. 未來的模塊機制**
ES6 中為模塊增加了一級語法支持。但通過模塊系統進行加載時,ES6 會將文件當作獨立的模塊來處理。每個模塊都可以導入其他模塊或特定的API 成員,同樣也可以導出自己的API 成員。
基于函數的模塊并不是一個能被穩定識別的模式(編譯器無法識別),它們的API 語義只有在運行時才會被考慮進來。因此可以在運行時修改一個模塊的API(參考前面關于公共API 的討論)。
相比之下,ES6 模塊API 更加穩定(API 不會在運行時改變)。由于編輯器知道這一點,因此可以在(的確也這樣做了)編譯期檢查對導入模塊的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")
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
~~~
需要用前面兩個代碼片段中的內容分別創建文件foo.js 和bar.js。然后如第三個代碼片段中展示的那樣,bar.js 中的程序會加載或導入這兩個模塊并使用它們。
* `import `可以將一個模塊中的一個或多個API 導入到當前作用域中,并分別綁定在一個變量上(例子里是hello)。
* `module `會將整個模塊的API 導入并綁定到一個變量上(在我們的例子里是foo 和bar)。
* `export `會將當前模塊的一個標識符(變量、函數)導出為公共API。這些操作可以在模塊定義中根據需要使用任意多次。
模塊文件中的內容會被當作好像包含在作用域閉包中一樣來處理,就和前面的函數閉包模塊一樣。
- 前言
- 第一章 JavaScript簡介
- 第三章 基本概念
- 3.1-3.3 語法、關鍵字和變量
- 3.4 數據類型
- 3.5-3.6 操作符、流控制語句(暫略)
- 3.7函數
- 第四章 變量的值、作用域與內存問題
- 第五章 引用類型
- 5.1 Object類型
- 5.2 Array類型
- 5.3 Date類型
- 5.4 基本包裝類型
- 5.5 單體內置對象
- 第六章 面向對象的程序設計
- 6.1 理解對象
- 6.2 創建對象
- 6.3 繼承
- 第七章 函數
- 7.1 函數概述
- 7.2 閉包
- 7.3 私有變量
- 第八章 BOM
- 8.1 window對象
- 8.2 location對象
- 8.3 navigator、screen與history對象
- 第九章 DOM
- 9.1 節點層次
- 9.2 DOM操作技術
- 9.3 DOM擴展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件處理程序
- 10.3 事件對象
- 10.4 事件類型
- 第十一章 JSON
- 11.1-11.2 語法與序列化選項
- 第十二章 正則表達式
- 12.1 創建正則表達式
- 12.2-12.3 模式匹配與RegExp對象
- 第十三章 Ajax
- 13.1 XMLHttpRequest對象
- 你不知道的JavaScript
- 一、作用域與閉包
- 1.1 作用域
- 1.2 詞法作用域
- 1.3 函數作用域與塊作用域
- 1.4 提升
- 1.5 作用域閉包
- 二、this與對象原型
- 2.1 關于this
- 2.2 全面解析this
- 2.3 對象
- 2.4 混合對象“類”
- 2.5 原型
- 2.6 行為委托
- 三、類型與語法
- 3.1 類型
- 3.2 值
- 3.3 原生函數