19岁少年带你写一款自己的 jQuery !

212 阅读23分钟

本文仓库地址:

gitee.com/achens/sque…

已上传至 NPM 可下载到项目中使用:

npm i achens-query

封装一个自己的 Query 库这个想法在一开始学习 jQuery 的时候将有想了,正巧最近复习 js DOM,就顺手封装了一个。
总共封装了十二类 DOM 操作,三十二个方法,以及两个全局方法,基本上我能想到开发时常用的 DOM 操作我都封装到了。
整体思路是创建一个 Query 对象,然后在这个对象的原型上封装一系列的方法,最后将其暴露出去。也就是说外部只能通过我们主动暴露的 Query 对象来调用 DOM 操作方法。且在最后我封装了一个 dom() 方法,专门用于将 Query 对象转为征程 dom 对象。
这样的好处是,可以封装内部 DOM 操作方法的同时可以封装一个全局方法,非 Query 对象也可以使用的基于当前库的全局方法。

通过 $ 方法查找元素 --> 使用 Query 构造函数将查找到的元素转为 Query对象 --> 在 Query 对象上封装 DOM 方法。

// 使用自自执行函数创建全局 Query 对象
let $ = (S = (function (window) {
  /**
   * @function $
   * @description 获取目标 DOM 元素
   * @param { String } nodeSelector 查找目标元素字符串
   * @return { DOMObject } 返回 Query 对象信息
   * @author achens 2022-1-08 17:47
   * @version 0.1.1
   */
  function $(nodeSelector) {
    // 如果传入的是一个 DOM 标签,则直接调用 Query 返回 Query 对象
    if (typeof nodeSelector == "object" && nodeSelector.length == undefined) {
      return new Query(nodeSelector, null);
    }
    // 如果传入的是一个 NodeList 列表,也直接调用 Query 方法返回 Query 对象
    if (typeof nodeSelector == "object" && nodeSelector.length >= 1) {
      return new Query(nodeSelector, null);
    }
    // 判断传入的是否不是一个字符串
    if (typeof nodeSelector != "string")
      throw "请传入合法 selector 字符和 dom 对象!";
    // 获取目标元素
    let dom = document.querySelectorAll(nodeSelector);
    // 获取 DOM 元素集合
    return new Query(dom, nodeSelector);
  }

  /**
   * @function Query
   * @description 将 DOM 对象转换为 Query 对象并返回
   * @param { DOMObject } dom 目标 DOM 对象
   * @param { String } selector 查找的目标字符串
   * @return { Object } 当前 Query 对象信息
   * @author achens 2022-1-08 17:47
   * @version 0.3.0
   */
  function Query(dom, selector) {
    // 判断有无获取到 DOM,如果获取到了就保存获取到的 DOM 元素总长度
    this.length = dom ? dom.length : 0;
    // 如果 dom 是一个对象且没有长度(dom 是一个标签的情况下)
    // 解决 .parentNode 获取的父元素不是一个 NodeList,导致没有长度的 BUG
    if (typeof dom == "object" && dom.length == undefined) {
      // 手动设置长度为 1
      this.length = 1;
      // 设置第一项为传进来的 DOM 元素
      this[0] = dom;
      // 返回这个 Query 对象
      return this;
    }
    // 保存所有获取到的 DOM 元素到当前对象中
    for (let i = 0; i < this.length; i++) this[i] = dom[i];
    // 获取查找目标元素
    this.selector = selector || "";
    // 返回当前 DOM 对象的 Query 化对象
    return this;
  }

  // 基于 Query 对象封装对 DOM 操作的方法
  Query.prototype = {
     // 将当前 Query 对象转为 DOM 对象
    dom() {
      // 判断当前对象是否不是一个 Query 对象
      if (!this instanceof Query) throw "非 Query 对象!";
      // 判断当前 DOM 元素是否有多个
      if (this.length >= 2) {
        // 存放 DOM 元素集合
        let domSet = [];
        // 将所有 DOM 元素依次放入数组中
        for (let i = 0; i < this.length; i++) {
          domSet.push(this[i]);
        }
        // 最后将这个 DOM 数组返回
        return domSet;
      }
      // 返回 DOM 对象
      return this[0];
    },
 	}
  // 全局方法:判断目标是否是一个 Query 对象
  $.isQuery = function (value) {
    return value instanceof Query;
  };
  // 返回查找到的 DOM 元素集合
  return $;
})(window));

Query 对象与全局方法共同存在但却不互相干扰。这样我们就可以通过全局方法封装一些 DOM 操作之外的方法,亦或者是对 Query、DOM 对象的一些额外的扩展操作。
直接通过 $ 符使用的全局方法:

// 输出:false
console.log($.isQuery("a"));

// 报错:$.isQuery(...).dom is not a function
console.log($.isQuery('a').dom())

// 报错:$(...).isQuery is not a function
$("div").isQuery("a");

一、on()、bind()

1. on() 单个事件绑定

