JavaScript实现DOM对象选择器

作者:流萤

时间:2026-10-31

浏览:392

本文介绍如何使用JavaScript手动实现类似jQuery的DOM对象选择器,支持ID、标签名、类名及属性选择,并深入解析复合选择器的两种处理思路:从内向外筛选与从外向内筛选,同时分析其潜在缺陷与性能问题。

文章目录

DOM对象选择器的实现目标与类型

目标:

依据传入的选择器类型,选取首个匹配的DOM元素。 ①支持通过ID获取DOM元素,例如 $("#adom"); ②支持通过标签名获取DOM元素,例如 $("a"); ③支持通过类名获取DOM元素,例如 $(".classa"); ④支持通过属性匹配获取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和标签名可直接调用DOM原生方法。 •类名选择器中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元素。

若有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结构如下时,它会筛选出“第一个”元素,但这并非预期结果。

DOM对象选择器的实现目标与类型

尽管实际应用中父元素和子元素定义相同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

以上为本文全部内容,希望对各位的学习有所帮助,也欢迎大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章