貧下中農版jQuery

謙行發表於2013-08-20

之前寫過一篇JavaScript名稱空間的文章,寫完後一對比對jQuery的簡單使用很是驚羨,看了看人家原始碼,用的原理很類似啊,改進一下之前的版本,做個簡易版的jQuery

之前的程式碼

(function () {
            var _NS = function () {
              
            }
         
            _NS.prototype.select = function (selector,context) {
                var context = context || document;
                return context.querySelectorAll(selector);
            }

            _NS.prototype.isArrayLike=function(obj){
                if(obj instanceof Array){
                    return true;
                }

                var length=obj.length;
                if ( obj.nodeType === 1 && length ) {
                    return true;
                }
                return false;
            }

            _NS.prototype.html = function (obj,value) {
                var isArray=this.isArrayLike(obj), i=0;

                if (typeof value == 'string') {
                    if (!isArray) {
                        obj.innerHTML = value;
                    } else {
                        var length = obj.length;
                        while (i < length) {
                            obj[i].innerHTML = value;
                            i += 1;
                        }
                    }
                } else {
                    if (!isArray) {
                        return obj.innerHTML;
                    } else {
                        return obj[0].innerHTML;
                    }
                }
            }

            window.NS = new _NS();
        })();

這樣的寫法只是對各種自定義方法的隔離,只是能用而已,不支援日趨流行的鏈式呼叫,使用jQuery的時候可以很方便的寫出$(selector).xxx().xxxx().xxxxxx() 這樣格式的程式碼,既簡潔易讀效率又高,而上面的寫法只能呼叫一個個孤零零函式,沒有物件整體性可言。

上面的select方法返回值為查詢的物件(IE低版本瀏覽器不支援),很多時候獲取一個物件後我們希望使用庫函式對其直接進行操作,比如我們希望把頁面上所有div的innerHTML設為test,並隱藏這些div,如果用jQuery會這麼寫

$(div).html('test').css(‘display’,’none’);

上面的程式碼雖然沒實現css方法,但是如果有的話得這麼寫

var divs=NS.select('div');
divs.html('test');
divs.css('display','none');

為什麼jQuery很方便

jQuery好用有幾個原因:

1. $本身是個function物件,包含一些“靜態方法”(不用例項化就可以用的方法),比如$.ajax、$.animation,可以這樣$.xxx()直接使用jQuery的一些庫函式

2. 因為$本身是一個函式,可以被呼叫。但是$(selector) 返回結果並不是搜尋的結果集,而是一個jQuery例項,結果集被封裝在jQuery物件內,這樣可以使用一些jQUery的例項方法(也就是定義在prototype內的方法等),例如$(‘div’).html(‘test’), 這樣由於$(‘div’)返回的是jQuery例項,所以可以呼叫例項方法html()。

3. jQuery物件大部分例項方法儘量返回jQuery物件,即呼叫者本身,這樣可以支援鏈式呼叫,比如$(‘div’).html(‘test’).css(‘display’,’none’) , $(‘div’)返回jQuery物件,裡面包含結果集,呼叫例項方法html(‘test’) 同樣返回jQuery物件,呼叫 css(‘display’,’none’) 同樣也返回jQuery物件,可以這樣一直呼叫下去。

建構函式的一些知識

想要做到上面幾點除了prototype等基本知識,還需要了解一些關於JavaScript建構函式的知識。

1.什麼樣的函式是建構函式

在JavaScript的世界裡建構函式並不神祕,也不特殊,任何函式通過new 操作符呼叫都可以變為建構函式,不使用new 操作符就不是建構函式,而是直接按普通函式呼叫。

2.建構函式返回什麼樣的結果

建構函式的返回值分為兩種情況,當function沒有return語句或者return回一個基本型別(bool,int,string,undefined,null)的時候,返回new 建立的一個匿名物件,該物件即為函式例項;如果function體內return一個引用型別物件(Array,Function,Object等)時,該物件會覆蓋new建立的匿名物件作為返回值。