事件绑定这里很简单,注意点就是需要兼容 IE,所以加了个 addEventListener() 方法的判断,如果浏览器不支持则使用 attachEvent() 进行事件的绑定,其中 on() 和 bind() 方法都需要对传入参数进行合法判断性,所以将其抽离出来单独封装了一个函数。

// 判断事件是否合法
isEvent(on, func) {
  // 判断传入的事件类型是否基本合法
  if (on == undefined || typeof on != "string") {
    throw "事件类型不合法...";
  }
  // 判断传入的事件回调是否合法
  if (func == undefined || typeof func != "function") {
    throw "传入的事件回调不合法...";
  }
},

// 给当前 DOM 绑定事件类型
on(on, func, bool) {
  // 获取当前 DOM 对象
  let dom = this[0];
  // 判断传入事件是否合法
  this.isEvent(on, func);
  // 判读昂当前浏览器是否支持 addEventListener
  if (addEventListener) {
    // 给当前 DOM 绑定事件
    dom.addEventListener(on, func, bool);
  } else {
    // 兼容 ie 事件绑定
    dom.attachEvent(on, func, bool);
  }
  // 将当前对象返回出去使其可以继续调用 Query 原型上的方法,
  // 进行衍生操作
  return this;
},

2. bind() 多个事件绑定

bind() 方法内部也很简单,就是将传入方法数组遍历出来然后复用 on() 方法进行事件绑定。

// 给当前 DOM 一次性绑定多个事件类型
bind(eventObj) {
  // 如果传入的参数是个字符串
  if (typeof eventObj == "string") throw "绑定单个事件请使用 .on() 方法";
  // 判断传入参数是否是一个数组
  if (eventObj instanceof Array) {
    // 使用 .forEach 对数组内的事件类型依次进行事件绑定
    eventObj.forEach((k) => {
      // 使用 on() 方法进行事件绑定
      this.on(k.on, k.func, k.bool);
    });
  } else if (eventObj instanceof Object) {
    // 使用解构拿到事件相关参数
    let { on, func, bool } = eventObj;
    // 并使用 on() 方法绑定事件类型
    this.on(on, func, bool);
  }
  return this;
},

二、css() 样式设置

css() 方法代码很多但其实不复杂,无非是对传入参数进行判断并使用相应操作进行样式的添加删除修改等等。
其内部我们只考虑三种情况:

  1. 只传入了一个参数。
  2. 传入了一个对象。
  3. 传入了两个参数。

然后我们基于这三种情况进行合法验证并使用:

  1. .getComputedStyle()
  2. .getPropertyValue()
  3. .style.setProperty()

三个方法对样式进行获取和设置操作。

// 获取或设置当前 DOM 的 CSS 样式
css(pattern) {
  // 拿到当前 DOM
  let dom = this[0];
  // 获取样式:只传入一个参数,且这个参数是字符串的情况下
  if (typeof pattern == "string" && arguments.length == 1) {
    // 返回当前样式值
    return getComputedStyle(dom).getPropertyValue(pattern);
  }
  // 同时设置多个样式:只传入一个参数,且这个参数是对象
  if (pattern instanceof Object && arguments.length == 1) {
    // 将对象中的参数键值对遍历出来
    for (let name in pattern) {
      // 保存样式名
      let key = name;
      // 声明获取字符串中大写正则表达式
      let reg = /[A-Z]/g;
      // 在样式名中查找是否有大写字母
      if (reg.test(key)) {
        // 将大写字母改为划线
        key = key.replace(/[A-Z]/g, function (match) {
          return "-" + match.toLowerCase();
        });
      }
      // 修改样式
      dom.style.setProperty(key, pattern[name]);
    }
    return this;
  }
  // 修改样式:传入两个参数,且第二个参数是字符串或者数字
  // 获取样式名和值
  let name = arguments[0],
    value = arguments[1],
    // 获取第二个传入的参数是否是字符串或者数字
    isArg = name == "string" || "number" ? true : false;
  if (arguments.length == 2 && isArg) {
    // 修改样式
    dom.style.setProperty(name, value);
    return this;
  }
},

三、attr() 属性设置

attr() 属性的代码其实和 css() 差不多。这里不多讲了。

// 获取或设置当前 DOM 的属性
attr(attribute) {
  // 拿到当前 DOM
  let dom = this[0];
  // 获取属性:只传入一个参数,且这个参数是字符串的情况下
  if (typeof attribute == "string" && arguments.length == 1) {
    // 返回当前属性值
    return dom.getAttribute(attribute);
  }
  // 同时设置多个属性:只传入一个参数,且这个参数是对象
  if (attribute instanceof Object && arguments.length == 1) {
    // 将对象中的参数键值对便利出来
    for (let name in attribute) {
      // 修改样式
      dom.setAttribute(name, attribute[name]);
    }
    return this;
  }
  // 修改属性:传入两个参数,且第二个参数是字符串或者数字
  // 获取样式名和值
  let name = arguments[0],
    value = arguments[1],
    // 获取第二个传入的参数是否是字符串或者数字
    isArg = name == "string" || "number" ? true : false;
  if (arguments.length == 2 && isArg) {
    // 修改属性
    dom.setAttribute(name, value);
    return this;
  }
},

