仿照jquery封裝一個自己的js庫(一)


所謂造輪子的好處就是復習知識點,加深對原版jquery的理解。
本文系筆者學習jquery的筆記,記述一個名為"dQuery"的初級版和縮水版jquery庫的實現。主要涉及知識點包括面向對象,jquery,綁定,腳本化css等。


一. jquery的美元符意味什么?

先思考alert(typeof $)的結果中的$,它是一個對象,也是一個函數。
所以美元符字面上是jQuery,其實就是一個jq對象,里面可以接受函數,字符串(#xxx,.xxx,xxx...),還有一種是對象(比如this)。

dQuery是基於面向對象實現的,所以需要先寫構造函數。為了方便遍歷和其它方法操作,所有內容返回到一個數組中。這個數組依附於dQuery對象存在,之后將為這個dquery對象屬性添加方法。

function dQuery(vArg){//參數是變體變量
    this.elements=[];//選擇器選擇的元素扔到這個數組中
}

因為新的對象可以接受三種類型的參數(字符串,對象,函數)。作為一個變體變量,根據匈牙利命名法可以用vXxx命名,同時需要分類討論變量的情況:

1.當參數為函數時,執行函數——window.onload.——不好。

jquery寫作時,經常允許多個$(function()),但是寫window.onload會導致最終只執行最后一個。這時需要給函數綁定window.onload事件。
綁定事件的函數如下:

function myAddEvent(obj,sEv,fn){
    if(obj.attachEvent){
        obj.attachEvent('on'+sEv,fn);
    }else{
        obj.addEventListener(sEv,fn,false);
    }
}

所以,當為id選擇器時,查找:document.getElementById(vArg.substring(1));並把它加到this.elements中去。

2.當參數為字符串時,執行選擇器操作.

這里需要對id選擇器,類選擇器,元素選擇器進行判斷。
注意:把選擇器結果丟到一個數組(dQuery.elements)中去。方便遍歷事件。
用charAt進行判斷參數首位:(#,.或是其它)

(1)首位為#時:

執行document.getElementById,同時把這個字符串稍作處理,用substring(1)方法把首個字符給去掉。

(2)首位為.

選取class類。可以設計一個getByClass函數

function getByClass(oParent,sClass){
    var aEle=oParent.getElementsByTagName('*');//選擇父元素的所有元素
    var aResult=[];
    var re=new RegExp('\\b'+sClass+'\\b','i');//正則邊界
    var i=0;
    for(i=0;i<aEle.length;i++){
        if(re.test(aEle[i].className)){
            aResult.push(aEle[i]);
        }
    }
    return aResult;
}

在本實例中,oParent父級對象實際上就是document,另一方面,因為getByClass返回的對象實際是一個數組,所以:

this.elements=getByClass(document,vArg.substring(1));
(3)參數為對象時,返回一個對象本身

把這個對象push到

(4)綁定基本的事件——比如點擊事件

在接下來就是綁定事件。比如click(function(){}),它是一個回調函數。需要用到原型(prototype)方法。

//對選擇器函數綁定click事件
dQuery.prototype.click=function(fn){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        myAddEvent(this.elements[i],'click',fn);
    }
}
(5)簡寫設置

到目前為止,在調用這個dQuery時,每次都需要這樣寫:

new dQuery(function(){
    new dQuery('input').click(function(){
        alert('a');
    })
});

每次都要使用new,不用就會出問題。
如果我想使用類似jquery的簡寫方式,使用$d作為簡寫,可以通過一個函數來定義:

function $d(vArg){
    return  new dQuery(vArg);
}

所以當前的最終代碼為:

//可重復調用的加載函數
function myAddEvent(obj,sEv,fn){
    if(obj.attachEvent){
        obj.attachEvent('on'+sEv,fn);
    }else{
        obj.addEventListener(sEv,fn,false);
    }
}

//class選擇器調用函數
function getByClass(oParent,sClass){
    var aEle=oParent.getElementsByTagName('*');//選擇父元素的所有元素
    var aResult=[];
    var re=new RegExp('\\b'+sClass+'\\b','i');//正則邊界
    var i=0;
    for(i=0;i<aEle.length;i++){
        if(re.test(aEle[i].className)){
            aResult.push(aEle[i]);
        }
    }
    return aResult;
}

//定義dQuery對象
function dQuery(vArg){//參數是變體變量
    this.elements=[];//選擇器選擇的元素扔到這個數組中
    switch(typeof vArg){
        //如果參數是函數
        case 'function':
            myAddEvent(window,'load',vArg);
            break;
        //如果參數是字符串
        case 'string':
            switch(vArg.charAt(0)){
                case '#'://id選擇器參數應該為#號之后的字符段
                    var obj=document.getElementById(vArg.substring(1));
                    this.elements.push(obj);
                break;

                case '.'://class
                    this.elements=getByClass(document,vArg.substring(1));
                    break;

                default://標簽
                    this.elements=document.getElementsByTagName(vArg);
            }
            break;
        //如果參數是對象。
        case 'object':
            this.elements.push(vArg);

    }
}

//對選擇器函數綁定click事件
dQuery.prototype.click=function(fn){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        myAddEvent(this.elements[i],'click',fn);
    }
}

