1. 程式人生 > 實用技巧 >自呼叫匿名函式(匿名閉包)解析與呼叫

自呼叫匿名函式(匿名閉包)解析與呼叫

開啟jQuery原始碼,首先你會看到這樣的程式碼結構:

(function(window,undefined ){
//
})();
這是一個自呼叫匿名函式。什麼東東呢?在第一個括號內,建立一個匿名函式;第二個括號,立即執行

為什麼要建立這樣一個“自呼叫匿名函式”呢?
通過定義一個匿名函式,建立了一個“私有”的名稱空間,該名稱空間的變數和方法,不會破壞全域性的名稱空間。這點非常有用也是一個JS框架必須支援的功能,jQuery被應用在成千上萬的JavaScript程式中,必須確保jQuery建立的變數不能和匯入他的程式所使用的變數發生衝突。
接下來看看在 自呼叫匿名函式 中都實現了什麼功能,按照程式碼順序排列:

複製程式碼
複製程式碼
(function( window, undefined ) {
// 構造jQuery物件
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context, rootjQuery );
}
// 工具函式 Utilities
// 非同步佇列 Deferred
// 瀏覽器測試 Support
// 資料快取 Data
// 佇列 queue
// 屬性操作 Attribute
// 事件處理 Event
// 選擇器 Sizzle
// DOM遍歷
// DOM操作
// CSS操作
// 非同步請求 Ajax
// 動畫 FX
// 座標和大小
window.jQuery = window.$ = jQuery;
})(window);
複製程式碼
複製程式碼

匿名函式從語法上叫函式直接量,JavaScript語法需要包圍匿名函式的括號,事實上自呼叫匿名函式有兩種寫法:

複製程式碼
複製程式碼
(function() {
console.info( this );
console.info( arguments );
}( window ) );

(function() {
console.info( this );
console.info( arguments );
})( window );
複製程式碼
複製程式碼
經常使用的方法如下格式:

複製程式碼
複製程式碼
(function($, owner){
//登入
owner.login = function(info,callback){
//code
};

//註冊
owner.reg = function(name,callback){
    //code
};

}(jQuery, window.app = {}));

呼叫:
app.login(info,function(){});
app.reg(name,function(){})
複製程式碼
複製程式碼

為什麼要傳入window呢?

通過傳入window變數,使得window由全域性變數變為區域性變數,當在jQuery程式碼塊中訪問window時,不需要將作用域鏈回退到頂層作用域,這樣可以更快的訪問window;這還不是關鍵所在,更重要的是,將window作為引數傳入,可以在壓縮程式碼時進行優化,看看jquery-1.6.1.min.js: (function(a,b){})(window); // window 被優化為 a
通過以上的介紹,我們大概瞭解通過()可以使得一個函式表示式立即執行。

匿名函式作為一個“容器”,“容器”內部可以訪問外部的變數,而外部環境不能訪問“容器”內部的變數,
所以 ( function(){…} )() 內部定義的變數不會和外部的變數發生衝突,俗稱“匿名包裹器”或“名稱空間”。

複製程式碼
複製程式碼
(function () {
// ... 所有的變數和function都在這裡宣告,並且作用域也只能在這個匿名閉包裡
// ...但是這裡的程式碼依然可以訪問外部全域性的物件
}());
同下面
(function () {/* 內部程式碼 */})();
複製程式碼
複製程式碼

通俗的講,()就是用來求值的,因此這個()任何時候都不能為空,因為它是要計算的。函式解析它只會解析到 {}為止,不會解析到 ()的。
把表示式放在()中會返回表示式的值;
把函式放在()中會返回函式本身;(function(){}());
如果()緊跟在函式後面,就是表示在呼叫函式,即對函式求值:(function(){})();

(function() {
//自執行函式中的內部變數,外部是無法訪問的
var name = 'kevin';
})( window );
name //undefined,無法獲取name的值

程式碼在執行過程中,會優先解析 【巳宣告的函式】;
而函式表示式是當執行到它時,才會解析;
匿名函式是不會單獨寫的,因此它的執行是需要其它函式的呼叫,通常看到的匿名函式,都是當作引數被傳遞的。而立即執行函式它本身就是個匿名函式,

js程式碼執行的順序:
//巳宣告的函式 function test(){}
//匿名函式 function (){}
//函式表示式 var test = function(){}
//立即執行函式 (function(){})();