四、text()、html()

1. text() 元素文本值设置

这个方法很简单,这里不过是对其封装成了 Query 操作。
如果是设置元素的文本,那么在设置后就可接着对当前元素继续操作了。
反之,如果只是获取值,则是直接返回当前元素的文本值,而不可接着使用 Query 方法了。

// 获取或修改元素的文本值
text(text) {
  // 拿到当前 DOM
  let dom = this[0];
  // 判断有无传入参数
  if (arguments.length >= 1) {
    // 修改文本值
    dom.innerText = text;
    return this;
  } else {
    // 返回当前文本值
    return dom.innerText;
  }
},

2. html() 元素内容设置

如上一样。

// 获取或修改元素的内容
html(value) {
  // 拿到当前 DOM
  let dom = this[0];
  // 判断有无传入参数
  if (arguments.length >= 1) {
    // 修改 DOM 内容
    dom.innerHTML = value;
    return this;
  } else {
    // 返回当前 DOM 内容
    return dom.innerHTML;
  }
},

五、class()、addClass()、delClass()、toggle()

1. class() 获取、修改类名

这也很好理解,一眼就能看懂。

// 获取、修改类名
class(taxon) {
  // 获取当前 DOM
  let dom = this[0];
  // 没有传入参数就将元素本身的类名返回出去
  if (taxon == undefined) {
    return dom.className;
    // 如果传入了参数,并且这个参数还是字符串
  } else if (taxon != undefined && typeof taxon == "string") {
    // 修改当前元素的类名并将当前 Query 对象返回
    dom.className = taxon;
    return this;
  }
},

2. addClass() 添加类名

这里也很好理解,核心就是使用 .classList.add() 这个原生方法对元素的类名进行添加。其中使用 arguments 函数参数合集来进行判断是否有传入参数,或传入参数是否符合操作条件。
因为 arguments 参数集合是一个类数组对象,所以需要先使用 Array.from() 将其转为一个纯正的数组对象后在可以使用数组方法。

什么是类数组对象?

// 添加类名
addClass(taxon) {
  // 获取当前 DOM
  let dom = this[0];
  // 没有传入参数主动抛出错误
  if (arguments.length <= 0) throw "请传入要添加的类名";
  // 只传入一个类名,且不是以数组形式传入的,那就直接将这个类名添加进当前元素并返回
  if (arguments.length == 1 && typeof taxon == "string") {
    dom.classList.add(taxon);
    return this;
  }
  // 将参数集合转为为数组类型
  let arg = Array.from(arguments);
  // 使用 .map() 方法将数组中的每一项遍历出来并一一加入当前 DOM 中
  arg.map((v) => {
    if (typeof v == "string") {
      // 添加类名
      dom.classList.add(v);
    } else if (v instanceof Array) {
      // 将数组中的类名遍历出来加入当前 DOM 中
      for (let className of v) {
        dom.classList.add(className);
      }
    }
  });
  return this;
},

3. delClass() 删除类名

就操作上来说其实和 addCLass() 一样,代码内容都一样。
使用原生 API .classLIst.remove() 来删除类名。

// 删除类名
delClass(taxon) {
  // 获取当前 DOM
  let dom = this[0];
  // 没有传入参数删除所有类名
  if (arguments.length <= 0) {
    dom.className = "";
    return this;
  }
  // 只传入了一个类名
  if (arguments.length == 1 && typeof taxon == "string") {
    // 直接删除这个类名
    dom.classList.remove(taxon);
    return this;
  }
  // 将参数集合转为为数组类型
  let arg = Array.from(arguments);
  // 使用 .map() 方法将数组中的每一项遍历出来并一一删除
  arg.map((v) => {
    if (typeof v == "string") {
      // 添加类名
      dom.classList.remove(v);
    } else if (v instanceof Array) {
      // 将数组中的类名遍历出来加入当前 DOM 中
      for (let className of v) {
        dom.classList.remove(className);
      }
    }
  });
  return this;
},

4. toggle() 切换类名

核心代码就一行:.classList.toggle() 非常好理解。

// 切换类名
toggle(taxon) {
  // 获取当前 DOM
  let dom = this[0];
  // 如果没传入参数或参数类型不是字符串就主动抛出错误
  if (arguments.length <= 0) {
    throw "请传入参数!";
  } else if (typeof arguments[0] != "string") {
    throw "仅支持字符串类型的参数!";
  }
  // 进行类名切换
  dom.classList.toggle(taxon);
  return this;
},

六、addFirstChild()、addLastChild()、insertChild()、removeChild()

1. addFirstChild() 在子元素第一位插入

addFirstChild() 就逻辑上而言其实也不难。
具体实现逻辑是:

  1. 判断传入的是否是一个字符串或数字,如果是则创建一个 p 标签将传入内容放入这个 p 标签中,然后使用 .insetBefore 将这个 p 标签插入到子元素的最前面。
  2. 反之如果是一个元素,则直接使用 .insetBefore 将这个元素插入到子元素的最前面。