function $d(vArg){
    return  new dQuery(vArg);
}
(6)小結
  • 通過element屬性儲存被選中的元素
  • 判斷參數類型,以此做出不同的判斷
  • 對於參數是函數的情況,需要進行綁定window.onload。
  • 對象則直接插入

二. 添加事件

1. 之前已經用原型方法的加上了click事件,現在研究下show/hide的實現:

從最簡單的display——none/block結果開始寫起:

//對選擇器函數綁定show/hide事件
dQuery.prototype.show=function(){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        this.elements[i].style.display='block';
    }
}

dQuery.prototype.hide=function(){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        this.elements[i].style.display='none';
    }
};

實現功能已經沒有問題了。

2.hover的實現:

(1)hover有兩個參數,第一個是鼠標移入函數,第二個是鼠標移出函數。實際上跟click事件一樣:
dQuery.prototype.hover=function(fnover,fnout){
    var i=0;
    for(i=0;i<this.elements.length;i++){
        //給這個對象一次性綁定兩個事件
        myAddEvent(this.elements[i],'mouseover',fnover);
        myAddEvent(this.elements[i],'mouseout',fnout);
    }
};

就調用一般的alert方法等已經沒有問題了,接下來要用類似jquery中css的寫法定義它的樣式

(2)css有兩種調用:設置樣式,只有一個參數時獲取樣式。總之參數不定(1或2個,attr、value)。只有兩個參數時很簡單。只需要做個循環,把value值賦給相應的樣式屬性就可以了:
this.elements[i].style[attr]=value;

當只有一個參數attr時,獲取的樣式可能不是唯一的。這時可以參考jquery的css方法定義:獲取的是第一個元素的樣式——如果你用xxx.style.樣式的方法——獲取的僅僅是行間樣式,那這就麻煩了。需要再引入一個兼容性良好的樣式獲取函數getStyle。

function getStyle(obj,attr){
    //元素,樣式
    if(obj.currentStyle){//兼容ie9及以下瀏覽器
        return obj.currentStyle[attr];
    }else{
        return getComputedStyle(obj,false)[attr];
    }
}
//css方法
dQuery.prototype.css=function(attr,value){
    if(arguments.length==2){//當參數個數為2時,使用設置css的方法
        var i=0;
        for(i=0;i<this.elements.length;i++){
            this.elements[i].style[attr]=value;
            
        }
    }else{//只有一個參數時獲取樣式
        return getStyle(this.elements[0],attr);
    }
};

實驗證明,傳入background無效,但是對具體的background-color無效。

(3)改進:this

雖然說當dQuery對象的參數為一個對象時,返回的就是對象本身。但是有時候在使用this的時候(ie),this指向了其它的對象(window對象)。
不能用this的情形——行間,定時器,綁定。js庫中,實際上是綁定處理的。在此需要了解call方法。因此需要對myAddEvent函數進行修改。