寫個小例子驗證一下

function A(){
                return true;
            }
            
            var a=new A();
            console.log(a instanceof A); //true
            
            function B(){
                return new Array();
            }
            
            var b=new B();
            console.log(b instanceof Array); //true

做個貧下中農版的jQuery

針對上面講的jQuery的幾點好處,嘗試寫一個版本,為了更像jQuery一些,把返回函式名字也改為$,先寫一個框架

version 0.1

(function(){
                var $=function(selector,context){
                    
                };
                
                $.ajax=function(configs){ //靜態方法
                    //TODO
                } 
                
                $.prototype.html=function(value){ //例項方法
                    //TODO
                }
                
                window.$=$;
            })();

關於靜態方法和例項方法的部分很好實現,就寫成類似version 0.1的樣子就行,在這個版本中$確實是個函式了,但是怎麼讓$(selector)執行返回$的例項,同時例項內又包含搜尋結果。嘗試寫下一個版本

version0.2

(function(){
                var $=function(selector,context){
                    var context = context || document; 
                    var nodeList = context.querySelectorAll(selector); 
                    
                    var $=new $();
                    $.elements=nodeList;
                    return $;
                };
                
                $.ajax=function(configs){ //靜態方法
                    //TODO
                } 
                
                $.prototype.html=function(value){ //例項方法
                    //TODO
                }
                
                window.$=$;
            })();

這個看起來可以,$是個函式,有一些靜態方法,$(selector)返回$例項,包含搜尋結果集,但是這種方式有語法上的錯誤,程式碼中試圖在$function體內new自己,在執行到new的時候JavaScript還不認識$,失敗!

可以嘗試換一個思路 version 0.3

(function(){
                function f(selector,context){
                    return ?;
                }
                
                var $=(function(){
                    return f;
                })();
                window.$=$;
            })();

這樣$同樣也是個函式,但是怎麼才能讓其執行結果返回本身的例項呢,也就是f到底應該怎樣返回$的例項呢,說了這麼多遍例項終於想起除了直接new一個物件可以得到其例項,還有一個地方可以得到其例項,在prototype中定義的函式可以訪問this物件並返回。這就要求在f的prototype函式內返回this,不斷的改啊調啊終於成了這樣

version 0.4

(function(){
                var $=(function(){    
                    function f(selector,context){
                        return f.prototype.init(selector,context);
                    }
                    
                    f.prototype.init=function(selector,context){
                        var context = context || document; 
                        var nodeList = context.querySelectorAll(selector); 
                        this.length = nodeList.length; 
                        this.elements=new Array();
                        for (var i = 0; i < this.length; i++) { 
                            this.elements[i] = nodeList[i]; 
                        } 
                        return this; 
                    }
                        
                    return f;
                })();
                
                window.$=$;
            })();

填上剛才自定義的函式

version 1.0

(function(){
                var $=(function(){    
                    function f(selector,context){
                        return f.prototype.init(selector,context);
                    }
                    
                    f.ajax=function(configs){
                        //TODO
                    }
                    
                    f.prototype.init=function(selector,context){
                        var context = context || document; 
                        var nodeList = context.querySelectorAll(selector); 
                        this.length = nodeList.length; 
                        this.elements=new Array();
                        for (var i = 0; i < this.length; i++) { 
                            this.elements[i] = nodeList[i]; 
                        } 
,                        return this; 
                    }
                    
                    f.prototype.html=function(value){
                        //TODO
                    }
                        
                    return f;
                })();
                
                window.$=$;
            })();

這樣終於所有要求都實現了,在內部匿名函式中定義function f,最後返回賦值給$,這樣$是個函式,在執行的時候層層呼叫,最後呼叫到f.prototype.init,並返回其返回物件(好繞口),在init中把搜尋結果放到this的屬性中,最後返回this,然後f在把this返回,這樣$(selector)的結果是$物件例項,而且包含搜尋結果。

jQuery原始碼結構