// 在指定子元素之前插入元素 DOM.insertBefore(新元素, 子元素)

// 在子元素的最前面插入元素
addFirstChild(node = null) {
  if (node == null) throw "请传入要插入的元素...";
  // 获取当前 DOM
  let dom = this[0];
  // 如果传入的是一个字符串或者数字
  if (typeof node == "string" || typeof node == "number") {
    // 创建一个 p 标签
    let p = document.createElement("p");
    // 将传入字符串或数字放入这个标签中
    p.innerHTML = node;
    // 然后将这个标签插入到第一个子元素之前
    dom.insertBefore(p, dom.firstChild);
    return new Query(p, null);
    // 如果传入的是一个元素
  } else if (typeof node == "object" && node.nodeType) {
    // 将这个元素插入到当前元素的子元素中的第一位;
    let newDom = dom.insertBefore(node, dom.firstChild);
    return new Query(newDom, null);
  }
},

2. addLastChild() 在子元素末尾插入

addLastChild() 方法整体逻辑上其实还是和 addFirstChild() 一样的,只不过这里是使用 appendChild() 实现子元素插入。

// 在子元素的最后插入元素
addLastChild(node = null) {
  if (node == null) throw "请传入要插入的元素...";
  // 获取当前 DOM
  let dom = this[0];
  // 如果传入的 node 是一个字符串或者数字
  if (typeof node == "string" || typeof node == "number") {
    // 创建一个 p 标签
    let p = document.createElement("p");
    // 将传入字符串或数字放入这个标签中
    p.innerHTML = node;
    // 然后将这个标签插入到 DOM 中
    dom.appendChild(p);
    return new Query(p, null);
    // 传入的 node 是一个对象且有 nodeType 属性,那就代表其是一个元素
  } else if (typeof node == "object" && node.nodeType) {
    // 将这个元素插入到当前元素的子元素中的最后一位;
    let newDom = dom.appendChild(node);
    return new Query(newDom, null);
  }
},

3. insertChild() 插入子元素

在当前元素下指定子元素处的前后进行元素插入,嗯...可以说是来到了第一个比较复杂的方法实现了,写这个方法时花了我一段功夫。
首先从参数上来看:

  • i:指定在第几个子元素处进行插入。
  • node:插入的元素或是字符串、数字。
  • isBefore:决定是在元素之前还是之后插入。(默认 false,在元素之后插入)

随后是整段代码的运行逻辑:

  1. 进入方法第一步自然是判断传入参数是否合法,这里不细究。
  2. 进入第二步,先判断当前是否是在子元素的最前面和最后面进行插入:通过判断所以是否小于等于零和大于等于整个子元素的总长度,并同时判断 isBefore 是否是 true 和 false,如果是的话,则直接复用 addFirstChild() 和 addLastChild()。
  3. 第三步首先判断当前是在子元素的前面还是后面插入。
    1. 前面:这里实现逻辑是和 addFirstChild() 一样的,不细究。
    2. 后面:这里的核心逻辑也和 addFirstChild() 一样都是通过 .insertBefore() 进行元素插入,不过因为 .insertBefore() 只能在元素之前插入而不能在其后进行插入。所以这里我们先获取插入目标位置的元素的下一个兄弟元素,然后将元素插入到下一个兄弟元素的前面。即可实现在目标位置的元素之后插入元素效果。
// 在指定索引处插入子元素
insertChild(i, node, isBefore = false) {
  // 判断传入参数是否不合法
  if (
    typeof node != "string" &&
    typeof node != "number" &&
    typeof node != "object" &&
    typeof isBefore != "boolean" &&
    i < 0
  ) {
    throw "发生错误...";
  }
  // 获取当前 DOM
  let dom = this[0];
  // 判断是否是在元素的最前面插入,是的话直接调用 this.addFirstChild
  if (i <= 0 && isBefore == true) {
    return this.addFirstChild(node);
    // 判断是否是在元素的最后面插入,是的话直接调用 this.addLastChild
  } else if (i >= dom.children.length && isBefore == false) {
    return this.addLastChild(node);
  }
  // 判断是在元素前面还是在后面插入
  if (isBefore) {
    // 判断插入的内容是否是字符串或者数字
    if (typeof node == "string" || typeof node == "number") {
      // 创建一个 p 标签
      let p = document.createElement("p");
      // 设置内容为传入的字符串或数字
      p.innerHTML = node;
      // 最后将其插入到 DOM 中
      dom.insertBefore(p, dom.children[i]);
      return new Query(p, null);
      // 判断插入的是否是一个节点
    } else if (typeof node == "object" && node.nodeType) {
      // 直接将其插入到对应位置上
      dom.insertBefore(node, dom.children[i]);
      return new Query(node, null);
    }
  } else {
    // 获取目标子元素的下一个兄弟元素
    let nextDom = dom.children[i].nextElementSibling;
    // 判断插入的内容是否是字符串或者数字
    if (typeof node == "string" || typeof node == "number") {
      // 创建一个 p 标签
      let p = document.createElement("p");
      // 将传入内容设置为这个标签的内容
      p.innerHTML = node;
      // 然后将这个标签插入到 DOM 中
      dom.insertBefore(p, nextDom);
      return new Query(node, null);
      // 判断插入的是否是一个节点
    } else if (typeof node == "object" && node.nodeType) {
      // 直接插入到 DOM 中
      dom.insertBefore(node, nextDom);
      return new Query(node, null);
    }
  }
},