call方法
應該知道,jquery中充滿了各種簡寫。比如show,全稱為show.call。比如下面這個例子:

function show(){
    alert(this);
}
show();
show.call();

這個show函數的彈出結果為:window對象。show.call()彈出結果也是window對象,但call可以加參數,加了參數之后,this返回的值為參數——show.call('abc');彈出結果為abc。
進一步,給a,b傳入兩個參數:

function show(a,b){
    alert('this是:'+this+',a是:'+a+',b是:'+b);
}
show(12,5);
show.call(12,5);

調用show函數的第一個彈出結果是:

調用show.call(12,5)彈出:

這說明call方法中的第一個參數默認指向this。比如要調用可以寫成show('abc',12,5)
與call類似,apply也類似——
apply('abc',[12,5])的結果和show('abc',12,5)完全一樣。


知道了call方法,回到myAddEvent函數的修改上來,應該讓fn返回的主題回到obj——

function myAddEvent(obj,sEv,fn){
    if(obj.attachEvent){
        obj.attachEvent('on'+sEv,function(){
            fn.call(obj);//兼容ie
        });
    }else{
        obj.addEventListener(sEv,fn,false);
    }
}

這樣一來,就可以使用$d(this)了。

3.toggle方法

toggle方法是舊jquery中很有用的點擊處理方法。可以傳多個事件處理函數。每一次觸發,就按順序執行。調用形式如下:

$d(function(){
    $d('#btn1').click(function(){
        $d('#div1').toggle(function fn1(){

        },function fn2(){

        },function fn3(){
            
        }...);
    });

});

怎么模擬這個函數呢?——

(1)需要一個計數器,根據計數的結果選擇執行哪個函數。

toggle可以傳任意個參數。所以參數干脆不加了。


在計數器的實現過程中會出現問題。

window.onload=function (){
    var aBtn=getElementsByTagName('button');
	var i=0;
	var count=0;//計數器
	
	for(i=0;i<aBtn.length;i++){
	    aBtn[i].onclick=function(){
		    alert(count++);
		}
	}
}

如果我有多個按鈕需要觸發toggle內的事件,但是按照dQuery先前的方法,必然導致所有按鈕公用一套計數器。這樣一來效果就全亂了。怎么解決呢?
最好的辦法是,把計數過程存為一個函數,

window.onload=function (){
    var aBtn=getElementsByTagName('button');
	var i=0;
	
	function addClick(obj){
	    var count=0;//關鍵步驟:計數器塞進函數里了。
	    alert(count++);
	}
	
	for(i=0;i<aBtn.length;i++){
	    
	    aBtn[i].onclick=function(){
		    addClick(aBtn[i]);
		}
	}
}

或者(對閱讀不友好):

window.onload=function (){
    var aBtn=getElementsByTagName('button');
	var i=0;
	
	for(i=0;i<aBtn.length;i++){
	    (function(obj){
		    var count=0;//關鍵步驟:計數器塞進函數里了。
	        alert(count++);
	    })(aBtn[i]);
		}
	}
}

分別計數就這么實現了。調用函數幾次,就會產生多少個局部變量。也可以使用xxx.index這樣的方法。


現在要在toggle里面調用私有的計數器,思路也是把計數器放到一個聲明的函數里邊。

dQuery.prototype.toggle=function(){
    //私有計數器,計數器會被一組對象所享用。
    function addToggle(obj){
        var count=0;
        myAddEvent(obj,'click',function(){
            alert(count++);
        })
    }

    var i=0;
    for(i=0;i<this.elements.length;i++){
        addToggle(this.elements[i]);
    }
}

接下來是使用arguments來做,用以替換掉addToggle里邊的事件處理:讓計數器遞增的同時,選取它對參數長度取模的索引的參數,。並把本體call為參數obj(有點拗口)

