你不知道的js——作用域 1 作用域 1.1編譯原理 在傳統編譯語言的流程中,程式中的一段源代碼在執行之前會經歷三個步驟,統稱為“編 譯” 1分詞/詞法分析(Tokenizing/Lexing) 會將由字元組成的字元串分解成(對編程語言來說)有意義的代碼塊,這些代 碼塊被稱為詞法單元(token) ...
你不知道的js——作用域
1 作用域
1.1編譯原理
在傳統編譯語言的流程中,程式中的一段源代碼在執行之前會經歷三個步驟,統稱為“編 譯”
- 1分詞/詞法分析(Tokenizing/Lexing)
會將由字元組成的字元串分解成(對編程語言來說)有意義的代碼塊,這些代 碼塊被稱為詞法單元(token)
var a = 2; 分解成 為下麵這些詞法單元:var、a、=、2 、; - 2解析/語法分析(Parsing)
是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的代表了程式語法 結構的樹——“抽象語法樹”(Abstract Syntax Tree,AST)
var a = 2; 的抽象語法樹中可能會有一個叫作 VariableDeclaration (變數聲明)的頂級節點,接下 來是一個叫作 Identifier(識別碼)(它的值是 a)的子節點,以及一個叫作 AssignmentExpression 的子節點。AssignmentExpression (作業表達)節點有一個叫作 NumericLiteral(數字文字)(它的值是 2)的子 節點。 - 3代碼生成
將 AST 轉換為可執行代碼的過程稱被稱為代碼生成。
將 var a = 2; 的 AST 轉化為一組機器指 令,用來創建一個叫作 a 的變數(包括分配記憶體等),並將一個值儲存在 a 中。 - JavaScript 引擎不會有大量的(像其他語言編譯器那麼多的)時間用來進行優化,因 為與其他語言不同,JavaScript 的編譯過程不是發生在構建之前的。大部分情況下編譯發生在代碼執行前的幾微秒(甚至更短!)的時 間內。
1.2理解作用域
- 引擎:從頭到尾負責整個 JavaScript 程式的編譯及執行過程。
- 編譯器:負責語法分析及代碼生成等臟活累活
- 作用域:責收集並維護由所有聲明的標識符(變數)組成的一系列查 詢,並實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問許可權。
- var a = 2; ,引擎認為這裡有兩個完全不同的聲明,一個由編譯器在編譯時處理,另一個則由引擎在運行時處理。
- 總結:變數的賦值操作會執行兩個動作,首先編譯器會在當前作用域中聲明一個變數(如果之前沒有聲明過),然後在運行時引擎會在作用域中查找該變數,如果能夠找到就會對 它賦值。
- LHS 查詢:變數出現在賦值操作左側時進行,試圖找到變數的容器本身。
a = 2; - RHS查詢:(非LHS) retrieve his source value(取到它的源值),變數出現在賦值操作右側時進行,查找變數的值。
console.log( a );
LHS 和 RHS 引用都會在當前樓層進行查找,如果沒有找到,就會坐電梯前往上一層樓
不成功的 RHS 引用會導致拋出 ReferenceError 異常。不成功的 LHS 引用會導致自動隱式 地創建一個全局變數(非嚴格模式下),該變數使用 LHS 引用的目標作為標識符,或者拋 出 ReferenceError 異常(嚴格模式下)。
1.3 作用域嵌套
當前作用域無法找到某個變數時,引擎會在外層嵌套的作用域中查找,直至抵達最外層(全局)時仍未找到,拋出錯誤。
1.4 異常
變數無法找到時,拋出ReferenceError(參考錯誤),與作用域判別失敗相關。 對變數的值進行不合理操作,拋出TypeError,與操作是否合法相關。
當引擎執行 LHS 查詢時,如果在頂層(全局作用域)中也無法找到目標變數, 全局作用域中就會創建一個具有該名稱的變數,並將其返還給引擎,前提是程式運行在非 “嚴格模式”下。
(嚴格模式禁止自動或隱式地創建全局變數。)
(如果 RHS 查詢找到了一個變數,但是你嘗試對這個變數的值進行不合理的操作, 比如試圖對一個非函數類型的值進行函數調用,或著引用 null 或 undefined 類型的值中的 屬性,那麼引擎會拋出另外一種類型的異常,叫作 TypeError。
ReferenceError 同作用域判別失敗相關,而 TypeError 則代表作用域判別成功了,但是對 結果的操作是非法或不合理的。)
2 詞法作用域
詞法分析階段 基本能夠知道全部標識符在哪裡以及是如何聲明的,從而能夠預測在執行過程中如何對它 們進行查找。
作用域共有兩種主要的工作模型。第一種是最為普遍的,被大多數編程語言所採用的詞法 作用域,我們會對這種作用域進行深入討論。另外一種叫作動態作用域,仍有一些編程語 言在使用(比如 Bash 腳本、Perl 中的一些模式等)。
2.1 詞法階段
詞法作用域即定義在詞法階段的作用域。
查找
作用域氣泡的結構和互相之間的位置關係給引擎提供了足夠的位置信息,引擎用這些信息 來查找標識符的位置。
作用域查找會在找到第一個匹配的標識符時停止。。在多層的嵌套作用域中可以定義同名的 標識符,這叫作“遮蔽效應”(內部的標識符“遮蔽”了外部的標識符)。
全局變數會自動成為全局對象(比如瀏覽器中的 window 對象)的屬性,因此 可以間接地通過對全局對象屬性的引 用來對其進行訪問。——→ window.a
通過這種技術可以訪問那些被同名變數所遮蔽的全局變數。但非全局的變數 如果被遮蔽了,無論如何都無法被訪問到。
2.2 欺騙詞法
在運行時來“修 改”(也可以說欺騙)詞法作用域,:欺騙詞法作用域會導致性能 下降(原因主要是無法對代碼的詞法進行靜態分析,預先確認變數和函數的位置,從而快速尋找。)
【另外一個不推薦使用 eval(..) 和 with 的原因是會被嚴格模式所影響(限 制)。with 被完全禁止,而在保留核心功能的前提下,間接或非安全地使用 eval(..) 也被禁止了。】
2.2.1 eval
接受一個字元串為參數,並將其中的內容視為好像在書 寫時就存在於程式中這個位置的代碼
eval(..) 通常被用來執行動態創建的代碼
eval(..) 調用中的 "var b = 3;" 這段代碼會被當作本來就在那裡一樣來處理。由於那段代 碼聲明瞭一個新的變數 b,因此它對已經存在的 foo(..) 的詞法作用域進行了修改。事實 上,和前面提到的原理一樣,這段代碼實際上在 foo(..) 內部創建了一個變數 b,並遮蔽 了外部(全局)作用域中的同名變數。
在嚴格模式的程式中,eval(..) 在運行時有其自己的詞法作用域,意味著其中的聲明無法修改所在的作用域。
與之相似的有setTimeout,setInterval,new Function(),都可以接收字元串。
2.2.2 with
with通常被當作重覆引用同一個對象中的多個屬性的快捷方式,可以不需要重覆引用對象本身。with會創建一個全新的詞法作用域。 在嚴格模式下,with被禁止。
例子:
function foo(obj) {
with (obj) {
a = 2;
} }
var o2 = {
b: 3
};
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
(但當我們將 o2 作為作用域時,其中並沒有 a 標識符, 因此進行了正常的 LHS 標識符查找;
o2 的作用域、foo(..) 的作用域和全局作用域中都沒有找到標識符 a,因此當 a=2 執行 時,自動創建了一個全局變數(因為是非嚴格模式)。)
3 函數作用域和塊作用域
3.1 函數中的作用域
函數作用域的含義是指,屬於這個函數的全部變數都可以在整個函數的範圍內使用及復 用(事實上在嵌套的作用域中也可以使用)。JS具有基於函數的作用域。內部能向外訪問,外部不能向內訪問。
3.2 隱藏內部實現
把變數和函數包裹在一個函數的作用域中,然後用這個作用域 來“隱藏”它們。
最小授權或最小暴露原則:在軟體設計中,應該最小限度地暴露必要的內容,而將其他內容都隱藏起來。 隱藏作用域能夠避免同名標識符之間的衝突。
規避衝突:“隱藏”作用域中的變數和函數所帶來的另一個好處,是可以避免同名標識符之間的衝突, 兩個標識符可能具有相同的名字但用途卻不一樣,無意間可能造成命名衝突。衝突會導致 變數的值被意外覆蓋。
3.2.1 全局命名空間
引入第三方庫時,沒有妥善地將內部私有的函數或變數隱藏起來,就會很容易引發衝突
3.2.2 模塊管理
另外一種避免衝突的辦法和現代的模塊機制很接近,就是從眾多模塊管理器中挑選一個來 使用。
無需將標識符加入到全局作用域中,而是通過依賴管理器 的機制將庫的標識符顯式地導入到另外一個特定的作用域中
3.3 函數作用域
如果function是聲明中第一個詞(前面沒有其他詞,甚至是括弧),那麼就是一個函數聲明,否則就是一個函數表達式。函數表達式可以是匿名的, 而函數聲明則不可以省略函數名
在任意代碼片段外部添加包裝函數,可以將內部的變數和函數定義“隱 藏”起來,外部作用域無法訪問包裝函數內部的任何內容。但是它並不理想,因為會導致一些額外的問題。首先, 必須聲明一個具名函數 foo(),意味著 foo 這個名稱本身“污染”了所在作用域(在這個 例子中是全局作用域)。
函數不需要函數名(或者至少函數名可以不污染所在作用域),並且能夠自動運行, 這將會更加理想。
☆☆☆☆☆包裝函數的聲明以 (function... 而不僅是以 function... 開始。,函數會被當作函數表達式而不是一 個標準的函數聲明來處理。
(function foo(){ .. }) 作為函數表達式意味著foo 被綁定在函數表達式自身的函數中,外部作用域則不行。foo 變數名被隱藏在自身中意味著不會非必要地污染外部作 用域。
3.3.1 匿名和具名
於函數表達式你最熟悉的場景可能就是回調參數
函數表達式可以匿名,函數聲明必須具名。 匿名函數也有缺點:
- 沒有函數名,棧追蹤調試困難。
- 遞歸自身時,只能使用arguments.callee。或者是事件解綁時候找不到。
- 沒有函數名,降低了可讀性。
選擇性地給函數表達式具名,可以解決以上問題。
行內函數表達式非常強大且有用——匿名和具名之間的區別並不會對這點有任何影響,始終給函數表達式命名是一個最佳實踐。
3.3.2 立即執行函數表達式IIFE(Immediately Invoked Function Expression)
用圓括弧()將函數包裹,然後緊跟圓括弧調用。 另一種可以將括弧放在內部。 (function ( ){ …… } ( )); UMD標準中,IIFE也被廣泛運用,比如:
var a = 2;
(function IIFE( def ) {
def( window )
})(function def ( global ) {
var a = 3;
console.log( a ); //3
console.log( global.a ); //2
});
//函數表達式 def 定義在片段的第二部分,然後當作參數(這個參數也叫作 def)被傳遞進 IIFE 函數定義的第一部分中。最後,參數 def(也就是傳遞進去的函數)被調用,並將 window 傳入當作 global 參數的值
3.4 塊作用域
在 for 迴圈的頭部直接定義了變數 i,通常是因為只想在 for 迴圈內部的上下文中使 用 i,而忽略了 i 會被綁定在外部作用域(函數或全局)中的事實。這就是塊作用域的用處。變數的聲明應該距離使用的地方越近越好,並最大限度地本地 化。
塊作用域是一個用來對之前的最小授權原則進行擴展的工具,將代碼從在函數中隱藏信息 擴展為在塊中隱藏信息。
(因多次多處訪問而提前做緩存除外) 塊級作用域在ES6中得到廣泛應用。在此之前需要註意,var聲明的變數並不屬於塊級作用域。 可以生成塊級作用域的有:
- with :用 with 從對象中創建出的作用域僅在 with 聲明中而非外 部作用域中有效
try/catch: catch 分句會創建一個塊作 用域,其中聲明的變數僅在 catch 內部有效
try {
undefined(); // 執行一個非法操作來強制製造一個異常
} catch (err) {
console.log( err ); // 能夠正常執行!
}
console.log( err ); // ReferenceError: err not found- let: 用 let 將變數附加在一個已經存在的塊作用域上的行為是隱式的。只要聲明是有效的,在聲明中的任意位置都可以使用 { .. } 括弧來為 let 創建一個用於綁 定的塊。
使用 let 進行的聲明不會在塊作用域中進行提升。聲明的代碼被運行之前,聲明並不 “存在”
{
console.log( bar ); // ReferenceError!
let bar = 2;
} const(ES6新特性),可以形成暫時性鎖區。同樣可以用來創建塊作用域變數,但其值是固定的 (常量)。之後任何試圖修改值的操作都會引起錯誤。
*塊級作用域的用處:**有利於垃圾收集。程式塊在執行後,其中的變數如果不被後續需要(閉包等),就可以將記憶體回收 . 減少變數空間污染。
1 垃圾收集
2 let迴圈
4 提升
4.1 先有雞還是先有蛋
function和var聲明,會被提升到頂部。 其他的操作不會提升,處於自身原本的位置。
可以看到,函數聲明會被提升,但是函數表達式卻不會被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
//這段程式中的變數標識符 foo() 被提升並分配給所在作用域(在這裡是全局作用域),因此 foo() 不會導致 ReferenceError。但是 foo 此時並沒有賦值(如果它是一個函數聲明而不 是函數表達式,那麼就會賦值)。foo() 由於對 undefined 值進行函數調用而導致非法操作, 因此拋出 TypeError 異常。
4.2 編譯器再度來襲
當你看到 var a = 2; 時,可能會認為這是一個聲明。但 JavaScript 實際上會將其看成兩個聲明:var a; 和 a = 2; 第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在 原地等待執行階段。
每個作用域都會進行提升操作,其頂部為其自身作用域的頂部(註意不會跨越作用域)。 函數表達式不會提升,而是處於正常的位置。 具名的函數表達式也不存在提升
foo(); // TypeError,因為此時foo被初始化為undefined,還沒有賦值為函數表達式
bar(); // ReferenceError,因為函數表達式不提升,此時bar還沒有聲明
var foo = function bar(){
// do sth
}
等效於
var foo;
foo(); // TypeError,因為此時foo被初始化為undefined,還沒有賦值為函數表達式
bar(); // ReferenceError,因為函數表達式不提升,此時bar還沒有聲明
foo = function (){
var bar = self;
// do sth
}
4.3 函數優先
函數首先被提升,然後是變數。函數與變數同名時,變數的聲明會被省略,但是依舊可以進行賦值操作。 後出現的函數聲明會覆蓋前面的同名函數,所以在同一作用域內,千萬不要聲明同名的函數 在普通塊內部的聲明,也會提升到作用域頂部,而不是在塊內。
//一個普通塊內部的函數聲明通常會被提升到所在作用域的頂部,這個過程不會像下麵的代 碼暗示的那樣可以被條件判斷所控制:
foo() // 'b'
var a = true;
if(a){
function foo(){ console.log('a') }
// 被提升到作用域頂部。先聲明,被後面覆蓋了
} else {
function foo(){ console.log('b') }
// 被提升到第二個。後聲明,覆蓋了前面
}
因此應該 儘可能避免在塊內部聲明函數。
5 作用域閉包
5.1 啟示
JS中閉包無處不在。 閉包是基於詞法作用域書寫代碼時產生的自然結果。 閉包特征就是將函數表達式,連帶其詞法作用域,進行傳值。
5.2 實質問題
一個閉包的例子:
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar; //也可以直接返回函數表達式,function(){ console.log(a); }
}
var baz = foo();
baz(); // 2,讀取到了foo中定義的變數a
本質上是,函數bar的詞法作用域能夠訪問foo的內部作用域,因為它的作用域嵌套在了foo的作用域內。 也就是說,最終return的結果是帶有scope(作用域)信息的,這個是能夠找到需要調用變數的根本依據。 當bar在使用時,閉包會阻止垃圾回收器回收foo的作用域(有好有壞)。
5.3 現在我懂了
引擎在調用函數的同時,其詞法作用域會保持完整。 如果將(訪問他們各自詞法作用域的)函數當作第一級的值類型併到處傳遞,你就會看到閉包在這些函數中的應用。 也就是說,只要使用了回調函數,實際上就是在使用閉包。(在定時器、事件監聽器、 Ajax 請求、跨視窗通信、Web Workers 或者任何其他的非同步(或者同步)任務中,只要使 用了回調函數,實際上就是在使用閉包!)
5.4 迴圈和閉包
非同步時候,函數進行回調時,調用變數值的是回調發生時候的值,所以要考慮運行時的狀態。
拓展解析:任務隊列、進階
for(var i=0; i<5; i++){
setTimeout( function(){
console.log(i); //輸出都是5
}, 1000*i);
}
解決方法有IIFE
for(var i=0; i<5; i++){
// 創建一個新的詞法作用域,把它的值在這個作用域裡面記錄下來
(function(j){
setTimeout(function(){
console.log(j); //這樣輸出就是0,1,2,3,4了
}, 1000*j);
})(i);
}
或者利用作用塊
// let可以作用於作用塊
for(let i=0; i<5; i++){
setTimeout( function(){
console.log(i); //這樣輸出就是0,1,2,3,4了
}, 1000*i);
}
這裡面本質就是
for(var i=0; i<5; i++){
// 使用僅存在於該作用塊的變數
let _i = i;
setTimeout( function(){
console.log(_i); //這樣輸出就是0,1,2,3,4了
}, 1000*_i);
}
5.5 模塊
實現模塊的模式稱為模塊暴露。
function MyModule(){
var something = 'cool';
var another = [1,2,3];function doSomething(){ console.log(something); } function doAnother(){ console.log(another.join(',')); } //ES5的寫法 return { doSomething: doSomething, doAnother: doAnother, }; // ES6的寫法 /* return { doSomething, doAnother, }; */
}
// 得到了這個閉包
var foo = MyModule();foo.doSomething(); // cool
foo.doAnother(); // 1,2,3
- 只需要使用一次的時候,記得改成IIFE。
- 需要參數的時候,記得自己去定義參數表。
模塊模式另一個簡單但強大的變化用法是,命名將要作為公共 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 對象的內部引用,可以從內部對模塊實例進行修 改,包括添加或刪除方法和屬性,以及修改它們的值。
通過在模塊實例的內部保留對公共 API 對象的內部引用,可以從內部對模塊實例進行修 改,包括添加或刪除方法和屬性,以及修改它們的值。
5.5.1 現代的模塊機制
一個典型的模塊管理器可以定義為(應該是參考了Require.js)
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,
};
});
其使用方式為
// 名稱為bar的模塊,沒有依賴
MyModules.define('bar', [], function(){
function hello(who){
return 'Let me introduce: ' + who;
}
return {
hello: hello,
};
});
// 名稱為foo,依賴了bar。
// 註意這裡函數表達式能拿到bar,是因為define方法去modules中抽取了bar,然後傳入給它。
MyModules.define('foo', ['bar'], function(bar){
var hungry = 'hippo';
function awesome(){
// 因為外層參數已經傳入了bar,所以這裡也就能拿到bar的hello方法
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome,
};
});
var foo = MyModules.get('foo');
foo.awesome(); // LET ME INTRODUCE HIPPO
5.5.2 未來的模塊機制
ES6 的模塊沒有“行內”格式,必須被定義在獨立的文件中(一個文件一個模塊)。瀏覽 器或引擎有一個預設的“模塊載入器”(可以被重載,但這遠超出了我們的討論範圍)可 以在導入模塊時非同步地載入模塊文件。
bar.js
export function hello(who){
return 'Let me introduce: ' + who;
}
foo.js
// 直接獲得了hello
import {hello} from 'bar';
var hungry = 'hippo';
export function awesome(){
console.log(hello(hungry).toUpperCase());
}
baz.js
// 導入完整的foo模塊
module foo from 'bar';
foo.awesome(); // LET ME INTRODUCE HIPPO
5.6 小結
閉包類似於一個標準,關於如何在函數作為值按需傳遞的詞法環境中書寫代碼的
當函數可以記住並訪問所在的詞法作用域,即使函數是在當前詞法作用域之外執行,這時 就產生了閉包。
時閉包也是一個非常強大的工具,可以用多種形式來實現模塊等模式。
模塊有兩個特征:
- 為創建內部作用域而調用了一個包裝函數。
- 包裝函數的返回值至少包括一個對內部函數的引用,這樣就會創建涵蓋整個包裝函數內部作用域的閉包。
補充:模塊機制、AMD、require.js
立即執行函數寫法
使用"立即執行函數"(Immediately-Invoked Function Expression,IIFE),可以達到不暴露私有成員的目的。
var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})();
使用上面的寫法,外部代碼無法讀取內部的_count變數。
console.info(module1._count); //undefined
module1就是Javascript模塊的基本寫法。下麵,再對這種寫法進行加工。
放大模式:,必須分成幾個部分,或者一個模塊需要繼承另一個模塊,這時就有必要採用"放大模式"(augmentation)。
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
上面的代碼為module1模塊添加了一個新方法m3(),然後返回新的module1模塊。
寬放大模式(Loose augmentation) :在瀏覽器環境中,模塊的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先載入。如果採用上一節的寫法,第一個執行的部分有可能載入一個不存在空對象,這時就要採用"寬放大模式"。
var module1 = ( function (mod){
//...
return mod;
})(window.module1 || {});
與"放大模式"相比,"寬放大模式"就是"立即執行函數"的參數可以是空對象。
輸入全局變數
獨立性是模塊的重要特點,模塊內部最好不與程式的其他部分直接交互。
為了在模塊內部調用全局變數,必須顯式地將其他變數輸入模塊。
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO);
上面的module1模塊需要使用jQuery庫和YUI庫,就把這兩個庫(其實是兩個模塊)當作參數輸入module1。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關係變得明顯。
AMD規範
通行的Javascript模塊規範共有兩種:CommonJS和AMD。
CommonJS
2009年,美國程式員Ryan Dahl創造了node.js項目,將javascript語言用於伺服器端編程。
node.js的模塊系統,就是參照CommonJS規範實現的。在CommonJS中,有一個全局性方法require(),用於載入模塊。假定有一個數學模塊math.js,就可以像下麵這樣載入。
var math = require('math');
然後,就可以調用模塊提供的方法:
var math = require('math');
math.add(2,3); // 5
因為這個系列主要針對瀏覽器編程,不涉及node.js,所以對CommonJS就不多做介紹了。我們在這裡只要知道,require()用於載入模塊就行了。
瀏覽器環境 對於瀏覽器,模塊都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。
因此,瀏覽器端的模塊,不能採用"同步載入"(synchronous),只能採用"非同步載入"(asynchronous)。這就是AMD規範誕生的背景。
AMD是"Asynchronous Module Definition"的縮寫,意思就是"非同步模塊定義"。
AMD也採用require()語句載入模塊,但是不同於CommonJS,它要求兩個參數:
require([module], callback);
第一個參數[module],是一個數組,裡面的成員就是要載入的模塊;第二個參數callback,則是載入成功之後的回調函數。
目前,主要有兩個Javascript庫實現了AMD規範:require.js和curl.js。
require.js的用法
依次載入多個js文件會有很大的弊端。
首先,載入的時候,瀏覽器會停止網頁渲染,載入文件越多,網頁失去響應的時間就會越長;其次,由於js文件之間存在依賴關係,因此必須嚴格保證載入順序(比如上例的1.js要在2.js的前面),依賴性最大的模塊一定要放到最後載入,當依賴關係很複雜的時候,代碼的編寫和維護都會變得困難。
require.js的誕生解決了兩個問題
- 實現js文件的非同步載入,避免網頁失去響應;
- 管理模塊之間的依賴性,便於代碼的編寫和維護。
require.js的載入
使用require.js的第一步,是先去官方網站下載最新版本。
下載後,假定把它放在js子目錄下麵,就可以載入了。
<script src="js/require.js"></script>
//載入這個文件,也可能造成網頁失去響應。解決辦法有兩個,一個是把它放在網頁底部載入,另一個是寫成下麵這樣:
<script src="js/require.js" defer async="true" ></script>
//async屬性表明這個文件需要非同步載入,避免網頁失去響應。IE不支持這個屬性,只支持defer,所以把defer也寫上。
/*載入require.js以後,下一步就要載入我們自己的代碼了。假定我們自己的代碼文件是main.js,也放在js目錄下麵。那麼,只需要寫成下麵這樣就行了:*/
<script src="js/require.js" data-main="js/main"></script>
//data-main屬性的作用是,指定網頁程式的主模塊。在上例中,就是js目錄下麵的main.js,這個文件會第一個被require.js載入。由於require.js預設的文件尾碼名是js,所以可以把main.js簡寫成main。
主模塊的寫法
這時就要使用AMD規範定義的的require()函數。
require()非同步載入moduleA,moduleB和moduleC,瀏覽器不會失去響應;它指定的回調函數,只有前面的模塊都載入成功後,才會運行,解決了依賴性的問題。
一個實際的例子:假定主模塊依賴jquery、underscore和backbone這三個模塊,main.js就可以這樣寫:
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
// some code here
});
//require.js會先載入jQuery、underscore和backbone,然後再運行回調函數。主模塊的代碼就寫在回調函數中。
模塊的載入
預設情況下,require.js假定這三個模塊與main.js在同一個目錄,文件名分別為jquery.js,underscore.js和backbone.js,然後自動載入。
使用require.config()方法,我們可以對模塊的載入行為進行自定義。require.config()就寫在主模塊(main.js)的頭部。參數就是一個對象,這個對象的paths屬性指定各個模塊的載入路徑。
require.config({
paths: {
"jquery": "jquery.min",
"underscore": "underscore.min",
"backbone": "backbone.min"
}
});
如果這些模塊在其他目錄,比如js/lib目錄,則有兩種寫法。一種是逐一指定路徑;另一種則是直接改變基目錄(baseUrl) 。
require.config({//1
paths: {
"jquery": "**lib/**jquery.min",
"underscore": "**lib/**underscore.min",
"backbone": "**lib/**backbone.min"
}
});
//2
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min",
"underscore": "underscore.min",
"backbone": "backbone.min"
}
});
//也可以直接指定它的網址
require.config({
paths: {
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
}
});
AMD模塊的寫法
模塊必須採用特定的define()函數來定義。如果一個模塊不依賴其他模塊,那麼可以直接定義在define()函數之中。
假定現在有一個math.js文件,它定義了一個math模塊。那麼,math.js就要這樣寫:
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
//載入方法如下:
require(['math'], function (math){
alert(math.add(1,1));
});
如果這個模塊還依賴其他模塊,那麼define()函數的第一個參數,必須是一個數組,指明該模塊的依賴性。
define(['myLib'], function(myLib){
function foo(){
myLib.doSomething();
}
return {
foo : foo
};
});//當require()函數載入上面這個模塊的時候,就會先載入myLib.js文件。