從零開始學習jQuery框架

摘要: 本文由簡到繁地介紹了以jQuery作爲藍本的js框架開發步聚, 希望藉助本文大家對jQuery這樣的框架內部有一個大致的認識。

隨着時代發展,javascript陣營裏面出現了越來越多的優秀的框架,大大簡化了我們的開發工作,在我們使用這些框架的時候是不是也應該飲水思源想想它們都是怎樣構建起來的呢?如果你不滿足於僅僅是使用一些現成的API,而是深入瞭解它們內部的實現機制(照某人的說法, API是貶值最快的東西),最好的辦法就是閱讀它們的源代碼了,前提是你讀得懂。

最近兩天研究了一下jQuery的源碼,在這裏將本人一些粗淺認識分享出來,不當之處請各位指正。好了,下面我們就來看看jQuery大概是怎樣工作的,我假定你已經具備了一些基本的javascript知識,如果基礎不夠俺推薦你閱讀《JavaScript高級程序設計》和《悟透JavaScript》這兩本書。本文不適合對js裏面的類、對象、函數、prototype等概念沒有了解的朋友。

我們從最開始的說起:
首先構造一個對象給使用者,假定我們這個框架叫 Shaka   ( 俺的名字;) )
var Shaka = function(){}; 這裏我們創建了一個空函數,裏面什麼也沒有,這個函數實際上就是我們的構造函數。爲了讓我們生成的對象能夠調用在prototype裏定義出來的方法, 我們需要用原型的方式(把Shaka當作是一個類)給Shaka添加一些方法,於是定義::
Shaka.fn =  Shaka.prototype = {}; 這裏的Shaka.fn相當於Shaka.prototype的別名,方便以後使用,它們指向同一個引用。

OK,我們添加一個sayHello的方法, 給Shaka添加一個參數,這樣這個框架最基本的樣子已經有了,如果它有生命的話那麼它現在是1歲, 看代碼:
<script type="text/javascript">
var Shaka = function(age){ this.age = age; };
Shaka.fn = Shaka.prototype = { sayHello: function() { alert('I am a little baby, my age is ' + this.age + ' years old.'); }};
var babyShaka = new Shaka(1);
babyShaka.sayHello();
</script>