4. removeChild() 删除子元素

删除子元素也很简单,代码中注释的说明的另一种方法是之前脑子短路忘了 js 中还有 removeChild[] 这个 API,写到这里的时候就顺手改正了。
这个方法内部实现就业很简单了,传入索引就是用 removeChild[] 删除索引子元素,传入字符串,就先在当前元素下获取所有目标子元素,然后遍历删除。
如果一个不传的话,那就删除当前元素下所有的子元素。这里写的时候给我绕到了,因为这个子元素的属性是随着每次子元素的删除而改变的,所以我在外部保存后在进行遍历。随后也因为子元素是一直随着删除而改变的,所以这里我们只需要让其一直删除第一个子元素就可以实现删除所有子元素效果。

// 删除子元素
removeChild(index) {
  // 获取当前元素
  let dom = this[0];
  // 判断传入的是否是索引
  if (typeof index == "number") {
    dom.removeChild[index];
    // 另外一种实现方法
    // // 删除目标索引子元素
    // dom.children[index].remove();
    return new Query(dom, null);
    // 判断传入的是否是字符串
  } else if (typeof index == "string") {
    // 获取要删除的目标元素
    let indexDom = dom.querySelectorAll(index);
    // 遍历删除
    for (let i = 0; i < indexDom.length; i++) {
      indexDom[i].remove();
    }
    return new Query(dom, null);
    // 如果没有传入参数
  } else if (index == undefined) {
    // 获取子元素长度
    let childLength = dom.children.length;
    // 遍历子元素长度来删除所有子元素
    for (let i = 0; i < childLength; i++) {
      // 因为是一个个删除的,所以子元素是一直在变化的
      // 这里只需要一直删除第一个就可
      dom.children[0].remove();
    }
    return new Query(dom, null);
  }
  throw "只接受数字以及字符串!";
},

七、parent()、parentAll()、children()

1. parent() 获取父元素

这里我考虑到了使用时可能会出现的三种情况:

  1. 单纯的获取父元素:直接调用 .parentNode 返回当前元素的父元素。
  2. 获取隔级父元素:通过传入的数字来决定获取隔几级的父元素。
  3. 获取准确父元素:通过传入的 CSS 查找器来获取目标父元素。

如果在使用的时候没有传入任何参数,或传入参数不是数字或者字符串,就会直接返回当前元素的上一级元素。
如果传入的是数字,那么就会使用 for 循环,不断更新获取上一级元素,直到目标层级或 document 为止。
如果传入的是字符串,则先获取当前元素的父元素的父元素即祖先元素。然后在这个祖先元素通过传入的 CSS 选择器获取祖先元素的子元素,也就是我们当前元素的父元素。
最后将获取到的元素通过 Query 返回,形成一个链式调用。

// 获取父元素
parent(selector) {
  // 获取当前 DOM
  let dom = this[0];
  // 传入了参数,且这个参数是 number 的情况下
  if (
    selector != undefined &&
    typeof arguments[0] == "number" &&
    selector > 1
  ) {
    // 保存自身
    let domParent = dom;
    // 遍历取父元素
    for (let i = 0; i < selector; i++) {
      // 判断是否已经获取到了最顶层的 document 对象
      if (domParent == document) return domParent;
      // 拿到当前元素的父元素
      domParent = domParent.parentNode;
    }
    // 将获取到的父元素返回
    return new Query(domParent, null);
    // 传入了参数且这个参数是一个字符串的情况下
  } else if (selector != undefined && typeof selector == "string") {
    // 获取当前元素的祖先元素,然后在祖先元素中查找准确父元素
    let selectorParent = dom.parentNode.parentNode.querySelector(selector);
    // 将获取到的父元素全部返回出去
    return new Query(selectorParent, selector);
  }
  // 没有传入参数就直接返回当前元素的父元素
  return new Query(dom.parentNode, null);
},

2. parentAll() 获取所有父元素

这个就很简单了,一直获取当前元素的上级元素,并将获取到的父元素全部保存到一个数组中,直到 docuemnt,最后将其获取到的父元素数组以 Query 的形式返回出去就行了。

// 获取所有父级元素
parentAll() {
  // 获取当前 DOM
  let dom = this[0];
  // 保存获取到的父级元素集合
  let parentArr = [];
  // 使用 while 循环获取父级元素
  while (dom.parentNode != document) {
    // 在没获取到 document 文档对象之前一直获取父级元素,并添加到数组当中
    dom = dom.parentNode;
    parentArr.push(dom);
  }
  return new Query(parentArr, null);
},

