第二章 jQuery技術解密(一)

mybwu_com發表於2013-12-28

2.2 jQuery 原型技術分解

任何複雜的技術都是從最簡單的問題開始的,如果你被 jQuery 幾千行龐雜結構的原始碼所困惑,那麼建議你閱讀本節內容,我們將探索 jQuery 是如何從最簡單的問題開始,並逐步實現羽翼漸豐的演變過程,從 jQuery 核心技術的還原過程來理解 jQuery 框架的搭建原理。

2.2.1 起源 -- 原型繼承

用過 JavaScript 的讀者都會明白,在 JavaScript 指令碼中到處都是函式,函式可以歸置程式碼段,把相對獨立的功能封裝在一個函式包中。函式也可以實現類,這個類是物件導向程式設計中最基本的概念,也是最高抽象,定義一個類就相當於製作了一個模型,然後藉助這個模型複製無數的例項。

例如,下面的程式碼就可以定義最初的 jQuery 類,類名就是 jQuery ,你可以把它視為一個函式,函式名是 jQuery 。當然,你也可以把它視為一個物件,物件名就是 jQuery 。與其他物件導向的程式語言相比,JavaScript 對於這個概念的界定好像很隨意,這降低了程式設計的門檻,反之也降低了 JavaScript 作為程式語言的層次。

<script language="javascript" type="text/javascript">

var jQuery = function(){

// 函式體

};

</script>

上面建立了一個空的函式,好像什麼都不能夠做,這個函式實際上就是所謂的建構函式。建構函式在面嚮物件語言中是類的一個特殊方法,用來建立類。在 JavaScript 中,你可以把任何函式都視為建構函式,這沒有什麼不可以的,這樣不會傷害程式碼本身。

所有類都有最基本的功能,如繼承、派生和重寫等。JavaScript 很奇特,它通過為所有函式繫結一個 prototype 屬性,由這個屬性指向一個原型物件,原型物件中可以定義類的繼承屬性和方法等。所以,對於上面的空類,可以繼續擴充套件原型,其程式碼如下。

<script language="javascript" type="text/javascript">

var jQuery = function(){};

jQuery.prototype = {

// 擴充套件的原型物件

};

</script>

原型物件是 JavaScript 實現繼承的基本機制。如果你覺得 jQuery.prototype 名稱太長,沒有關係,我們可以為其重新命名,如 fn ,當然你可以隨便命名。如果直接命名 fn ,則表示該名稱屬性 Window 物件,即全域性變數名。更安全的方法是為 jQuery 類定義一個公共屬性, jQuery.fn ,然後把 jQuery 的原型物件傳遞給這個公共屬性,實現程式碼如下。

<script language="javascript" type="text/javascript">

jQuery.fn = jQuery.prototype = {

// 擴充套件的原型物件

};

</script>

這裡的 jQuery.fn 相當於 jQuery.prototype 的別名,方便以後使用,它們指向同一個引用。因此若要呼叫 jQuery 的原型方法,直接使用 jQuery.fn 公共屬性即可,不需要直接引用 jQuery.prototype ,當然直接使用 jQuery.prototype 也是可以的。

既然原型物件可以使用別名,jQuery 類也可以起個別名,我們可以使用 $ 符號來引用它,程式碼如下。

var $ = jQuery = function(){};

現在模仿 jQuery 框架原始碼,給它新增兩個成員,一個是原型屬性 jquery ,一個是原型方法 size(),其程式碼如下。

<script language="javascript" type="text/javascript">

var $ = jQuery = function(){};

jQuery.fn = jQuery.prototype = {

jquery: "1.3.2", // 原型屬性

size: function(){// 原型方法

return this.length;

}

};

</script>

2.2.2 生命 -- 返回例項

當我們為 jQuery 新增了兩個原型成員:jquery 屬性和 size() 方法之後,這個框架最基本的樣子就孕育出來了。但是該如何呼叫 jquery 屬性和 size() 方法呢?

也許,你可以採用如下方法呼叫:

<script language="javascript" type="text/javascript">

var my$ = new $(); // 例項化

alert(my$.jquery);// 呼叫屬性,返回 "1.3.2"

alert(my$.size());// 呼叫方法,返回 undefined

</script>

但是,jQuery 不是這樣呼叫的。它模仿類似下面的方法進行呼叫。

$().jquery;

$().size();

也就是說,jQuery 沒有使用 new 運算子將 jQuery 類例項化,而是直接呼叫 jQuery() 函式,然後在這個函式後面直接呼叫 jQuery 的原型方法。這是怎麼實現的呢?

如果你模仿 jQuery 框架的用法執行下面的程式碼,瀏覽器會顯示編譯錯誤。這說明上面這個案例程式碼還不是真正的 jQuery 技術原型。

alert($().jquery);

alert($().size());

也就是說,我們應該把 jQuery 看做一個類,同時也應該把它視為一個普通函式,並讓這個函式的返回值為 jQuery 類的例項。因此,下面這種結構模型才是正確的。

  1. <scripttype="text/javascript">
  2. var$=jQuery=function(){
  3. returnnewjQuery();//返回類的例項
  4. };
  5. jQuery.fn=jQuery.prototype={
  6. jquery:"1.3.2",//原型屬性
  7. size:function(){//原型方法
  8. returnthis.length;
  9. }
  10. };
  11. alert($().jquery);
  12. alert($().size());
  13. </script>
但是,如果在瀏覽器中預覽,則會提示如圖 2.1 所示的錯誤。記憶體外溢,說明出現了死迴圈引用。


那麼如何返回一個 jQuery 例項呢?

回憶一下,當使用 var my$ = new $(); 建立 jQuery 類的例項時,this 關鍵字就指向物件 my$ ,因此 my$ 例項物件就獲得了 jQuery.prototype 包含的原型屬性或方法,這些方法內的 this 關鍵字就會自動指向 my$ 例項物件。換句話說,this 關鍵字總是指向類的例項。

因此,我們可以這樣嘗試:在 jQuery 中使用一個工廠方法來建立一個例項 (就是 jQuery.fn),把這個方法放在 jQuery.prototype 原型物件中,然後在 jQuery() 函式中返回這個原型方法的呼叫。程式碼如下所示。

  1. <scripttype="text/javascript">
  2. var$=jQuery=function(){
  3. returnjQuery.fn.init();//呼叫原型init()
  4. };
  5. jQuery.fn=jQuery.prototype={
  6. init:function(){//在初始化原型方法中返回例項的引用
  7. returnthis;
  8. },
  9. jquery:"1.3.2",//原型屬性
  10. size:function(){//原型方法
  11. returnthis.length;
  12. }
  13. };
  14. alert($().jquery);//呼叫屬性,返回"1.3.2"
  15. alert($().size());//呼叫方法,返回undefined
  16. </script>

2.2.3 學步 -- 分隔作用域

我們已經初步實現了讓 jQuery() 函式能夠返回 jQuery 類的例項,下面繼續思考:init() 方法返回的是 this 關鍵字,該關鍵字引用的是 jQuery 類的例項,如果在 init() 函式中繼續使用 this 關鍵字,也就是說,假設我們把 init() 函式也視為一個構造器,則其中的 this 該如何理解和處理?

例如,在下面示例中,jQuery 原型物件中包含一個 length 屬性,同時 init() 從一個普通的函式轉身變成了構造器,它也包含一個 length 屬性和一個 test() 方法。執行該示例,我們可以看到,this 關鍵字引用了 init() 函式作用域所在的物件, 此時它訪問 length 屬性時,返回0. 而 this 關鍵字也能夠訪問上一級物件 jQuery.fn 物件的作用域,所以 $().jquery 返回 "1.3.2" 。但是呼叫 $().size() 方法時,返回的是 0, 而不是 1 。

  1. <scripttype="text/javascript">
  2. var$=jQuery=function(){
  3. returnjQuery.fn.init();//呼叫原型init()
  4. };
  5. jQuery.fn=jQuery.prototype={
  6. init:function(){//在初始化原型方法中返回例項的引用
  7. this.length=0;
  8. this.test=function(){
  9. returnthis.length;
  10. }
  11. returnthis;
  12. },
  13. jquery:"1.3.2",//原型屬性
  14. length:1,
  15. size:function(){//原型方法
  16. returnthis.length;
  17. }
  18. };
  19. alert($().jquery);//返回"1.3.2"
  20. alert($().test());//返回0
  21. alert($().size());//返回0
  22. </script>
這種設計思路很容易破壞作用域的獨立性,對於 jQuery 這樣的框架來說,很可能會造成消極影響。因此,我們可以看到 jQuery 框架是通過下面的方式呼叫 init() 初始化建構函式的。

<script type="text/javascript">

var $ = jQuery = function(){

return new jQuery.fn.init(); // 例項化 init 初始化型別,分隔作用域

};

</script>

這樣就可以把 init() 構造器中的 this 和 jQuery.fn 物件中的 this 關鍵字隔離開來,避免相互混淆。但是,這種方式也會帶來另一個問題:無法訪問 jQuery.fn 物件的屬性或方法。例如,在下面的示例中,訪問 jQuery.fn 原型物件的 jquery 屬性和 size() 方法時就會出現這個問題。

  1. <scripttype="text/javascript">
  2. var$=jQuery=function(){
  3. returnnewjQuery.fn.init();//例項化init初始化型別,分隔作用域
  4. };
  5. jQuery.fn=jQuery.prototype={
  6. init:function(){//在初始化原型方法中返回例項的引用
  7. this.length=0;
  8. this.test=function(){
  9. returnthis.length;
  10. }
  11. returnthis;
  12. },
  13. jquery:"1.3.2",//原型屬性
  14. length:1,
  15. size:function(){//原型方法
  16. returnthis.length;
  17. }
  18. };
  19. alert($().jquery);//返回undefined
  20. alert($().test());//返回0
  21. alert($().size());//丟擲異常
  22. </script>

2.2.4 生長 -- 跨域訪問

如何做到既能夠分隔初始化構造器函式與 jQuery 原型物件的作用域,又能夠在返回例項中訪問 jQuery 原型物件呢?

jQuery 框架巧妙地通過原型傳遞解決了這個問題,它把 jQuery.fn 傳遞給 jQuery.fn.init.prototype ,也就是說用 jQuery 的原型物件覆蓋 init 構造器的原型物件,從而實現跨域訪問,其程式碼如下所示。

  1. <scripttype="text/javascript">
  2. var$=jQuery=function(){
  3. returnnewjQuery.fn.init();//例項化init初始化型別,分隔作用域
  4. };
  5. jQuery.fn=jQuery.prototype={
  6. init:function(){//在初始化原型方法中返回例項的引用
  7. this.length=0;
  8. this.test=function(){
  9. returnthis.length;
  10. }
  11. returnthis;
  12. },
  13. jquery:"1.3.2",//原型屬性
  14. length:1,
  15. size:function(){//原型方法
  16. returnthis.length;
  17. }
  18. };
  19. jQuery.fn.init.prototype=jQuery.fn;//使用jQuery的原型物件覆蓋init的原型物件
  20. alert($().jquery);//返回"1.3.2"
  21. alert($().test());//返回0
  22. alert($().size());//返回0
  23. </script>

這是一招妙棋,new jQuery.fn.init() 建立的新物件擁有 init 構造器的 prototype 原型物件的方法,通過改變 prototype 指標的指向,使其指向 jQuery 類的 prototype ,這樣建立出來的物件就繼承了 jQuery.fn 原型物件定義的方法。

2.2.5 成熟 -- 選擇器

jQuery 返回的是 jQuery 物件,jQuery 物件是一個類陣列的物件,本質上它就是一個物件,但是它擁有陣列的長度和下標,卻沒有繼承陣列的方法。

很顯然,上面幾節的講解都是建立在一種空理論基礎上的,目的是希望讀者能夠理解 jQuery 框架的核心構建過程。下面,我們就嘗試為 jQuery() 函式傳遞一個引數,並讓它返回一個 jQuery 物件。

jQuery() 函式包含兩個引數 selector 和 context ,其中 selector 表示選擇器,而 context 表示選擇的內容範圍,它表示一個 DOM 元素。為了簡化操作,我們假設選擇器的型別僅限定為標籤選擇器。實現的程式碼如下所示。

  1. <div></div>
  2. <div></div>
  3. <div></div>
  4. <scripttype="text/javascript">
  5. var$=jQuery=function(selector,context){//定義類
  6. returnnewjQuery.fn.init(selector,context);//返回選擇器的例項
  7. };
  8. jQuery.fn=jQuery.prototype={//jQuery類的原型物件
  9. init:function(selector,context){//定義選擇器構造器
  10. selector=selector||document;//設定預設值為document
  11. context=context||document;//設定預設值為document
  12. if(selector.nodeType){//如果選擇符為節點物件
  13. this[0]=selector;//把引數節點傳遞給例項物件的陣列
  14. this.length=1;//並設定例項物件的length屬性,定義包含的元素個數
  15. this.context=selector;//設定例項的屬性,返回選擇範圍
  16. returnthis;//返回當前例項
  17. }
  18. if(typeofselector==="string"){//如果選擇符是字串
  19. vare=context.getElementsByTagName(selector);//獲取指定名稱的元素
  20. for(vari=0;i<e.length;i++){//遍歷元素集合,並把所有元素填入到當前例項陣列中
  21. this[i]=e[i];
  22. }
  23. this.length=e.length;//設定例項的length屬性,即定義包含的元素個數
  24. this.context=context;//設定例項的屬性,返回選擇範圍
  25. returnthis;//返回當前例項
  26. }else{
  27. this.length=0;//否則,設定例項的length屬性值為0
  28. this.context=context;//設定例項的屬性,返回選擇範圍
  29. returnthis;//返回當前例項
  30. }
  31. },
  32. jquery:"1.3.2",//原型屬性
  33. size:function(){//原型方法
  34. returnthis.length;
  35. }
  36. };
  37. jQuery.fn.init.prototype=jQuery.fn;//使用jQuery的原型物件覆蓋init的原型物件
  38. alert($("div").size());//返回3
  39. </script>
在上面示例中,$("div") 基本擁有了 jQuery 框架中 $("div") 語法的功能,使用它可以選取頁面中指定範圍的 div 元素。同時,呼叫 size() 方法可以返回 jQuery 物件集合的長度。

相關文章