好啦,先別激動, 我們注意到這個框架跟jQuery在使用上是有一些差別的, 比如在jq 中我們可以這樣寫
jQuery('#myid').someMethod(); 這是怎樣做到的呢, 也就是說 jQuery()這個構造函數返回了一個jQuery的對象實例,因此我們可以在上面調用它的方法,所以Shaka的構造函數應該返回一個實例,它看起來應該是這個樣子:
var Shaka = function(){ return //返回Shaka的實例; }; 那麼我們要如何取得一個Shaka的實例呢, 我們先來回顧一下使用prototype方式來模擬類的時候 var someObj = new  MyClass(); 這個時候實際上是創建一個新對象someObje,把新對象作爲this指針,調用 MyClass函數,即類的構造函數, 然後 someObj 就獲得了在 MyClass.prototype裏面定義的方法, 這些方法內的this指針指當前對象實例。

在jQuery中使用了一個工廠方法來創建一個實例,這個方法位於jQuery.prototype中, 現在我們重新來定義Shaka.prototype, 給它添加一個init方法用於返回一個Shaka的實例, 並且把Shaka的構造函數稍稍改變一下:

var Shaka = function(age) { return new Shaka.fn.init(age); };

Shaka.fn = Shaka.prototype = {
       init: function(age) { this.age = age; return this; },
       sayHello: function() { alert('I am a little baby, my age is ' + this.age + ' years old.'); }
};

Shaka.fn.init.prototype = Shaka.fn;//這裏new Shaka.fn.init(age)創建的對象具有init方法的prototype指向對象的方法 , 因此我們將init方法的prototype指向 Shaka的prototype, 這樣創建出來的對象就具有了Shaka.prototype裏面定義的方法。

OK,現在我們的小寶寶變成大一點的寶寶了,打個招呼先:
<script type="text/javascript">
var Shaka = function(age) { return new Shaka.fn.init(age); };
Shaka.fn = Shaka.prototype = {
 init: function(age) { this.age = age; return this; },
 sayHello: function() { alert('I am a little big baby, my age is ' + this.age + ' years old.'); }
};
Shaka.fn.init.prototype = Shaka.fn;
Shaka(2).sayHello();
</script>

嗯,好象有點樣子了,但是光這樣還不行,來點實際的, 我們在新框架中實現jquery裏val()方法的部分功能,這個方法不加參數調用時返回指定ID的input的值,加參數時爲設定這個input的值,與jQery一樣,我們約定使用id來查找對象時使用"#"符號。把要查找的目標ID作爲構造函數的參數傳進去,我們給Shaka.prototype添加一個val()方法, 給Shaka添加一個selector的屬性用於存儲我們要查找的目標。:
Shaka.fn = Shaka.prototype = {
       init: function(selector) { this.selector = selector; return this; },
       val: function(newValue) { //方法實現代碼 }
};
var Shaka = function(selector) { return new Shaka.fn.init(selector); };

<form method="post" action="" name="myform">
 我幾歲了? <br />
<input id="myInput"  type="text" value="Hello world!" size="50" />
</form>
<script type="text/javascript">
var Shaka = function(selector) { return new Shaka.fn.init(selector); };
Shaka.fn = Shaka.prototype = {
 init: function(selector) { if(selector) this.selector = selector; return this; },
 val: function(newValue) {
 //start val function body
  if(!(this.selector && this.selector.indexOf('#') == 0 && this.selector.length != 1))
   return; //簡單地判斷傳入值非法, 最好使用正則
  var id = this.selector.substring(1);
  var obj = document.getElementById(id);
  if(obj)//如果對象存在
  {
   if(newValue == undefined)
    return obj.value;//獲取目標對象的值.
   obj.value = newValue;// 將目標對象的value屬性設置爲newValue.
   return this; //爲了使方法可以連續調用。
  }
 //end val function body
 }
};
Shaka.fn.init.prototype = Shaka.fn;
alert('object old value is '+Shaka('#myInput').val());
alert(Shaka('#myInput').val('I am 3 years old now!').val());
</script>

到目前爲止我們已經創建一個可以工作的框架雛形,爲了使程序可以更方便地被調用,比如jQuery可以使用$符號來簡寫,我們也弄一個,在此之前我們先來回顧兩個東西:

1. 我們在腳本中可以這樣定義變量:
var foo = 'someThing';
bar = 'otherthing';
這樣兩種寫法都是合法的,但是意義完全不同, 第一個語句創建了一個新的變量,而第二個是定義了window對象的一個屬性,相當於window.bar = 'otherthing';, 因此我們想使我們的Shaka具有這樣的調用方式能力: $.someMethod();就需要將Shaka設置爲window的一個屬性, 於是我們的Shaka構造函數就得寫成這樣:
var Shaka = window.Shaka = window.$ = function(selector) { return new Shaka.fn.init(selector); };

2. javascript的匿名函數.
創建並執行一個匿名函數的基本形式: (function(){ alert('Hello World!'); })(); 爲什麼要用到匿名函數呢,因爲我們不想把Shaka的內部實現暴露出來,這樣容易與其它代碼衝突,只提供一個單一的入口,我們可以這樣測試一下:
<script type="text/javascript">
(function(){
function privateFunction(){ alert('You can not see me, haha'); };
})();
function publicMethod() {alert('I am public');};
alert('匿名函數內部的函數是不可訪問的, privateMethod 目前是: ' + typeof privateMethod);
alert('全局函數可訪, publicMethod 目前是: ' + typeof publicMethod);
</script>
然後,還有一個問題需要解決,俺們的框架做出來了但是還很簡陋,在這之前我們需要讓它與其它的框架協同工作,因此帶來一個問題, 如果我們都使用$作爲簡寫形式就會衝突了, 象jQuery一樣,我們需要提供一個noConfilit的方法“出讓”$的使用權。在我們的程序最開始處加入下面的代碼:
var _$ = window.$;
意思是將此前定義的$對象引用放到 _$ 中, 然後我們再給Shaka擴展一個方法出來, 如果其它開發者需要自行擴展的話也可以使用這個方式(jQuery的extend方法提供了更爲強大的功能,請大家自行研究):
(function($){ //extend method definition. })(Shaka);
意思是將Shaka作爲這個匿名函數的參數來調用這個方法。
前面我們講過 Shaka.fn 就是 Shaka.prototype 的別名,因此我們要在Shaka.prototype 裏面添加新的方法就可以寫成這樣
(function($){
       $.fn.noConflict = function(){
              window.$ = _$;//把$還給在開始處取得的引用.
       };
})(Shaka);

現在我們來看一個完整的:
<form method="post" action="" name="myform">
<h1> 我幾歲了?</h1> <br />
<input id="myInput"  type="text" value="Hello world!" size="50" />
<br /><br />
<input id="otherInput"  type="text" size="50" />
</form>
<script type="text/javascript">
//我們在這裏模擬一下在這之前如果加載了其它框架的情形, 這個時候window.$不爲空.
window.$ = { whoAmI: function(){ alert('This function result is from other js lib.');} };
(function(){ // 創建最外層匿名函數.
window._$ = window.$;//將別的框架定義的$暫存.
//給Shaka加上$ 的別名.
var Shaka = window.Shaka = window.$ = function(selector) { return new Shaka.fn.init(selector); };
Shaka.fn = Shaka.prototype = {
 init: function(selector) { if(selector) this.selector = selector; return this; },
 val: function(newValue) {
 //start val function body
  if(!(this.selector && this.selector.indexOf('#') == 0 && this.selector.length != 1))
   return; //簡單地判斷傳入值非法, 最好使用正則
  var id = this.selector.substring(1);
  var obj = document.getElementById(id);
  if(obj)//如果對象存在
  {
   if(newValue == undefined)
    return obj.value;//獲取目標對象的值.
   obj.value = newValue;// 將目標對象的value屬性設置爲newValue.
    return this; //爲了使方法可以連續調用, 返回當前實例。
  }
 //end val function body
 }
};
Shaka.fn.init.prototype = Shaka.fn;
})();
//擴展新的方法.
(function($){
 //alert(obj.fn);
 $.noConflict = function(){
  window.$ = window._$;//把$還給在開始處取得的引用.
 };
})(Shaka);
//如果沒有引入其它的框架,可以這麼寫
//alert('object old value is '+$('#myInput').val());
//alert($('#myInput').val('I am 3 years now!').val());
//強制使用完整名稱.
Shaka.noConflict();
alert('object old value is '+Shaka('#myInput').val());
alert(Shaka('#myInput').val('I am 5 years old now!').val());
//Shaka('#otherInput').val('這裏的值是使用Shaka(/'#otherInput/').val()方法來寫入的哦');
//或者可以這樣寫也行,仍然使用$, 把Shaka作爲匿名函數的參數$傳進去。
(function($){
 //又可以用$了, 哈哈
 $('#otherInput').val('這裏的值是使用Shaka(/'#otherInput/').val()方法來寫入的哦');
})(Shaka);
//現在仍然可以使用$調用其它框架的方法.
$.whoAmI();
</script>
現在好象不錯了,我們的Shaka baby已經5歲了;) , 當然這還只是個簡陋的東西,要實現健壯而強大的功能還需要付出很多努力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章