myAddEvent(obj,'click',function(){
            arguments[count++%arguments.length].call(obj);
}

結果出錯。主要是arguments亂了,實際執行情況是,arguments是函數myAddEvent第三個參數(函數)的參數。因此,需要提前把toggle方法的arguments傳入一個數組。
完整toggle的的代碼是

//toggle方法:
dQuery.prototype.toggle=function(){
    var _arguments=arguments;//把toggle的arguments存起來,以便在其它函數中可以調用。
    
    //私有計數器,計數器會被一組對象所享用。
    function addToggle(obj){
        var count=0;
        myAddEvent(obj,'click',function(){
            _arguments[count++%_arguments.length].call(obj);
        })
    }

    var i=0;
    for(i=0;i<this.elements.length;i++){
        addToggle(this.elements[i]);
    }
}

接下來可以做一個簡單的示例驗證一下效果:
html

    <input type="button" id="btn1" value="顯示隱藏">
    <div id="div1" style="width:100px;height:100px;background:red;"></div>

javascript:

$d(function(){
    $d('input').toggle(function(){
        $d('#div1').hide();
    },function(){
        $d('#div1').show();
    })
});

效果如下

三. attr方法

attr管樣式,css管屬性。所以方法設置方面沒有太大區別。

//attr方法和css方法類似。
dQuery.prototype.attr=function(attr value){
    if(arguments.length==2){//設置屬性
        var i=0
        for(i=0;i<this.elements.length;i++){
            this.elements[i][attr]=value;
        }
    }else{//獲取屬性
        return this.elements[0][attr];
    }
}

沒有問題。

四.獲取與查找(eq,find,index)

1.eq方法跟數組索引差不多

在jquery中,$('.class1').eq(1)表示選取選擇器的第2個對象。現在模擬jquery的取法。
在dQuery中,很自然想到用return this.elements[n];這樣的代碼選取。問題在於:返回的是一個原生的js對象,因此無法調用dQuery方法。因此需要把this.elements[n]這個原生對象轉化為dQuery對象。所以——

//eq選擇器
dQuery.prototype.eq=function(n){
    return new dQuery(this.elements[n]);
}

這樣保證了返回出來的對象是可鏈的。

2.find方法

find方法在jquery中是用於篩選。比如$('ul').find('li')表示在所有ul標簽下選擇li,而ol標記下的li不會被選擇。此方法有一個父級,find里面,可能有class類,可能有div類,不可能有id類(為什么不直接用id選擇器呢?所以不予考慮了)。
因為返回的是一個較為復雜的對象——它是依賴於dQuery對象的一個數組(aResult)。因此具體做法和定義dQuery的object類似。需要分類討論:

//find選擇器
dQuery.prototype.find=function(str){
    var i=0;
    var aResult=[];//存放臨時數據

    for(i=0;i<this.elements.length;i++){
        switch(str.charAt(0)){
            case '.'://class類
                var aEle=getByClass(this.elements[i],str.substring(1));
            aResult.concat(aEle);//橋接到aResult內。
            break;

            default://其它標簽名(TagName)
                var aEle=this.elements[i].getElementsByTagName(str);

                aResult.concat(aEle);
        }
    }
    var newdQuery=new dQuery();
    newdQuery.elements=aResult;
    return newdQuery;//保持可鏈。
}

這段代碼有很大問題,aResult本是一個數組,但是this.elements[i].getElementsByTagName(str)是一個html collection,是一個長得很像數組的集合。因此concat方法不適用。——concat不能用,但是push可以用。為了達到橋接的效果,可以人工定義一個小函數來實現。所以全部帶代碼是

//find選擇器
//定義一個小函數,兩個數組(元素集合),把兩個類數組(html元素集合)合並在一塊。
function appendArr(arr1, arr2){
    var i=0;

    for(i=0;i<arr2.length;i++){
        arr1.push(arr2[i]);
    }
}

dQuery.prototype.find=function(str){
    var i=0;
    var aResult=[];//存放臨時數據

    for(i=0;i<this.elements.length;i++){
        switch(str.charAt(0)){

            case '.'://class類
                var aEle=getByClass(this.elements[i],str.substring(1));
            aResult.concat(aEle);//橋接到aResult內。但是
            break;

            default://其它標簽名(TagName)
                var aEle=this.elements[i].getElementsByTagName(str);
                appendArr(aResult,aEle);
        }
    }
    var newdQuery=new dQuery();
    newdQuery.elements=aResult;
    return newdQuery;//保持可鏈。
}

3.index方法

jquery的原意是——獲取一組同輩元素內,一組元素的索引值。
比如

<span></span>
<div id="div1"></div>
<div></div>
<div></div>
$(funciton(){
    $('div').click(function(){
	    alert($(this).index());
	})
})

在上面的過程中,點擊#div1,彈出的結果為1,而不是0(把span算進索引0了)。簡單點說就是:如何求某個元素在同級中的位置。
下面實現這段代碼。
首先,使用index方法的必然是一個單獨的對象,而不能是一個集合或數組。所以,必須指定this.elements[0]
一個人,比如說王五必然是他爹的兒子,要找到他的兄弟,只能通過他爹來找。接下來可以有頭到尾遍歷判斷,假設老王的第五個兒子名字是王五,那么就可以判斷,王五的索引值為4.同理,js用parentNode和child進行判斷。

//獲取索引值函數
function getIndex(obj){
    var aBrother=obj.parentNode.children;
    var i=0;
    
    for(i=0;i<aBrother.length;i++){
        if(aBrother[i]==obj){
            return i;
        }
    }
}

接下來要做的就是把這個封裝好的函數加到原型函數內:

dQuery.prototype.index=function(){
    return getIndex(this.elements[0]);
}

那么,這個方法就算完成了。為了說明這個庫是實用的,接下來以一個案例說明。



【應用案例】選項卡的實現

html骨架

  <div id="tab">
      <ul class="list_group">
          <li><a class="active" href="javascript:;">1</a></li>
          <li><a href="javascript:;">2</a></li>
          <li><a href="javascript:;">3</a></li>
          <li><a href="javascript:;">4</a></li>
      </ul>
      <div class="box">
          <div class="content">
              <img alt="1" src="images/1.jpg">
          </div>
          <div class="content">
              <img alt="2" src="images/2.jpg">
          </div>
          <div class="content">
              <img alt="3" src="images/3.jpg">
          </div>
          <div class="content">
              <img alt="4" src="images/4.jpg">
          </div>
      </div>
  </div>

css

*{
    margin:0;
    padding: 0;
}
ul{
    list-style: none;
}
a{text-decoration: none;}

#tab{
    width: 400px;
    margin:100px auto;
}
.list_group li{
    float: left;
}
.list_group li a{
    display: block;
    width: 40px;
    line-height: 30px;
    text-align: center;
}
.box{
    clear: both;
}
.content{
    display: none;
}
.content img{
    border: 1px solid black;
    width: 400px;height: 300px;
}