3. children() 获取子元素

children() 方法和 parent() 很相像,都接受一个或为数字或为字符串的参数。
如果传入了一个字符串,则通过这个字符串来获取所有指定的子元素。
相反传入的是数字的话,则先获取所有子元素,再通过 .filter() 方法,拿到指定的子元素。
但如果什么都不传入的话,默认会将所有子元素全部返回出去。

// 获取子元素
children(selector) {
  // 获取当前 DOM
  let dom = this[0];
  // 判断是否传入了目标字符串
  if (selector != undefined && typeof selector == "string") {
    // 获取指定类型子元素
    let domChild = dom.querySelectorAll(selector);
    return new Query(domChild, selector);
    // 判断传入的参数是否是一个数字
  } else if (selector != undefined && typeof selector == "number") {
    // 先拿到所有子元素
    let domChild = Array.from(dom.children);
    // 使用 .filter() 从所有子元素中拿到想要的那个子元素
    domChild = domChild.filter((v, i) => i == selector);
    // 如果没有获取到子元素
    if (domChild.length == 0) throw "目标子元素不存在...";
    return new Query(domChild, selector);
  }
  // 没有传入参数就将所有子元素全部返回出去
  return new Query(dom.children, null);
},

八、firstChild()、lastChild()

1. firstChild() 获取第一个子元素

这里的实现都非常简单,直接获取所有子元素然后将第一个子元素返回即可。

// 获取第一个子元素
firstChild() {
  // 获取当前 DOM
  let dom = this[0];
  // 获取第一个子元素
  let firstDom = dom.children[0];
  return new Query(firstDom, null);
},

2. lastChild() 获取最后一个子元素

同上,返回最后一个子元素即可。

// 获取最后一个子元素
lastChild() {
  // 获取当前 DOM
  let dom = this[0];
  // 获取 子元素长度
  let len = dom.children.length;
  // 获取最后个子元素
  let lastDom = dom.children[len - 1];
  return new Query(lastDom, null);
},

3. child() 获取准确子元素

一样简单,直接返回指定索引处的子元素即可。

// 获取准确子元素
child(i) {
  // 获取当前 DOM
  let dom = this[0];
  // 获取 子元素长度
  let len = dom.children.length;
  // 判断传入索引是否合法
  if (typeof i == "number" && i >= 0) {
    // 获取准确子元素
    let lastDom = dom.children[i];
    return new Query(lastDom, null);
  }
  throw "传入索引不合法!";
},

九、first()、last()、eq()

1. first() 获取 Query 对象中的第一个元素

和上面获取子元素是一样的,只不过这里是获取自身 Query 对象中的元素。

// 获取 Query 对象中的第一个元素
first() {
  return new Query(this[0], null);
},

2. last() 获取 Query 对象中的最后一个元素

略...

// 获取 Query 对象元素中的最后一个元素
last() {
  return new Query(this[this.length - 1], null);
},

3. eq() 获取 Query 对象中的准确元素

略...

// 通过索引获取准确的 Query 对象列表中的某个元素
eq(i) {
  // 判断传入索引是否合法
  if (typeof i == "number" && i >= 0) {
    // 获取准确 Query 元素
    return new Query(this[i], null);
  }
  throw "传入索引不合法!";
},

十、prevNode()、prevNodeAll()、nextNode()、nextNodeAll()

1. prevNode() 获取上一个兄弟元素

prevNode() 同样接受一个数字作为参数。
在没有传入数字的情况下,或传入了一个 false,0 这样的无效参数,我们就直接返回当前元素的上一个兄弟元素。
如果传入了一个非零的数字进来,那我们就基于这个数字进行遍历获取 i+ 上一个兄弟元素,最后一并返回出去。

// 获取上一个同级节点或指定的同级节点
prevNode(i) {
  // 获取当前 DOM
  let dom = this[0];
  // 判断有无传入参数
  if (i == undefined || i == false || i <= 0) {
    // 获取上一个兄弟元素
    let prevDom = dom.previousElementSibling;
    return new Query(prevDom, null);
  }
  // 保存兄弟元素
  let prevDom = dom.previousElementSibling;
  for (let j = 0; j < i; j++) {
    // 获取上一个兄弟元素
    prevDom = prevDom.previousElementSibling;
  }
  return new Query(prevDom, null);
},

2. prevNodeAll() 获取全部的上一个兄弟元素