上面的結果已經很讓人滿意了,仔細讀了讀jQuery原始碼,看看jQuery結構

(function( window, undefined ) {
   
    var jQuery = (function() {
       // 構建jQuery物件
       var jQuery = function( selector, context ) {
           return new jQuery.fn.init( selector, context, rootjQuery );
       }
   
       // jQuery物件原型
       jQuery.fn = jQuery.prototype = {
           constructor: jQuery,
           init: function( selector, context, rootjQuery ) {
              // selector有以下7種分支情況:
              // DOM元素
              // body(優化)
              // 字串:HTML標籤、HTML字串、#id、選擇器表示式
              // 函式(作為ready回撥函式)
              // 最後返回偽陣列
           }
       };
   
      //把jQuery的prototype賦值給init方法的prototype
       jQuery.fn.init.prototype = jQuery.fn;
   
       // 合併內容到第一個引數中,後續大部分功能都通過該函式擴充套件
       // 通過jQuery.fn.extend擴充套件的函式,大部分都會呼叫通過jQuery.extend擴充套件的同名函式
       jQuery.extend = jQuery.fn.extend = function() {};
      
       // 在jQuery上擴充套件靜態方法
       jQuery.extend({
           // ready bindReady
           // isPlainObject isEmptyObject
           // parseJSON parseXML
           // globalEval
           // each makeArray inArray merge grep map
           // proxy
           // access
           // uaMatch
           // sub
           // browser
       });

       return jQuery;
   
    })();
   
    window.jQuery = window.$ = jQuery;
})(window);

總體上是一致的,但是jQuery的結構要科學很多

1.將window物件傳入匿名函式,使匿名函式內部可以直接訪問,防止匿名函式內部使用window物件的時候需要層層查詢作用域鏈,最後才能找到window

2. 沒有一棍子打死,完全使用$,當出現$命名衝突的時候可以使用jQuery代替

3. 定義jQuery.fn=jQuery.prototype,程式碼寫起來方便了很多,也有利於壓縮

4. 沒有使用elements屬性,而是利用陣列特性封裝搜尋結果集,在使用的時候更容易想到

5. 定義each函式用於遍歷結果集

6. 提供extend函式用於向物件內部新增屬性

窮人版jQuery Version2.0

看了大師的寫法終於可以脫離貧下中農了

(function () { 
                var $ = (function () { 
                    var $ = function (selector, context) { 
                        return new $.prototype.init(selector, context); 
                    }

                    $.prototype.init = function (selector, context) { 
                        var context = context || document; 
                        var nodeList = context.querySelectorAll(selector); 
                        this.length = nodeList.length; 
                        for (var i = 0; i < this.length; i++) { 
                            this[i] = nodeList[i]; 
                        } 
                        return this; 
                    }

                    $.prototype.each = function (callback, args) { 
                        var length = this.length, i = 0; 
                        if (args) { 
                            while (i < length) { 
                                callback.call(this[i], args); 
                                i += 1; 
                            } 
                        } else { 
                            while (i < length) { 
                                callback.call(this[i]); 
                                i += 1; 
                            } 
                        } 
                        return this; 
                    }

                    $.prototype.html = function (value) { 
                        if (typeof value == 'string') { 
                            this.each(function () { 
                                this.innerHTML = value; 
                            }); 
                            return this; 
                        } else { 
                            return this[0].innerHTML; 
                        } 
                    }

                    $.prototype.init.prototype = $.prototype;

                    return $; 
                })();

                window.$ = $; 
            })();

最後

本文就是在總結我試圖實現jQuery的過程,思緒結構有些混亂,希望不要誤導讀者。在讀了很多次jQuery原始碼,加上網上很多部落格解析才一步步把jQuery看清,jQuery的設計非常巧妙,常人很難一次想到實現方式,而且非常有前瞻性,任何時候都在使用jQuery名稱空間或jQuery例項,防止了與未來JavaScript原生API的衝突,多研究研究受益匪淺。

相關文章