在引用了dQuery的情況下,輸入下列js代碼:

$d(function(){
    $d('li').click(function(){
        var index=$d(this).index();

        $d('.content').hide();
        $d('.content').eq(index).show();
        $d('li').css('background-color','white');
        $d(this).css('background-color','#ccc');

    })
})


就成功實現了一個丑陋的選項卡。因為不是完全可鏈的。還有addClass等功能還有待進一步完善。


附錄

dQuery代碼

//可重復調用的加載函數
function myAddEvent(obj,sEv,fn){
    if(obj.attachEvent){
        obj.attachEvent('on'+sEv,function(){
            fn.call(obj);//兼容ie
        });
    }else{
        obj.addEventListener(sEv,fn,false);
    }
}

//class選擇器調用函數
function getByClass(oParent,sClass){
    var aEle=oParent.getElementsByTagName('*');//選擇父元素的所有元素
    var aResult=[];
    var re=new RegExp('\\b'+sClass+'\\b','i');//正則邊界
    var i=0;
    for(i=0;i<aEle.length;i++){
        if(re.test(aEle[i].className)){
            aResult.push(aEle[i]);
        }
    }
    return aResult;
}

//獲取計算后的樣式
function getStyle(obj,attr){
    //元素,樣式
    if(obj.currentStyle){//兼容ie9及以下
        return obj.currentStyle[attr];
    }else{
        return getComputedStyle(obj,false)[attr];
    }
}

