【问题标题】:Javascript: Chaining on elements like jQueryJavascript:链接像 jQuery 这样的元素
【发布时间】:2018-01-24 22:23:34
【问题描述】:

我试图在一定程度上复制 jQuery 的元素操作。现在我发现非常有用的是.first() 选择器。我希望能够像这样链接函数;
getElement(selector).first().hasClass(className);

现在,我已经取得了 2 个问题(请注意,我的代码示例被最小化了,所以请不要对错误处理进行 cmets。)

var getElement = function(selector, parent) {
  ret           = document.querySelectorAll(selector);
  this.element  = ret;
  this.hasClass = function(className) {
    className.replace('.', '');
    if(this.multiple())
    {
      console.log('Cannot use hasClass function on multiple elements');
      return false;
    }
  };

  this.first = function() {
    this.element = this.element[0];
    return this;
  };
  return this;
};

我目前的问题

如果我调用我的函数;

var $test = getElement('.something'); //result: nodelist with .something divs

如果我调用结果中的第一个元素;

$test.first(); //Result: First div, perfect!

但是,现在如果我再次调用$test,它将用first() 的结果替换elements 属性,这意味着我已经“丢失”了我的旧值。我不想失去它们,我只想要 first() 函数来实现该特定功能。然后我希望$test 再次返回所有元素。 此外,调用 first() 现在将结束 undefined,因为在 this 中只剩下 1 个元素,因为它已经从对象中删除了旧元素。

又一次尝试

现在,我还尝试通过返回第一个孩子而不是整个类对象来扭转它;

this.first = function() {
  return this.element[0];
};

但是,我会 $test.first().hasClass(className); //Returns ERROR, method hasClass undefined

这是因为 .hasClass 存在于原始 this 上,因为我现在要返回元素,所以它不再返回。

我试图从 jQuery 的库中获取一些东西,但这让我更加困惑......

我已经用谷歌搜索了这个主题,但是我找到的所有“链接方法”解决方案似乎都覆盖了对象的原始值,这不是我想要发生的。另一种解决方案实际上需要我一遍又一遍地重新启动对象,这对我来说似乎不是很有效......感谢任何帮助。我假设我的做法完全错误。

-- 如果你能帮助我,请解释为什么你的解决方案有效。我真的觉得如果我理解了这一点,我对javascript的理解可以进一步扩展。我只需要解决这个结构性(?)问题。

【问题讨论】:

  • 不返回元素(jQuery 也不返回),将其包装在您自己的类中,该类具有函数并在其自己的上下文中工作。另请注意,在我看来,你的这个在我看来非常模棱两可,因为我没有看到你使用 new getElement,这表明你的 this 很好,而不是你所期望的
  • 查看我的answer,了解一种实现您想要的方法,类似于jQuery@Babydead。
  • @Icepickle 谢谢,但我确实将它作为新的。但是,我在函数中使用了一个技巧(为了在此处发布它,我已经最小化了代码)。 AngelPolitis 现在检查你的答案。
  • @Babydead 从您对 Angels 回答的回复中,您似乎没有使用 new,因为您获得了 this 的窗口
  • @icepickle 是的,如果我忘了做新的事情,我会回到窗口。已经解决了这个问题,但我不明白触发它的方式或原因是什么哈哈哈。谢谢。

标签: javascript method-chaining


【解决方案1】:

first() 这样的方法不应该修改this,它应该创建一个新对象并返回它。您只能在修改元素的方法中使用return this;,而不是返回从元素派生的信息。

this.first = function() {
    return new getElement(this.element[0]);
};

请注意,您必须使用new getElement 创建对象,而不仅仅是getElement

这也需要对构造函数进行更改,因此它可以接受选择器字符串或元素:

var getElement = function(selector, parent) {
    var ret = typeof selector == "string" ? document.querySelectorAll(selector) : [selector];
    ...
}

您还应该考虑以适当的 OO 方式执行此操作,将方法放在原型中,而不是在每个对象中定义它们。

var getElement = function(selector, parent) {
  var ret = typeof selector == "string" ? document.querySelectorAll(selector) : [selector];
  this.element = ret;
};

getElement.prototype.hasClass = function(className) {
  className.replace('.', '');
  if (this.multiple()) {
    console.log('Cannot use hasClass function on multiple elements');
    return false;
  }
};

getElement.prototype.first = function() {
  return new getElement(this.element[0])
};

【讨论】:

  • 非常感谢!非常简单的解释和简单的答案。 -- 实际上,我确实先用原型进行了尝试。它没有成功,但我现在要再试一次,因为我对它有了更好的理解。你能举一个例子来说明如何做到这一点,也许?或者有一个很好的链接?
  • 你也应该看看 ES6 class 语法。
  • 是的,这就是我所拥有的。它一定没有奏效,因为其余的都是错误的。我将尝试这样做,因为为每个对象启动新函数似乎效率极低(逻辑上)。我还没有 ES6 的经验,但我现在会更深入地研究它。非常感谢!明天我将用原型替换 this.functions!
  • 我认为new getElement(this.elements[0]) 不会起作用。 document.querySelectorAll() 接受一个字符串参数,但它会被传递一个 DOM 节点。
  • @RickHitchcock 你是对的。但我已经在我的职能范围内发现了这种可能性。 if(typeof selector != 'string') { this.element = selector; } -- 但是,Barmar 最好将此添加到他对任何其他观众的回答中。
【解决方案2】:

您的外部函数中的this 指的是窗口/全局对象。

相反,返回 ret 变量本身。

inner 函数(成为对象的方法)中,this 按您期望的方式运行。

这是一个替代解决方案,它允许链接,即使在您调用了 first 方法之后:

var getElement = function(selector, parent) {
  var ret = typeof selector == 'string' ? document.querySelectorAll(selector)
                                        : selector;

  ret.hasClass = function(className) {
    if(!this.classList) {
      console.log('Cannot use hasClass function on multiple elements');
      return false;
    } else {
      return this.classList.contains(className);
    }
  };

  ret.first = function() {
    return new getElement(this[0]);
  };
  
  return ret;
};

console.log(getElement('p').length);                   //2
console.log(getElement('p').first().innerHTML);        //abc
console.log(getElement('p').first().hasClass('test')); //true
console.log(getElement('p').first().hasClass('nope')); //fase
console.log(getElement('p').hasClass('test'));         //false (multiple elements)
<p class="test">
  abc
</p>

<p>
  def
</p>

【讨论】:

  • 这解释了为什么我在特定情况下调试它时恢复了整个窗口,谢谢!我将首先接受 Barmar 的回答,因为它是第一个答案,但是您会因为非常好的解释和出色的工作而获得赞成票,是吗?非常感谢!
【解决方案3】:

这是我的处理方法:

  1. 创建一个构造函数,例如Search,负责根据输入查找元素。使用构造函数是正确的 OO 编程,并且您还具有在原型中定义一次方法的优势,并且它们可以被所有实例访问。
  2. 确保上下文 (this) 是一个类似数组的对象,具有数字属性和长度,以便您可以轻松地以传统方式遍历每个匹配的元素(使用 for 循环, [].forEach 等)
  3. 创建一个函数,比如getElement,它将使用构造函数并返回结果,而不必一直使用new 关键字。由于该函数返回我们构造函数的实例,因此您可以像往常一样链接所需的方法。
  4. first 方法使用构造函数创建新实例而不是修改原始实例,因为它的作用是返回第一个元素,而不是删除除第一个元素之外的所有内容。
  5. 每次您想出希望对象具有的新方法时,只需将其添加到构造函数的原型中即可。

片段:

;(function () {
  function Search (value) {
    var elements = [];

    /* Check whether the value is a string or an HTML element. */
    if (typeof value == "string") {
      /* Save the selector to the context and use it to get the elements. */
      this.selector = value;
      elements = document.querySelectorAll(value);
    }
    else if (value instanceof Element) elements.push(value);
      
    /* Give a length to the context. */
    this.length = elements.length;

    /* Iterate over every element and inject it to the context. */
    for (var i = 0, l = this.length; i < l; i++) this[i] = elements[i];
  }

  /* The method that returns the first element in a Search instance. */
  Object.defineProperty(Search.prototype, "first", {
    value: function () {
      return new Search(this[0]);
    }
  });
  
  /* The global function that uses the Search constructor to fetch the elements. */
  window.getElement = (value) => new Search(value);
  
  /* Create a reference to the prototype of the constructor for easy access. */
  window.getElement.fn = Search.prototype;
})();

/* Get all elements matching the class, the first one, and the first's plain form. */
console.log(getElement(".cls1"));
console.log(getElement(".cls1").first());
console.log(getElement(".cls1").first()[0]);
/* ----- CSS ----- */
.as-console-wrapper {
  max-height: 100%!important;
}
<!----- HTML ----->
<div id = "a1" class = "cls1"></div>
<div id = "a2" class = "cls1"></div>
<div id = "a3" class = "cls1"></div>

示例:

在本例中,我将一个名为 hasClass 的新方法添加到构造函数的原型中。

/* The method that returns whether the first element has a given class. */
Object.defineProperty(getElement.fn, "hasClass", {
  value: function (value) {
    return this[0].classList.contains(value);
  }
});

/* Check whether the first element has the 'cls2' class. */
console.log(getElement(".cls1").first().hasClass("cls2"));
<!----- HTML ----->
<script src="//pastebin.com/raw/e0TM5aYC"></script>
<div id = "a1" class = "cls1 cls2"></div>
<div id = "a2" class = "cls1"></div>
<div id = "a3" class = "cls1"></div>

【讨论】:

  • 嗯,你似乎做了很多我在原始函数中没有做的额外事情(尽管我在我的函数中也做了很多)。无论如何,当querySelectorAll 返回一个您可以计算其长度的有效数组时,为什么要执行推送和 for 循环?
  • 只有当参数是一个元素时才会发生推送。如果是选择器,则使用querySelectorAll。此外,for 循环对于将元素从nodelist querySelectorAll 返回传递到this 对象是必要的,以便原型方法可以访问它们@Babydead。实际上,我根本没有做额外的事情。想象一下,如果您希望能够传递documentwindow 或函数内部的任何内容;甚至可能使用上下文。我在这里很基本?
  • 感谢您的解释。我想当我有时间时,我将不得不更深入地了解您的答案。对我来说还是有点复杂!
  • 你当然应该这样做。我以最简洁的方式编写了代码并对其进行了注释,以便您阅读并轻松理解每一步。我希望它在某些场合对你有用@Babydead。
  • 会的。我要研究它以变得更好。我会在下节课上试试你的方法!
【解决方案4】:

我认为最简单的方法是返回一个包含您选择的节点的新类。这将是最简单的解决方案,因为您真的不想改变任何以前的选择器。

我做了一个小例子,使用一些 ES6 使一些事情更容易使用,它也有一个 $ 来启动正在进行的选择。

您会注意到,首先,所做的任何选择都只是调用本机document.querySelectorAll,但会返回一个新的Node 类。 firstlast 方法也都返回这些元素。

最后,hasClass 应该适用于当前节点选择中的所有元素,因此它将迭代当前节点,并检查那里的所有类,这个返回一个简单的布尔值,所以你不能继续使用链接那里的方法.

您希望链接的任何方法都应该:

  • 返回this对象(当前节点)
  • this 对象的一个​​元素作为新节点返回,以便在那里进行任何进一步的操作

const $ = (function(global) {
  class Node extends Array {
    constructor( ...nodes ) {
      super();
      nodes.forEach( (node, key) => {
        this[key] = node;
      });
      this.length = nodes.length;
    }
    first() {
      return new Node( this[0] );
    }
    last() {
      return new Node( this[this.length-1] );
    }
    hasClass( ...classes ) {
      const set = classes.reduce( (current, cls) => {
          current[cls] = true;
          return current;
        }, {} );
      for (let el of this) {
        for (let cls of el.classList) {
          if (set[cls]) {
            return true;
          }
        }
      }
      return false;
    }
  }
  global.$ = function( selector ) {
    return new Node( ...document.querySelectorAll( selector ) );
  };
  
  return global.$;
}(window));

let selector = $('.foo');
let first = selector.first(); // expect 1
console.log(first[0].innerHTML);
let last = selector.last();
console.log(last[0].innerHTML); // expect 4

console.log( first.hasClass('foo') ); // expect true
console.log( first.hasClass('bar') ); // expect false
console.log( selector.hasClass('foo') ); // expect true
console.log( selector.hasClass('bar') ); // expect true
<div class="foo">1</div>
<div class="foo">2</div>
<div class="foo bar">3</div>
<div class="foo">4</div>

【讨论】:

  • 对不起,我真的一点也看不懂这段代码。我很确定这是一段很棒的代码,但我完全不明白......
  • @Babydead 它只是一个 IIFE,它为 Node 类的声明创建了自己的范围,其余的是带有扩展运算符的 ES6 和 ES7。 has 类可能过于复杂,但它会考虑多个参数并检查当前Node中的所有类@
【解决方案5】:

您可以更新getElement,以便在您向其发送元素时再次返回。

var getElement = function(selector, parent) {
  var ret = null
  if (typeof selector === "string") {
    ret = document.querySelectorAll(selector);
  } else {
    ret = selector
  }
  this.element = ret;
  this.hasClass = function(className) {
    className.replace('.', '');
    if (this.multiple()) {
      console.log('Cannot use hasClass function on multiple elements');
      return false;
    }
  };

  this.first = function() {
    this.element = getElement(this.element[0]);
    return this;
  };
  return this;
};

var test = getElement(".foo");
console.log(test.first())
console.log(test.first().hasClass)
<div class="foo">1</div>
<div class="foo">2</div>
<div class="foo">3</div>
<div class="foo">4</div>

【讨论】:

  • 不会this 只是window 或您的示例中的全局对象?
【解决方案6】:

您可以使用.querySelectorAll()、展开元素和Array.prototype.find(),它返回数组中的第一个匹配项或undefined

const getElement = (selector = "", {prop = "", value = "", first = false} = {}) => {
    const el = [...document.querySelectorAll(selector)];
    if (first) return el.find(({[prop]:match}) => match && match === value)
    else return el;
};

let first = getElement("span", {prop: "className", value: "abc", first: true});
    
console.log(first);

let last = getElement("span");

console.log(all);
<span class="abc">123</span>
<span class="abc">456</span>

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2013-02-09
  • 1970-01-01
  • 2018-11-08
  • 1970-01-01
  • 1970-01-01
  • 2015-12-22
  • 2016-09-08
  • 2010-10-03
相关资源
最近更新 更多