prevNodeAll() 方法逻辑稍微有点绕,我这里考虑到了不传参数和传参数两种情况。

  1. 无论传不传参数,先获取所有的上一个兄弟元素。
  2. 不传参数:将获取到的上一个兄弟元素全部返回。
  3. 传了参数且这个参数合法:
    1. 获取传入参数的第一位,判断是 . 还是 #,亦或者是一个字符串。
    • 是类名(.):如果是个类名,就用 .filter 进行过滤,获取每个兄弟元素的类名,并转为数组,然后判断数组的第一位也就是第一个类名是否和传入的类名相同。(我这种写法只能判断第一个类名,因为我实在想不出其它能够同时判断多个类名是否合法的写法了。)
    • 是 ID(#):写法与逻辑和类名判断一样。
    • 是一个字符:写法上也和上面相同,不过这里因为 .nodeName 获取的标签名是大写,不能与传入的字符串相比较,所以我又使用了 .toUpperCase() 将传入参数转为大写再进行的比较。
// 获取上一个同级节点或指定的同级节点
prevNodeAll(selector) {
  // 获取到当前 DOM
  let dom = this[0];
  // 先获取所有的上一个兄弟元素
  // 获取上一个兄弟元素
  let prevDom = dom.previousElementSibling;
  // 保存获取到的兄弟元素
  let prevDomArr = [];
  // 不断循环获取
  while (prevDom != document && prevDom != null) {
    // 将获取到的兄弟元素放入数组中
    prevDomArr.push(prevDom);
    // 继续获取上一个兄弟元素
    prevDom = prevDom.previousElementSibling;
  }
  // 判断是否需要准确获取兄弟元素
  if (
    typeof selector == "string" &&
    selector != undefined &&
    selector != false
  ) {
    // 获取筛选条件;获取 CSS 查找器的第一位
    let index = selector.slice(0, 1);
    // 通过 switch 判断筛选目标是什么
    switch (index) {
      case ".":
        // 通过 class 择选兄弟元素
        // 使用 filter 进行过滤,获取到目标兄弟元素
        var domArr = prevDomArr.filter(
          (v) =>
            // 将类名列表转为数组并只判断第一个类名是否是我们要获取的准确兄弟元素 CSS 查询器
            v.classList.value.split(" ")[0] ==
            selector.slice(1, selector.length)
        );
        return new Query(domArr, null);
      case "#":
        // 通过 id 择选兄弟元素
        var domArr = prevDomArr.filter(
          // 只需要 id 相同的兄弟元素
          (v) => v.id == selector.slice(1, selector.length)
        );
        return new Query(domArr, null);
      default:
        var domArr = prevDomArr.filter(
          // 通过 .nodeName 来获取标签名称筛选合法兄弟元素
          // 因为 .nodeName 获取的名称都是大写,所以需要使用 .toUpperCase() 转为大写在进行判断
          (v) => v.nodeName === selector.toUpperCase()
        );
        return new Query(domArr, null);
    }
  }
  // 如果不需要获取准确兄弟元素就直接将所有兄弟元素全部返回
  return prevDomArr;
},

3. nextNode() 获取下一个兄弟元素

逻辑如 prevNode() 没有区别,略...

// 获取下一个同级节点或指定的同级节点
nextNode(i) {
  // 获取当前 DOM
  let dom = this[0];
  // 判断有无传入参数
  if (i == undefined || i == false || i == 0) {
    // 获取下一个兄弟元素
    let nextDom = dom.nextElementSibling;
    return new Query(nextDom, null);
  }
  // 保存兄弟元素
  let nextDom = dom.nextElementSibling;
  for (let j = 0; j < i; j++) {
    // 获取下一个兄弟元素
    nextDom = nextDom.nextElementSibling;
  }
  return new Query(nextDom, null);
},

4. nextNodeAll() 获取全部下一个兄弟元素

逻辑如 prevNodeAll() 一样,这里省略...

// 获取当前元素之后的所有同级节点
nextNodeAll(selector) {
  // 获取到当前 DOM
  let dom = this[0];
  // 先获取全部兄弟元素
  // 获取上一个兄弟元素
  let nextDom = dom.nextElementSibling;
  // 保存获取到的兄弟元素
  let nextDomArr = [];
  // 不断循环获取
  while (nextDom != document && nextDom != null) {
    // 将获取到的兄弟元素放入数组中
    nextDomArr.push(nextDom);
    // 继续获取上一个兄弟元素
    nextDom = nextDom.nextElementSibling;
  }
  // 判断是否需要准确获取兄弟元素
  if (
    typeof selector == "string" &&
    selector != undefined &&
    selector != false
  ) {
    // 获取筛选条件;获取 CSS 查找器的第一位
    let index = selector.slice(0, 1);
    // 通过 switch 判断筛选目标是什么
    switch (index) {
      case ".":
        // 通过 class 择选兄弟元素
        // 使用 filter 进行过滤,获取到目标兄弟元素
        var domArr = nextDomArr.filter(
          (v) =>
            // 将类名列表转为数组并只判断第一个类名是否是我们要获取的准确兄弟元素 CSS 查询器
            v.classList.value.split(" ")[0] ==
            selector.slice(1, selector.length)
        );
        return new Query(domArr, null);
      case "#":
        // 通过 id 择选兄弟元素
        var domArr = nextDomArr.filter(
          // 只需要 id 相同的兄弟元素
          (v) => v.id == selector.slice(1, selector.length)
        );
        return new Query(domArr, null);
      default:
        var domArr = nextDomArr.filter(
          // 通过 .nodeName 来获取标签名称筛选合法兄弟元素
          // 因为 .nodeName 获取的名称都是大写,所以需要使用 .toUpperCase() 转为大写在进行判断
          (v) => v.nodeName === selector.toUpperCase()
        );
        return new Query(domArr, null);
    }
  } else {
    // 如果不需要获取准确兄弟元素就直接返回全部兄弟元素
    return nextDomArr;
  }
},

十一、insertElementBefore()、insertElementAfter()

1. insertElementBefore() 在自身前面添加元素

逻辑类似 addFirstChild(),不过这里是在自身前面插入元素。
整体逻辑也很简单,先获取父元素,在父元素中使用 .insertBefore() 给自身元素的前面添加元素。

// 在元素自身的前面插入元素
insertElementBefore(node) {
  // 获取当前 DOM
  let dom = this[0];
  // 获取父元素
  let parentDom = dom.parentNode;
  // 判断传入是否是一个节点
  if (typeof node == "object" && node.nodeType) {
    // 在当前元素之前插入元素
    parentDom.insertBefore(node, dom);
    return new Query(node, null);
  }
  if (typeof node == "string" || typeof node == "number") {
    // 创建一个元素
    let p = document.createElement("p");
    // 设置新元素内容为传入的内容
    p.innerHTML = node;
    // 插入元素
    parentDom.insertBefore(p, dom);
    return new Query(p, null);
  }
  throw "使用不规范!";
},

2. insertElementAfter() 在自身后面添加元素

这个逻辑也和 addLastChild() 差不多,都是先获取自身的下个元素和父元素,然后使用 .insertBefore() 进行元素的插入。

// 在元素自身的后面插入元素
insertElementAfter(node) {
  // 获取当前 DOM
  let dom = this[0];
  // 获取当前元素的下个元素
  let nextDom = dom.nextElementSibling;
  // 获取当前元素的父元素
  let parentDom = dom.parentNode;
  // 判断传入的是否是一个元素
  if (typeof node == "object" && node.nodeType) {
    // 插入元素
    parentDom.insertBefore(node, nextDom);
    return new Query(node, null);
  }
  // 判断传入的是否是一个字符串或者数字
  if (typeof node == "string" || typeof node == "number") {
    // 创建一个新元素
    let p = document.createElement("p");
    // 设置新元素内容为传入的内容
    p.innerHTML = node;
    // 插入元素
    parentDom.insertBefore(p, nextDom);
    return new Query(p, null);
  }
  throw "使用不规范!";
},

十二、dom() Query 对象转 DOM 对象

dom() 方法仅仅只是提供将 Query 元素对象转为普通的 DOM 对象的功能。
整体逻辑也不难实现。

  // 将当前 Query 对象转为 DOM 对象
  dom() {
    // 判断当前对象是否不是一个 Query 对象
    if (!this instanceof Query) throw "非 Query 对象!";
    // 判断当前 DOM 元素是否有多个
    if (this.length >= 2) {
      // 存放 DOM 元素集合
      let domSet = [];
      // 将所有 DOM 元素依次放入数组中
      for (let i = 0; i < this.length; i++) {
        domSet.push(this[i]);
      }
      // 最后将这个 DOM 数组返回
      return domSet;
    }
    // 返回 DOM 对象
    return this[0];
  },
};