//定義dQuery對象
function dQuery(vArg){//參數是變體變量
    this.elements=[];//選擇器選擇的元素扔到這個數組中
    switch(typeof vArg){
        //如果參數是函數
        case 'function':
            myAddEvent(window,'load',vArg);
            break;
        //如果參數是字符串
        case 'string':
            switch(vArg.charAt(0)){
                case '#'://id選擇器參數應該為#號之后的字符段
                    var obj=document.getElementById(vArg.substring(1));
                    this.elements.push(obj);
                break;

                case '.'://class
                    this.elements=getByClass(document,vArg.substring(1));
                    break;

                default://標簽
                    this.elements=document.getElementsByTagName(vArg);
            }
            break;
        //如果參數是對象。
        case 'object':
            this.elements.push(vArg);
            
    }
}

//定義簡寫
function $d(vArg){
    return  new dQuery(vArg);
}


//對選擇器函數綁定click事件
dQuery.prototype.click=function(fn){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        myAddEvent(this.elements[i],'click',fn);
    }
}

//對選擇器函數綁定show/hide事件
dQuery.prototype.show=function(){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        this.elements[i].style.display='block';
    }
}

dQuery.prototype.hide=function(){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        this.elements[i].style.display='none';
    }
};

//hover方法
dQuery.prototype.hover=function(fnover,fnout){
    var i=0;
    //對於返回器數組的內容
    for(i=0;i<this.elements.length;i++){
        //給這個對象一次性綁定兩個事件
        myAddEvent(this.elements[i],'mouseover',fnover);
        myAddEvent(this.elements[i],'mouseout',fnout);
    }
};

//css方法
dQuery.prototype.css=function(attr,value){
    if(arguments.length==2){//當參數個數為2時,使用設置css的方法
        var i=0;
        for(i=0;i<this.elements.length;i++){
            this.elements[i].style[attr]=value;
        }
    }else{//只有一個參數時獲取樣式
        return getStyle(this.elements[0],attr);
    }
};

//toggle方法:
dQuery.prototype.toggle=function(){
    var _arguments=arguments;//把toggle的arguments存起來,以便在其它函數中可以調用。

    //私有計數器,計數器會被一組對象所享用。
    function addToggle(obj){
        var count=0;
        myAddEvent(obj,'click',function(){
            _arguments[count++%_arguments.length].call(obj);
        })
    }

    var i=0;
    for(i=0;i<this.elements.length;i++){
        addToggle(this.elements[i]);
    } 
}


//attr方法和css方法類似。
dQuery.prototype.attr=function(attr,value){
    if(arguments.length==2){//設置屬性
        var i=0;

        for(i=0;i<this.elements.length;i++){
            this.elements[i][attr]=value;
        }
    }else{//獲取屬性
        return this.elements[0][attr];
    }
}

//eq選擇器
dQuery.prototype.eq=function(n){
    return new dQuery(this.elements[n]);
}


//find選擇器
//定義一個小函數,兩個數組(元素集合),把兩個類數組(html元素集合)合並在一塊。
function appendArr(arr1, arr2){
    var i=0;

    for(i=0;i<arr2.length;i++){
        arr1.push(arr2[i]);
    }
}

dQuery.prototype.find=function(str){
    var i=0;
    var aResult=[];//存放臨時數據

    for(i=0;i<this.elements.length;i++){
        switch(str.charAt(0)){

            case '.'://class類
                var aEle=getByClass(this.elements[i],str.substring(1));
            aResult.concat(aEle);//橋接到aResult內。但是
            break;

            default://其它標簽名(TagName)
                var aEle=this.elements[i].getElementsByTagName(str);
                appendArr(aResult,aEle);
        }
    }
    var newdQuery=new dQuery();
    newdQuery.elements=aResult;
    return newdQuery;//保持可鏈。
}

//獲取索引值函數
function getIndex(obj){
    var aBrother=obj.parentNode.children;
    var i=0;
    
    for(i=0;i<aBrother.length;i++){
        if(aBrother[i]==obj){
            return i;
        }
    }
}
dQuery.prototype.index=function(){
    return getIndex(this.elements[0]);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM