当使用 foreach() 循环元素数组时,如何从 JQuery 模拟 $()?

How can I emulate $() from JQuery when cycling an array of elements with foreach()?

我正在研究我的 JS 代码并试图缩短它,因为我一直喜欢 JQuery 的极短语法,我希望我的代码少一些混乱。

我的主要目标是缩短 document.querySelectorAll 调用时间,就像 JQuery 对 $() 所做的一样。到目前为止一切顺利,这就是我得到的 (CodePen here):

function getEle(e) {
  if(document.querySelectorAll(e).length > 0) {
    e = document.querySelectorAll(e);
    if(e.length == 1)
      e = e[0];
    return e;
  }
}

// -------------------------------
// Example: grab multiple elements
// -------------------------------
getEle('p').forEach(e => {
  e.style.color = 'blue';
});

// ----------------------------
// Example: grab single element
// ----------------------------
getEle('h1').style.color = 'red';
getEle('#product-221').style.color = 'orange';

使用 function $(e){} 而不是 function getEle(e){} 会使它更短,但我不想弄乱乍一看看起来像 JQuery 的东西。

现在我的问题是:函数 returns 如果只有一个元素,则为单个元素,否则 returns 整个元素数组。在这种情况下,我需要使用 foreach() 循环来循环它们,如您在我的示例中所见。但是如果我不知道是有多个元素还是只有一个呢?

在 JQuery 中,如果您 运行 $(p).css('color','red') 它会应用红色,无论您是否有一个或多个 p 元素(或 none全部)。您不需要 运行 一个 foreach() 循环,因为它会为您完成。

我可以做类似的事情吗?

我希望这段代码能够自动检查是否有 >1 个元素并相应地循环它们,将 style.color = 'red' 应用于每个元素:

getEle('p').style.color = 'red';

而不是被迫这样做:

getEle('p').forEach(e => {
  e.style.color = 'red';
});

基本上,我想将 foreach() 合并到函数内部并从外部应用我需要的任何东西(例如:style.color = 'red')。

希望我说得足够详细,如果您需要更多详细信息,请告诉我!

确实,jQuery 基于集合的特性是(恕我直言)它的杀手级功能。

我肯定希望函数的 return 值保持一致。如果你愿意,你总是可以在你知道只有一个元素的情况下使用不同的函数。

为了在使用它时获得 jQuery-ness,您可能希望函数 return 包含要操作的元素和操作它们的方法的对象,例如

你有几个选择,但你基本上可以做 jQuery 做的事情:有一组函数来做一些事情(csshtml 等)包含循环,并有一个包含函数应该操作的元素的对象。

实现它,你有一个基本的选择:

  • 让每个方法实现循环,或者
  • 有一个执行循环并调用在单个元素上执行工作的实现函数的函数

我确信每种方式都有利有弊,但我可能会倾向于后者。你可能会想要一个通用的“只是循环遍历它们”的方法(比如 jQuery 的 each),所以你可以重用它。

除了其基于集合的性质外,jQuery 的 API 的另一个方便的功能是大多数方法 return 您调用它们的对象,这对于链接很方便.

这里有一个 sketch 的实现(一个草图,但它确实有效),请参阅评论以了解其工作原理:

"use strict";

// ==== "q.js" module

// import { qAddMethod } from "./q/core.js";

// The function that selects/wraps elements (vaguely like jQuery's `$`)
/*export */function q(selectorOrElement) {
    // Get or create the instance (if called with `new`, we already have an instance;
    // if not, create it with our prototype).
    const instance = new.target ? this : Object.create(q.prototype);

    // Get our `elements` array
    if (typeof selectorOrElement === "string") {
        // Assume it's a selector (note this doesn't support the way jQuery
        // hyper-overloads the `$` function; make creating new elements a
        // different function
        instance.elements = [...document.querySelectorAll(selectorOrElement)];
    } else if (selectorOrElement) {
        // Assume it's a DOM element
        instance.elements = [selectorOrElement];
    } else {
        // Start off empty
        instance.elements = [];
    }

    // Return the instance
    return instance;
}

// A method to loop through each element calling the given callback function.
// Calls the callback with the element and its index in the set followed by
// any additional arguments passed to `each`.
qAddMethod(function each(fn, ...args) {
    const { elements } = this;
    let index;
    const length = elements.length;
    for (index = 0; index < length; ++index) {
        const element = elements[index];
        fn.call(this, element, index, ...args);
    }
    return this;
});

