在 jQuery 框架中,jQuery 對象是一個很奇怪的概念,具有多重身份,所以很多初學者一聽說 jQuery 對象就感覺很是不解,誤以爲它是 John Resig 製造的新概念。我們可以對jQuery 對象進行如下分解。
第一,jQuery 對象是一個數據集合,它不是一個個體對象。因此,你無法直接使用 JavaScript 的方法來操作它。
第二,jQuery 對象實際上就是一個普通的對象,因爲它是通過 new 運算符創建的一個新的實例對象。它可以繼承原型方法或屬性,同時也擁有 Object 類型的方法和屬性。
第三,jQuery 對象包含數組特性,因爲它賦值了數組元素,以數組結構存儲返回的數據。我們可以以 JavaScript 的概念理解 jQuery 對象,例如下面的示例。
- <script type="text/javascript">
- var jquery = { // 定義對象直接量
- name: "jQuery", // 以屬性方式存儲信息
- value: "1.3.2"
- };
- jquery[0] = "jQuery"; // 以數組方式存儲信息
- jquery[1] = "1.3.2";
- alert(jquery.name); // 返回 "jQuery"
- alert(jquery[0]); // 返回 "jQuery"
- </script>
上面的 jQuery 對象就是一個典型的 jQuery 對象,jQuery 對象的結構就是按這種形式設計的。可以說,jQuery
對象就是對象和數組的混合體,但是它不擁有數組的方法,因爲它的數組結構是人爲附加的,也就是說它不是 Array 類型數據,而是 Object 類型數據。
第四,jQuery 對象包含的數據都是 DOM 元素,是通過數組形式存儲的,即通過 jQuery[n] 形式獲取。同時 jQuery 對象又定義了幾個模仿 Array 基本特性的屬性,如 length 等。
所以,jQuery 對象是不允許直接操作的,只有分別讀取它包含的每一個 DOM 元素,才能實現各種操作,如插入、刪除、嵌套、賦值和讀寫 DOM 元素屬性等。
那麼如何實現直接操作 jQuery 對象中的 DOM 元素呢?
在實際應用中,我們可以看到類似下面的 jQuery 用法。
$("div").html()
也就是直接在 jQuery 對象上調用 html(),並實現操作 jQuery 包含的所有 DOM 元素。那麼這個功能是怎麼實現的呢?
jQuery 定義了一個工具函數 each(),利用這個工具可以遍歷 jQuery 對象中所有的 DOM 元素,並把需要操作的內容封裝到一個回調函數中,然後通過在每個 DOM 元素上調用這個回調函數即可。實現代碼如下所示,演示效果如圖 2.2 所示。
- <script type="text/javascript">
- var $ = jQuery = function(selector, context){ // 定義類
- return new jQuery.fn.init(selector, context); // 返回選擇器的實例
- };
- jQuery.fn = jQuery.prototype = { // jQuery 類的原型對象
- init: function(selector, context){ // 定義選擇器構造器
- selector = selector || document; // 設置默認值爲 document
- context = context || document; // 設置默認值爲 document
- if(selector.nodeType){ // 如果選擇符爲節點對象
- this[0] = selector; // 把參數節點傳遞給實例對象的數組
- this.length = 1; // 並設置實例對象的 length 屬性,定義包含的元素個數
- this.context = selector; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- if(typeof selector === "string"){ // 如果選擇符是字符串
- var e = context.getElementsByTagName(selector); // 獲取指定名稱的元素
- for(var i = 0; i<e.length; i++){ // 遍歷元素集合,並把所有元素填入到當前實例數組中
- this[i] = e[i];
- }
- this.length = e.length; // 設置實例的 length 屬性,即定義包含的元素個數
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- } else {
- this.length = 0; // 否則,設置實例的 length 屬性值爲 0
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- },
- html: function(val){ // 模仿 jQuery 框架中的 html() 方法,爲匹配的每一個DOM元素插入html代碼
- jQuery.each(this, function(val){ // 調用 jQuery.each() 工具函數,爲每一個 DOM 元素執行回調函數
- this.innerHTML = val;
- }, val);
- },
- jquery: "1.3.2", // 原型屬性
- size: function(){ // 原型方法
- return this.length;
- }
- };
- jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型對象覆蓋 init 的原型對象
- // 擴展 jQuery 工具函數
- jQuery.each = function(object, callback, args){
- for(var i=0; i<object.length; i++){
- callback.call(object[i], args);
- }
- return object;
- };
- $("div").html("測試代碼");
- </script>
在上面的示例中,通過先爲自己的 jQuery 對象綁定 html() 方法,然後利用 jQuery() 選擇器獲取頁面中所有的 div 元素,再調用 html() 方法,爲所有匹配的元素插入 HTML 源碼。
注意,在上面的代碼中,each() 函數的當前作用對象是 jQuery 對象,故 this 指向當前 jQuery 對象,即 this 表示一個集合對象;而在 html() 方法中,由於 each() 函數是在指定 DOM 元素上執行的,所以該函數內的 this 指針指向的是當前 DOM 元素對象,即 this 表示一個元素。
2.2.7 延續 – 功能擴展
根據一般設計習慣,如果要爲 jQuery 或者 jQuery.prototype 添加函數或方法,可以直接通過點語法實現,或者在 jQuery.prototype 對象結構中增加一個屬性即可。但是,如果分析 jQuery 框架的源代碼,你會發現它是通過 extend() 函數來實現功能擴展的。例如,下面兩段代碼都是 jQuery 框架通過 extend() 函數來擴展功能的。
jQuery.extend({ // 擴展工具函數
noConflict: function(deep){},
isFunction: function(obj){},
isArray: function(obj){},
isXMLDoc: function(elem){},
globalEval: function(data){}
});
或者
jQuery.fn.extend({ // 擴展 jQuery 對象方法
show: function(speed, callback){},
hide: function(speed, callback){},
toggle: function(fn, fn2){},
fadeTo: function(speed, to, callback){},
animate: function(prop, speed, easing, callback){},
stop: function(clearQueue, gotoEnd){}
});
這樣做的好處是什麼呢?
extend() 函數能夠方便用戶快速擴展 jQuery 框架的功能,但是不會破壞 jQuery 框架的原型結構,從而避免後期人工手動添加工具函數或者方法破壞 jQuery 框架的單純性,同時也方便管理。如果不需要某個插件,只需要簡單地刪除即可,而不需要在 jQuery 框架源代碼中去篩選和刪除。
extend() 函數的功能實現起來也很簡單,它只是把指定對象的方法複製給 jQuery 對象或者 jQuery.prototype 對象。例如,在下面的示例中,我們爲 jQuery 類和原型定義了一個擴展功能的函數 extend() ,該函數的功能很簡單,它能夠把指定參數對象包含的所有屬性複製給 jQuery 或者 jQuery.prototype 對象,這樣就可以在應用中隨時調用它,並動態擴展 jQuery 對象的方法。
- <div></div>
- <div></div>
- <div></div>
- <script type="text/javascript">
- var $ = jQuery = function(selector, context){ // 定義類
- return new jQuery.fn.init(selector, context); // 返回選擇器的實例
- };
- jQuery.fn = jQuery.prototype = { // jQuery 類的原型對象
- init: function(selector, context){ // 定義選擇器構造器
- selector = selector || document; // 設置默認值爲 document
- context = context || document; // 設置默認值爲 document
- if(selector.nodeType){ // 如果選擇符爲節點對象
- this[0] = selector; // 把參數節點傳遞給實例對象的數組
- this.length = 1; // 並設置實例對象的 length 屬性,定義包含的元素個數
- this.context = selector; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- if(typeof selector === "string"){ // 如果選擇符是字符串
- var e = context.getElementsByTagName(selector); // 獲取指定名稱的元素
- for(var i = 0; i<e.length; i++){ // 遍歷元素集合,並把所有元素填入到當前實例數組中
- this[i] = e[i];
- }
- this.length = e.length; // 設置實例的 length 屬性,即定義包含的元素個數
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- } else {
- this.length = 0; // 否則,設置實例的 length 屬性值爲 0
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- },
- jquery: "1.3.2", // 原型屬性
- size: function(){ // 原型方法
- return this.length;
- }
- };
- jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型對象覆蓋 init 的原型對象
- // jQuery 功能擴展函數
- jQuery.extend = jQuery.fn.extend = function(obj){
- for(var prop in obj){
- this[prop] = obj[prop];
- }
- return this;
- };
- // 擴展 jQuery 對象方法
- jQuery.fn.extend({
- test: function(){
- alert("測試擴展功能");
- }
- });
- // 測試代碼
- $("div").test();
- </script>
在上面的示例中,先定義了一個功能擴展函數 extend(),然後爲 jQuery.fn 原型對象調用 extend() 函數,爲其添加一個測試方法 test()。這樣就可以在實踐中應用,如 $("div").test() 。
jQuery 框架定義的 extend() 函數的功能要強大很多,它不僅能夠完成基本的功能擴展,還可以實現對象合併等功能。
2.2.8 延續 – 參數處理
在很多時候,你會發現 jQuery 的方法都要求傳遞的參數爲對象結構,例如:
$.ajax({
type: "GET",
url: "test.js",
dataType: "script"
});
使用對象直接量作爲參數進行傳遞,方便參數管理。當方法或者函數的參數長度不固定時,使用對象直接量作爲參數存在很多優勢。例如,對於下面的用法,ajax()函數就需要進行更加複雜的參數排查和過濾。
$.ajax("GET", "test.js", "script");
如果 ajax() 函數的參數長度是固定的,且是必須的,那麼通過這種方式進行傳遞也就無所謂了,但是如果參數的個數和排序是動態的,那麼使用 $.ajax("GET", "test.js", "script"); 這種方法是無法處理的。而 jQuery 框架的很多方法都包含大量的參數,且都是可選的,位置也沒有固定要求,所以使用對象直接量是惟一的解決方法。
使用對象直接量作爲參數傳遞的載體,這裏就涉及參數處理問題。如何解析並提出參數?如何處理參數和默認值?我們可以通過下面的方法來實現。
- <script type="text/javascript">
- var $ = jQuery = function(selector, context){ // 定義類
- return new jQuery.fn.init(selector, context); // 返回選擇器的實例
- };
- jQuery.fn = jQuery.prototype = { // jQuery 類的原型對象
- init: function(selector, context){ // 定義選擇器構造器
- selector = selector || document; // 設置默認值爲 document
- context = context || document; // 設置默認值爲 document
- if(selector.nodeType){ // 如果選擇符爲節點對象
- this[0] = selector; // 把參數節點傳遞給實例對象的數組
- this.length = 1; // 並設置實例對象的 length 屬性,定義包含的元素個數
- this.context = selector; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- if(typeof selector === "string"){ // 如果選擇符是字符串
- var e = context.getElementsByTagName(selector); // 獲取指定名稱的元素
- for(var i = 0; i<e.length; i++){ // 遍歷元素集合,並把所有元素填入到當前實例數組中
- this[i] = e[i];
- }
- this.length = e.length; // 設置實例的 length 屬性,即定義包含的元素個數
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- } else {
- this.length = 0; // 否則,設置實例的 length 屬性值爲 0
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- },
- setOptions: function(options){
- this.options = { // 方法的默認值,可以擴展
- StartColor: "#000",
- EndColor: "#DDC",
- Step: 20,
- Speed: 10
- };
- jQuery.extend(this.options, options || {}); // 如果傳遞參數,則覆蓋原默認參數
- },
- jquery: "1.3.2", // 原型屬性
- size: function(){ // 原型方法
- return this.length;
- }
- };
- jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型對象覆蓋 init 的原型對象
- jQuery.extend = jQuery.fn.extend = function(destination, source){ // 重新定義 extend() 函數
- for (var property in source){
- destination[property] = source[property];
- }
- return destination;
- };
- </script>
在上面的示例中,定義了一個原型方法 setOptions(),該方法能夠對傳遞的參數對象進行處理,並覆蓋默認值。這種用法在本書插件部分還將進行講解。
在 jQuery 框架中, extend() 函數包含了所有功能,它既能夠爲當前對象擴展方法,也能夠處理參數對象,並覆蓋默認值。
2.2.9 涅槃 – 名字空間
現在,我們終於模擬出了 jQuery 框架的雛形,雖然它還比較稚嫩,經不起風雨,但至少能夠保證讀者理解 jQuery 框架構成的初期狀態。不過對於一個成熟的框架來說,需要設計者考慮的問題還是很多的,其中最核心的問題就是名字空間衝突問題。
當一個頁面中存在多個框架,或者自己寫了很多 JavaScript 代碼,我們是很難確保這些代碼不發生衝突的,因爲任何人都無法確保自己非常熟悉 jQuery 框架中的每一行代碼,所以難免會出現名字衝突,或者功能覆蓋現象。爲了解決這個問題,我們必須把 jQuery 封裝在一個孤立的環境中,避免其他代碼的干擾。
在詳細講解名字空間之前,我們先來溫習兩個 JavaScript 概念。首先,請看下面的代碼。
var jQuery = function(){};
jQuery = function(){};
上面所示的代碼是兩種不同的寫法,且都是合法的,但是它們的語義完全不同。第一行代碼聲明瞭一個變量,而第二行代碼定義了 Window 對象的一個屬性,也就是說它等同於下面的語句。
window.jQuery = function();
在全局作用域中,變量和 Window 對象的屬性是可以相等的,也可以是互通的,但是當在其他環境中 (如局部作用域中),則它們是不相等的,也是無法互通的。
因此如果希望 jQuery 具有類似 $.method(); 調用方式的能力,就需要將 jQuery 設置爲 Window 對象的一個屬性,所以你就會看到 jQuery 框架中是這樣定義的。
- <script type="text/javascript">
- var jQuery = window.jQuery = window.$ = function(selector, context){
- return new jQuery.fn.init(selector, context);
- };
- </script>
你可能看到過下面的函數用法。
(function(){
alert("觀察我什麼時候出現");
})();
這是一個典型的匿名函數基本形式。爲什麼要用到匿名函數呢?
這時就要進入正題了,如果希望自己的 jQuery 框架與其他任何代碼完全隔離開來,也就是說如果你想把 jQuery 裝在一個封閉空間中,不希望暴露內部信息,也不希望別的代碼隨意訪問,匿名函數就是一種最好的封閉方式。此時我們只需要提供接口,就可以方便地與外界進行聯繫。例如,在下面的示例中分別把 f1 函數放在一個匿名函數中,而把 f2 函數放在全局作用域中。可以發現,全局作用域中的 f2 函數可以允許訪問,而匿名函數中的 f1 函數是禁止外界訪問的。
- <script type="text/javascript">
- (function(){
- function f1(){
- return "f1()";
- }
- })();
- function f2(){
- return "f2()";
- }
- alert(f2()); // 返回 "f2()"
- alert(f1()); // 拋出異常,禁止訪問
- </script>
實際上,上面的匿名函數就是所謂的閉包,閉包是 JavaScript 函數中一個最核心的概念。
當然,
在這之前我們需要讓它與其他框架協同工作,這就帶來一個問題,如果我們都使用 作爲簡寫形式就會發生衝突,<strong><span style="color:rgb(255,0,0)">爲此 jQuery 提供了一個 noConflit() 方法,該方法能夠實現禁止 jQuery 框架使用這兩個名字。爲了實現這樣的目的,jQuery 在框架的最前面,先使用 _ 和 _jQuery 臨時變量寄存 和 jQuery 這兩個變量的內容,當需要禁用 jQuery 框架的名字時,可以使用一個臨時變量 _ 和 _jQuery 恢復 $ 和 jQuery 這兩個變量的實際內容。實現代碼如下。
- <script type="text/javascript">
- (function(){
- var
- window = this,
- undefined,
- _jQuery = window.jQuery, // 暫存 jQuery 變量內容
- _ = window. , // 暫存 $ 變量內容
- jQuery = window.jQuery = window.$ = function(selector, context){
- return new jQuery.fn.init(selector, context);
- },
- quickExpr = /^[^<]*(<(.|\s)+>)[^>]*|^#([\w-]+) /,
- isSimple = /^.[^:#\[\.,]*$/;
- jQuery.fn = jQuery.prototype = {
- init: function(selector, context){ // 定義選擇器構造器
- selector = selector || document; // 設置默認值爲 document
- context = context || document; // 設置默認值爲 document
- if(selector.nodeType){ // 如果選擇符爲節點對象
- this[0] = selector; // 把參數節點傳遞給實例對象的數組
- this.length = 1; // 並設置實例對象的 length 屬性,定義包含的元素個數
- this.context = selector; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- if(typeof selector === "string"){ // 如果選擇符是字符串
- var e = context.getElementsByTagName(selector); // 獲取指定名稱的元素
- for(var i = 0; i<e.length; i++){ // 遍歷元素集合,並把所有元素填入到當前實例數組中
- this[i] = e[i];
- }
- this.length = e.length; // 設置實例的 length 屬性,即定義包含的元素個數
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- } else {
- this.length = 0; // 否則,設置實例的 length 屬性值爲 0
- this.context = context; // 設置實例的屬性,返回選擇範圍
- return this; // 返回當前實例
- }
- },
- setOptions: function(options){
- this.options = { // 方法的默認值,可以擴展
- StartColor: "#000",
- EndColor: "#DDC",
- Step: 20,
- Speed: 10
- };
- jQuery.extend(this.options, options || {}); // 如果傳遞參數,則覆蓋原默認參數
- },
- jquery: "1.3.2", // 原型屬性
- size: function(){ // 原型方法
- return this.length;
- }
- };
- jQuery.fn.init.prototype = jQuery.fn; // 使用 jQuery 的原型對象覆蓋 init 的原型對象
- jQuery.extend = jQuery.fn.extend = function(destination, source){ // 重新定義 extend() 函數
- for (var property in source){
- destination[property] = source[property];
- }
- return destination;
- };
- })();
- </script>
至此,jQuery 框架的設計模式就初見端倪了,後面的工作就是根據應用需要或者功能需要,使用 extend() 函數不斷擴展 jQuery 的工具函數和 jQuery 對象的方法。