JavaScript實現DOM對象選擇器


目的:

根據傳入的選擇器類型選出第一個符合的DOM對象。

①可以通過id獲取DOM對象,例如 $("#adom");
②可以通過tagName獲取DOM對象,例如 $("a");
③可以通過樣式名稱獲取DOM對象,例如 $(".classa");
④可以通過attribute匹配獲取DOM對象,例如 $("[data-log]"),$("[data-time=2015]");
⑤可以通過層疊組合獲取DOM對象,例如 $("#adom .classa"); 
 
思路:
需要區分復合選擇還是單項選擇,單項選擇的話分別用各自的方法進行獲取,復合選擇的話就要進行篩選。
所以 第一步,區分是單項還是組合。
實現方法是將傳入選擇器的字符串轉換成數組,如果數組長度大於1的話,就是復合選擇。如果不是的話,再判斷是哪一種單項選擇器。
if(trim(selector).split(" ").length > 1){ //trim()方法用於去除字符串開頭和結尾的空白
//復合選擇器代碼
}
//判斷是哪一種單項選擇器

第二步,判斷是哪一種單項選擇器,然后進行篩選返回第一個元素。

①判斷,有兩種方法:

  • 方法一:用正則表達式。
if(/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
      //ID選擇器
}
if(/^((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
     //Tag選擇器
}
if(/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
     //class選擇器
}
if(/^\[[A-Za-z0-9_-\S]+\]$/.test(selector)){
    //屬性選擇器
}
  • 方法二:檢查傳入選擇器的第一個字符
var type=trim(selector).charAt(0);
switch(type){
    case ".":
        //class選擇器
    case "#":
        //id選擇器
    case "[":
        //屬性選擇器
    default:
        //tag選擇器
}

②根據選擇器進行篩選。

  • id和tag直接用DOM方法就可以了。
  • class的document.getElementsByClassName有兼容問題,需要為IE定義方法。
  • 屬性選擇器需要遍歷所有的DOM節點對象,選擇出符合條件的。
    //ID選擇器
    return document.getElementById(selector.slice(1,selector.length));
    //tag選擇器
    return document.getElementsByTagName(selector)[0];
    //類選擇器
    if(document.getElementsByClassName){
        return document.getElementsByClassName(selector.slice(1,selector.length))[0];
    }else{
        var nodes = document.all ? document.all : document.getElementsByTagName('*');
        for(var i=0;i<nodes.length;i++){
            var classes=nodes[i].className.split(/\s+/);
                if(classes.indexOf(selector.slice(1))!=-1){ //indexOf不兼容,需要在原型上擴展
                    return nodes[i];
                    break;
                } 
            }
        }    
    }
    //屬性選擇器
    if(/^\[[A-Za-z0-9_-\S]+\]$/.test(selector)){
        selector = selector.slice(1,selector.length-1);
        var eles = document.getElementsByTagName("*");
        selector = selector.split("=");
        var att = selector[0];
        var value = selector[1];
        if (value) {
            for (var i = 0; i < eles.length; i++) {
                if(eles[i].getAttribute(att)==value){
                    return eles[i];
                } 
            }
        }else{
            for (var i = 0; i < eles.length; i++) {
                if(eles[i].getAttribute(att)){
                    return eles[i];
                } 
            }
        }
    }

     

第三步,實現復雜選擇器。

  • 思路一:

最終篩選出的DOM對象一定是滿足最后一個選擇器的DOM對象集合之一,所以可以先選出這些對象,然后逐個檢查他的祖先元素,是否符合上一層選擇器,不符合的話就刪掉。一直迭代到最外一層選擇器,剩下的DOM對象集合中的第一個就是我們要找的DOM對象。

那么,如果有n個選擇器,就需要進行n-1輪篩選。

這里需要做兩件事情,①檢查元素的祖先元素是否是選擇器對象集合之一。②檢查對象集合中的每個元素,刪掉不符合條件的DOM對象。

定義兩個函數來做這兩件事:
//遞歸檢查ele的祖先對象是否符合選擇器
function isParent(ele,str){
    if (!isArray(str)) {        //如果不是數組
        str = toArray(str);   //轉換成數組
    }
    if (ele.parentNode) {
        if (str.indexOf(ele.parentNode)>-1) {
            return true;
        }else{
            return isParent(ele.parentNode,str); 
        }
    }else{
        return false;
    }
}
//從eles中刪掉祖先對象不符合選擇器的對象
function fliterEles(eles,str){
    if(!isArray(eles)){
            eles = toArray(eles);
    }
    for (var i = 0,len=eles.length;i<len; i++) {
        if (!isParent(eles[i],str)) {
            eles.splice(i,1);
            i = i - 1;
        }
    }
    return eles;
}

這個實現會有一個BUG,就是當HTML是下面這樣的時候,他會篩選出“第一個”,然而它並不是我們期待的。

 

雖然實際應用中很少會這樣給父元素和子元素定義相同的class名,但我們不能忽略這個BUG的存在。

這個實現的性能也是很差的,因為當他檢查對象集合中的一個對象的祖先元素是否符合一個選擇器時,他先檢查他的父元素,不滿足的話再檢查他父元素的父元素,一直到沒有父元素為止。然后他還需要檢查是否符合下一個選擇器,這樣他又遍歷了一遍他的父元素。這里有重復訪問的地方。

思路一的所有代碼:

//需要一個可以選擇所有元素的方法
function getElements(selector){
    //類選擇器,返回全部項
    if(/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
        if(document.getElementsByClassName){
            return document.getElementsByClassName(selector.slice(1,selector.length));
        }
        var nodes = document.all ? document.all : document.getElementsByTagName('*');
        var arr=[];  //用來保存符合的className;    
        for(var i=0;i<nodes.length;i++){
            if(hasClass(nodes[i],selector.slice(1,selector.length))){
                arr.push(nodes[i]);
            }
        }
        return arr;
    }

    //ID選擇器
    if(/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
        return document.getElementById(selector.slice(1,selector.length));
    }

    //tag選擇器
    if(/^((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
        return document.getElementsByTagName(selector);
    }

    //屬性選擇器
    if(/^\[[A-Za-z0-9_-\S]+\]$/.test(selector)){
        selector = selector.slice(1,selector.length-1);
        var eles = document.getElementsByTagName("*");
        selector = selector.split("=");
        var att = selector[0];
        var value = selector[1];
        var arr = []; 
        if (value) {
            for (var i = 0; i < eles.length; i++) {
                if(eles[i].getAttribute(att)==value){
                   arr.push(eles[i]);
                } 
            }
        }else{
            for (var i = 0; i < eles.length; i++) {
                if(eles[i].getAttribute(att)){
                    arr.push(eles[i]);
                } 
            }
        }
        return arr;
    }
}

//檢查ele的祖先對象是否符合選擇器
function isParent(ele,str){
    if (!isArray(str)) {
        str = toArray(str);
    }
    if (ele.parentNode) {
        if (str.indexOf(ele.parentNode)>-1) {
            return true;
        }else{
            return isParent(ele.parentNode,str); 
        }
    }else{
        return false;
    }
}

//從eles中刪掉祖先對象不符合選擇器的對象
function fliterEles(eles,str){
    if(!isArray(eles)){
            eles = toArray(eles);
    }
    for (var i = 0; i < eles.length; i++) {
        if (!isParent(eles[i],str)) {
            eles.splice(i,1);
            i = i - 1;
        }
    }
    return eles;
}


//DOM元素選擇器
function $(selector){
    if(!typeof selector === "string"){
        return false;
    }

    //復合選擇器
    if(trim(selector).split(" ").length > 1){
        var all = trim(selector).split(" ");
        var eles = getElements(all[all.length-1]);
        for(var i = 2 ; i < all.length+2 && all.length-i >=0; i++){
            eles = fliterEles(eles,getElements(all[all.length-i]));
        }
        return eles[0];
    }


    //ID選擇器
    if(/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
        return document.getElementById(selector.slice(1,selector.length));
    }


    //tag選擇器,只返回第一個
    if(/^((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
        return document.getElementsByTagName(selector)[0];
    }

    //類選擇器
    if(/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/.test(selector)){
        if(document.getElementsByClassName){
            return document.getElementsByClassName(selector.slice(1,selector.length))[0];
        }
        var nodes = document.all ? document.all : document.getElementsByTagName('*');
        for(var i=0;i<nodes.length;i++){
            if(hasClass(nodes[i],selector.slice(1,selector.length))){
                return nodes[i];
            }
        }    
    }


    //屬性選擇器
    if(/^\[[A-Za-z0-9_-\S]+\]$/.test(selector)){
        selector = selector.slice(1,selector.length-1);
        var eles = document.getElementsByTagName("*");
        selector = selector.split("=");
        var att = selector[0];
        var value = selector[1];
        if (value) {
            for (var i = 0; i < eles.length; i++) {
                if(eles[i].getAttribute(att)==value){
                    return eles[i];
                } 
            }
        }else{
            for (var i = 0; i < eles.length; i++) {
                if(eles[i].getAttribute(att)){
                    return eles[i];
                } 
            }
        }
    }
}

 

  • 思路二:

從最外層向里面篩選。

先從document選出符合最外層選擇器的對象集,目標對象一定是這個對象集的一個對象的子孫元素。
所以,遍歷這個對象集中的每個元素,從中選出符合第二個選擇器的對象集,然后再遍歷新的對象集。
直到篩選完最后一個選擇器,剩下的對象集中的第一個就是目標對象。
這個方法不需要區分符合選擇器和單項選擇器,也不需要重新定義獲得所有元素的方法。
function $(selector){
    var all=selector.split(/\s+/);
    var result = [],rooot=[document];
    for (var i = 0; i < all.length; i++) {
        var type=all[i][0];
        switch(type){
        //ID
        case "#" :
            for (var j = 0; j < rooot.length; j++) {
                var ele=rooot[j].getElementById(all[i].slice(1));
                if (ele) {
                    result.push(ele);
                }
            }
            break;
        
        //class
        case ".":
            for (var j = 0; j < rooot.length; j++) {
                if (document.getElementsByClassName) {
                    var eles=rooot[j].getElementsByClassName(all[i].slice(1));
                    if (eles) {
                        result=result.concat(Array.prototype.slice.call(eles));
                    }
                }else{
                    var arr = rooot[j].getElementsByTagName("*");
                    for (var i = 0; i < arr.length; i++) {
                        if (hasClass(arr[i], className)) {
                            result.push(arr[i]);
                        }
                    }
                }
            }
            break;
        //屬性
        case "[":
            var att = all[i].slice(1,all[i].length-1).split("=");
            var key = att[0],value=att[1];
            for (var j = 0; j < rooot.length; j++) {
                var eles=rooot[j].getElementsByTagName("*");
                for (var i = 0; i < eles.length; i++) {
                    if (value) {
                        for (var i = 0; i < eles.length; i++) {
                            if(eles[i].getAttribute(key)==value){
                                result.push(eles[i]);
                            }
                        }
                    }else{
                        for (var i = 0; i < eles.length; i++) {
                            if(eles[i].getAttribute(key)){
                                result.push(eles[i]);
                            }
                        }
                    }
                }
            }
            break;
        //tag
        default:
            for (var j = 0; j < rooot.length; j++) {
                eles=rooot[j].getElementsByTagName(all[i]);
                if (eles) {
                  result=result.concat(Array.prototype.slice.call(eles));
                }
            }
        }//switch
        rooot=result;
        result=[];   
    }//for
    return rooot[0];
}

用到的公共方法:

//IE9-不支持數組的indexOf()
if (!Array.prototype.indexOf) {
    Array.prototype.indexOf=function(value){
        for (var i = 0,len=this.length;i<len; i++) {
            if(this[i]==value){
                return i;
            }
        }
        return -1;
    };
}

//檢查ele是否有className
function hasClass(ele,className){
    if (ele&&ele.className) {
        var classes=ele.className.split(/\s+/);//這里必須要切成數組之后再判斷
        if(classes.indexOf(className)!=-1){
            return true;
        } 
    }
    return false;
}

// 判斷arr是否為一個數組,返回一個bool值
function isArray(arr){
    return Array.isArray(arr)||Object.prototype.toString.call(arr) === "[object Array]";
}

// 對字符串頭尾進行空格字符的去除、包括全角半角空格、Tab等,返回一個字符串
function trim(str){
    return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")
}

//把一個類數組轉換成數組
function toArray(obj){
    if (obj.nodeType == 1 ) {
        return [obj];
    }
    var arr = [];
    for( var i = 0 ; i < obj.length ; i++){
        arr.push(obj[i]);
    }
    return arr;
}

 

參考:

https://github.com/baidu-ife/ife/blob/master/2015_spring/task/task0002/review/demo/js/util_demo.js

https://github.com/starkwang/ife/blob/master/task/task0002/work/starkwang/js/util.js

 


免責聲明!

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



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