立即執行函式配合閉包,在模組化中的應用,其中要明白幾個點:
1、要在函式體後面加括號就能立即呼叫,則這個函式必須是函式表示式,不能是函式宣告;
2、立即執行函式可以當作是一個私有作用域,作用域內部可以訪問外部的變數,而外部環境是不能訪問作用域內部的變數的,因此,立即執行函式是一個封閉的作用域,不會和外部作用域起衝突。
JQuery使用的就是這種方法,將JQuery程式碼包裹在( function (window,undefined){…jquery程式碼…} (window)中,在全域性作用域中呼叫JQuery程式碼時,可以達到保護JQuery內部變數的作用。
3、Module模式,是自執行函式的高階模式,可以非常方便的在各個匿名閉包中以全域性物件呼叫閉包函式。有興趣可以檢視:http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html Module 模式為:
  a.建立一個立即呼叫的匿名函式表示式
  b.return一個變數,其中這個變數裡包含你要暴露的東西
  c.返回的這個變數將賦值給window

複製程式碼
複製程式碼
(function () {
var i = 0;
return {
get: function () {
return i;
},
set: function (val) {
i = val;
},
increment: function () {
return ++i;
}
};
} (window));

// window作為一個帶有多個屬性的全域性物件,上面的程式碼對於屬性的體現其實是方法,它可以這樣呼叫:
window.get(); // 0
window.set(3);
window.increment(); // 4
window.increment(); // 5

window.i; // undefined 因為i不是返回物件的屬性
i; // 引用錯誤: i 沒有定義(因為i只存在於閉包)
複製程式碼
複製程式碼

/上面就是關於自呼叫匿名函式的解析,那麼這樣的函式它是怎麼被呼叫的呢?*/

/下面是關於全域性變數的呼叫,也就是匿名閉包函式的呼叫*/

再次搬出Module模式,有興趣可以檢視:http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html
Module 模式,也就是匿名閉包的建立與呼叫:
  a.建立一個立即呼叫的匿名函式表示式
  b.return一個變數,其中這個變數裡包含你要暴露的東西
  c.返回的這個變數將賦值給window

window(或者是任意一個全域性物件)作為一個帶有多個屬性的全域性物件,也可以把window當成一個引數,以物件的方式,在其它函式中實現呼叫。用下面的例子說明:

複製程式碼
複製程式碼
(function ($, YAHOO) {
// 這裡,我們的程式碼就可以使用全域性的jQuery物件了,YAHOO也是一樣
$.aa = function(){
//code
}
} (jQuery, YAHOO));
//呼叫 jQuery.aa();
複製程式碼
複製程式碼

下面是一個標準的Module模式,通過匿名函式的返回值來返回這個全域性變數:

複製程式碼
複製程式碼
var blogModule = (function () {
var my = {}, privateName = "部落格園";

function privateAddTopic(data) {
// 這裡是內部處理程式碼
}

my.Name = privateName;
my.AddTopic = function (data) {
privateAddTopic(data);
};

return my;
} ());
//呼叫 blogModule.my();
複製程式碼
複製程式碼

在一些大型專案裡,將一個功能分離成多個檔案是非常重要的,因為可以多人合作易於開發。再回頭看看上面的全域性引數匯入例子,我們能否把blogModule自身傳進去呢?答案是肯定的,我們先將blogModule傳進去,新增一個函式屬性,然後再返回就達到了我們所說的目的:

複製程式碼
複製程式碼
var blogModule = (function (my) {
my.AddPhoto = function () {
//新增內部程式碼
};
return my;
} (blogModule || {}));

(function (my){
my.AddPhoto = function () {
//新增內部程式碼
};
return my;
})(blogModule || {}));
//呼叫 blogModule.AddPhoto();
複製程式碼
複製程式碼

那麼,多個自執行函式間是怎麼呼叫的呢?

複製程式碼
複製程式碼
(function(owner) {
//第一個匿名閉包
owner.debug = true;
//Ajax相關引數配置
owner.ajax = {
timeout: 10000,
type: 'post',
dataType: 'json',
};

})(window.C = {}));
複製程式碼
複製程式碼

如果第二個函式想呼叫 全域性變數為C中的 物件呢?要怎麼寫?

複製程式碼
複製程式碼
(function($, owner) {
//這裡呼叫上面全域性變數為C 中的物件呢
if(!C.debug) return false;
var url = 'aaa.html';
mui.ajax({
url: url,
dataType: C.ajax.dataType,
type: C.ajax.type,
});
})(mui, window.app = {});
複製程式碼
複製程式碼

再舉個例子,同樣的,不同自執行閉包函式間的呼叫方法:

複製程式碼
複製程式碼
(function($, owner) {
//獲取語言閉包
owner.getLanguage = function() {
var language = localStorage.getItem(C.state.field.language);
if(typeof language == "undefined" || language === null || language == '') {
var currentLang = navigator.language;
if(!currentLang)
currentLang = navigator.browserLanguage;
language = currentLang.toLowerCase();
language = language.replace(/-/g, '_');

if(language != 'en_us' && language != 'zh_cn')
language = 'en_us';

localStorage.setItem(C.state.field.language, language);
}
複製程式碼
複製程式碼

複製程式碼
複製程式碼
//在上面的解析中有說過,Module模式,return 一個變數,這個變數就是要爆露的東西。通過這個函式的全域性變數,這個 language 可以在任何地方呼叫
//return一個變數,其中這個變數裡包含你要暴露的東西
//全域性呼叫 storage.language
return language;
};
})(mui, window.storage = {}));

(function($, owner) {
owner.language = {};
owner.preload = function(settings){
var defaults = {
name: 'i18n',
language: '',
path: '/',
cache: true,
encoding: 'UTF-8',
autoReplace: true,
success: null,
error: null,
};

settings = $.extend(defaults, settings);
if(settings.language === null || settings.language == '') {
//全域性呼叫 storage.language
settings.language = storage.getLanguage();
}
}
})(mui, window.i18n = {});
複製程式碼
複製程式碼

所以 匿名閉包的呼叫規則是這樣的,立即執行(最後一個括號) (window),如果把window作為一個引數進行傳遞,那麼就把它以物件的方式,在其它函式中實現全域性呼叫。