十三、全局方法

1. isQuery

用以判断目标是否是一个 Query 对象。
很好理解,内部只是使用了 instanceof 来判断传入参数是否和 Query 相等。

// 判断目标是否是一个 Query 对象
$.isQuery = function (value) {
  return value instanceof Query;
};

2. forQuery

因为 Query 中每个方法都默认只会对获取到的第一个元素进行操作,所以特地写个循环参数,其内部会将获取到的所有元素拿出去重新生成 Query 对象并调用一次目标方法,最后将被操作过的所有 dom 统一返回。
**Tips:**我这里使用 switch 硬生生一个个判断属实很傻,但我是在想不出有什么可代替的方案了,求大佬告知!!!

  /**
   * @description 全局方法:将 Query 中的所有元素执行传入的内部 DOM 方法
   * @param { Object } Querys 要操作的目标 Query 对象合集
   * @param { String } func 需要执行的内部方法
   * @param { String | Number | Object } arg 传给该函数的参数
   */
  $.forQuery = function (Querys, func, ...arg) {
    // 保存备操作过的 dom
    let dom = [];
    // 判断传入方法是否存在
    if (Querys.__proto__.hasOwnProperty(func)) {
      // 使用 switch 进行方法校准
      switch (func) {
        // ...
        case "parent":
          // 依次对获取到的所有 Query 对象元素进行操作
          for (let i = 0; i < Querys.length; i++) {
            // 获取每个 dom
            let thisDom = new Query(Querys[i], null);
            // 保存被操作过的元素
            dom.push(thisDom.parent(...arg));
          }
          break;
         // ...
        default:
          throw "方法不存在!";
      }
    }
    // 返回统一执行结果
    return dom == [] ? "Error:出现意外错误!" : dom;
  };


原文地址:https://www.yuque.com/xiaochens/mr72m3/ogp9tt