// ==== "q/core.js" module

// import { q } from "./q.js";

// Add a method to the `q` prototype
/*export */function qAddMethod(fn) {
    Object.defineProperty(q.prototype, fn.name, {
        value: fn,
        writable: true,
        configurable: true,
    });
}

// ==== "q/css.js" module:

// import { qAddMethod } from "q/core.js";

// Implementation of the `css` method when given a style name and value
const cssImplOne = (element, index, styleName, value) => {
    element.style[styleName] = value;
};

// Implementation of the `css` method when given an object
const cssImplMulti = (element, index, styles) => {
    Object.assign(element.style, styles);
};

// Define the `css` method
qAddMethod(function css(styles, value) {
    if (typeof styles === "string") {
        return this.each(cssImplOne, styles, value);
    }
    return this.each(cssImplMulti, styles);
});

// ==== "q/html.js" module:

// import { qAddMethod } from "q/core.js";

// Implementation of the `html` method
const htmlImpl = (element, index, str) => {
    element.innerHTML = str;
};

// Define it
qAddMethod(function html(str) {
    return this.each(htmlImpl, str);
});

// ==== "qall.js" module:
// This is a rollup module that imports everything, fully populating the `q` prototype.
// You wouldn't use this if you wanted tree-shaking.

// import { q } from "./q.js";
// import "./q/css.js";
// import "./q/html.js";
//
// export q;

// ==== Using it

// Using the rollup:
// import { q } from "./qall.js";
// Or using only individual parts:
// import { q } from "./q.js";
// import "./q/css.js";
// import "./q/html.js";

q(".a").css("color", "green");
q(".b").css({
    fontWeight: "bold",
    border: "1px solid black",
});
q(".c")
    .html("hi")
    .each((element, index) => {
        element.textContent = `Testing ${index}`;
    });
<div class="a">class a</div>
<div class="b">class b</div>
<div class="c">class c</div>
<div class="c">class c</div>
<div class="c">class c</div>

请注意,我在它们自己的模块中实现了 csshtml,并且它们只会在加载模块时将自己添加到 q.prototype。这使您可以构建仅包含相关方法的捆绑包,而不是您从未使用过的方法。但同样,这是一个 sketch,我敢肯定上面有很多次优的地方。

根据以上评论...

"1/2 ... Of cause it can be done. For the desired syntax of e.g. getEle('p').style.color = 'red'; the OP needs to re-implement the entire DOM specification for an own super capable collection(wrapper) object. There are many reasons why John Resig 15 years ago did choose chained methods and not something the OP does request. The latter has to be implemented as ineterchained/interconnected class/type abstractions and getters/setters for every DOM API interface and every element property."

// - test implementation of a
//   node list `style` bulk accessor.
class ListStyle {
  #nodeList;
  constructor(nodeList) {
    this.#nodeList = nodeList;
  }
  set color(value) {
    this
      .#nodeList
      .forEach(node =>
        node.style.color = value
      );
    return value;
  }
  get color() {
    return [
      ...this.#nodeList
    ].map(
      node => [node, node.style.color]
    );
  }
}

// - old school module syntax.
const $$ = (function () {

  class NodeListWrapper {
    constructor(selector) {

      const nodeList = document.querySelectorAll(selector)
  
      Object.defineProperty(this, 'style', {
        value: new ListStyle(nodeList),
        // enumerable: true,
      });
    }
  }

  function $$(selector) {
    return new NodeListWrapper(selector)
  }
  // default export.
  return $$;

}());


// The OP's desired node list accessor/setter syntax.
$$('p').style.color = 'blue';


console.log(
  "$$('div, p') ...", $$('div, p')
);
console.log(
  "$$('div, p').style ...", $$('div, p').style
);
console.log(
  "$$('div, p').style.color ...", $$('div, p').style.color
);
* { margin: 0; padding: 0; }
body { margin: -2px 0 0 0; zoom: .88; }
.test p { color: #fc0; }
.test div { color: #c0f; }
<div class="test">
  <p>Foo bar ... paragraph</p>

  <div>Baz biz ... div</div>

  <p>Buzz booz ... paragraph</p>

  <div>Foo bar ... div</div>
</div>