为什么jQuery或者像getElementById这样的DOM方法找不到元素?

601

当使用document.getElementById$("#id")或其他任何DOM方法 / jQuery选择器时,如果无法找到元素,可能的原因有哪些?

示例问题包括:

  • jQuery默默地未能绑定事件处理程序
  • jQuery“getter”方法(.val().html().text())返回undefined
  • 标准DOM方法返回null,导致多个错误之一:

Uncaught TypeError:无法设置null的属性'...'
Uncaught TypeError:无法设置null的属性(设置'...')
Uncaught TypeError:无法读取null的属性'...'
Uncaught TypeError:无法读取null的属性(读取'...')

最常见的形式为:

Uncaught TypeError:无法设置null的属性'onclick'
Uncaught TypeError:无法读取null的属性'addEventListener'
Uncaught TypeError:无法读取null的属性'style'


43
关于为什么找不到某个DOM元素,经常会有很多问题。原因通常是JavaScript代码放在DOM元素之前。这篇文章旨在成为这类问题的规范答案。它是社区维护的,所以请随时改进它 - Felix Kling
你不需要输入“#”,只需使用$("your_id") - Post Malone
@PostMalone:可能在某个时候发生了变化,或者它一直是这样。无论如何,我总是会使用正确的CSS选择器与jQuery一起使用。这会使它更加一致。 - Felix Kling
7个回答

615

当你的脚本运行时,你试图找到的元素在DOM中不存在。

你的DOM依赖脚本的位置会对其行为产生深远影响。浏览器从上到下解析HTML文档。元素被添加到DOM中,脚本(默认情况下)在遇到它们时被执行。这意味着顺序很重要。通常情况下,脚本无法找到后面出现的标记,因为这些元素尚未添加到DOM中。

考虑以下标记;脚本#1无法找到<div>,而脚本#2成功:

<script>
  console.log("script #1:", document.getElementById("test")); // null
</script>
<div id="test">test div</div>
<script>
  console.log("script #2:", document.getElementById("test")); // <div id="test" ...
</script>

那么,你应该怎么办呢?你有几个选择:
选项1:移动您的脚本

Given what we've seen in the example above, an intuitive solution might be to simply move your script down the markup, past the elements you'd like to access. In fact, for a long time, placing scripts at the bottom of the page was considered a best practice for a variety of reasons. Organized in this fashion, the rest of the document would be parsed before executing your script:

<body>
  <button id="test">click me</button>
  <script>
    document.getElementById("test").addEventListener("click", function() {
      console.log("clicked:", this);
    });
  </script>
</body><!-- closing body tag -->

虽然这是有道理的,对于旧版浏览器来说是一个可靠的选择,但它的功能有限,现在有更加灵活、现代化的方法可供选择。

选项2:defer属性

虽然我们说过脚本会“(默认情况下)在遇到时执行”,但现代浏览器允许您指定不同的行为。如果您正在链接外部脚本,可以使用defer属性。

[defer,一个布尔属性]被设置为向浏览器指示该脚本应在文档解析完成后但在DOMContentLoaded事件触发之前执行。

这意味着您可以将带有defer标记的脚本放置在任何位置,甚至是<head>,它都可以访问完全实现的DOM。

<script src="https://gh-canon.github.io/misc-demos/log-test-click.js" defer></script>
<button id="test">click me</button>

只需记住...
1. defer 只能用于外部脚本,即具有 src 属性的脚本。 2. 注意 浏览器支持,即 IE < 10 中的错误实现。

选项3:模块

根据您的需求,您可以使用JavaScript模块。除了与标准脚本(在此处注明)有其他重要区别之外,模块会自动延迟加载,并且不限于外部来源。

将您的脚本的type设置为module,例如:

<script type="module">
  document.getElementById("test").addEventListener("click", function(e) {
    console.log("clicked: ", this);
  });
</script>
<button id="test">click me</button>


选项4:延迟处理事件
在文档解析完成后,添加一个监听器来处理事件。
DOMContentLoaded事件
DOMContentLoaded事件在DOM从初始解析完全构建完成后触发,无需等待样式表或图像等内容加载。

<script>
  document.addEventListener("DOMContentLoaded", function(e){
    document.getElementById("test").addEventListener("click", function(e) {
      console.log("clicked:", this);
    });
  });
</script>
<button id="test">click me</button>

窗口:加载事件

DOMContentLoaded和其他资源(如样式表和图像)加载完成后,load事件将触发。因此,它比我们期望的要晚一些。不过,如果你考虑到像IE8这样的旧浏览器,它几乎得到了普遍支持。当然,你可能需要一个addEventListener()的polyfill

<script>
  window.addEventListener("load", function(e){
    document.getElementById("test").addEventListener("click", function(e) {
      console.log("clicked:", this);
    });
  });
</script>
<button id="test">click me</button>

jQuery的ready()

DOMContentLoadedwindow:load各有其注意事项。jQuery的ready()提供了一个混合解决方案,当可能时使用DOMContentLoaded,当必要时转向window:load,如果DOM已经完成,则立即触发回调函数。

您可以直接将准备好的处理程序传递给jQuery,例如:$(handler)

<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
<script>
  $(function() {
    $("#test").click(function() {
      console.log("clicked:", this);
    });
  });
</script>
<button id="test">click me</button>


选项5:事件委托

将事件处理委托给目标元素的祖先元素。

当一个元素触发一个事件(前提是它是一个冒泡事件,并且没有任何东西阻止它的传播),该元素的每个父级,一直到window,也会接收到该事件。这使得我们可以将处理程序附加到现有元素上,并从其后代元素中捕获事件的冒泡...甚至是在处理程序附加之后添加的后代元素。我们只需要检查事件,看它是否由所需的元素触发,如果是,就执行我们的代码。

通常,这种模式适用于在加载时不存在的元素,或者为了避免附加大量重复的处理程序。为了效率,选择最近可靠的目标元素的祖先元素,而不是将其附加到document上。

原生JavaScript

<div id="ancestor"><!-- nearest ancestor available to our script -->
  <script>
    document.getElementById("ancestor").addEventListener("click", function(e) {
      if (e.target.id === "descendant") {
        console.log("clicked:", e.target);
      }
    });
  </script>
  <button id="descendant">click me</button>
</div>

jQuery的on()

jQuery通过on()提供了这个功能。给定一个事件名称、一个所需后代的选择器和一个事件处理程序,它将解决您的委托事件处理并管理您的this上下文:

<script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
<div id="ancestor"><!-- nearest ancestor available to our script -->
  <script>
    $("#ancestor").on("click", "#descendant", function(e) {
      console.log("clicked:", this);
    });
  </script>
  <button id="descendant">click me</button>
</div>


3
“defer”属性对我帮助很大。此外,这可能是我在这里看到的最详细的答案之一。 - lStoilov

169

简短明了:因为你正在寻找的元素尚未存在于文档中。


在接下来的回答中,我将使用getElementById作为示例,但同样适用于getElementsByTagName, querySelector和任何其他选择元素的DOM方法。

可能的原因

一个元素不存在可能有三个原因:

  1. 传递的 ID 在文档中不存在。您应该仔细检查传递给 getElementById 的 ID 是否确实与 (生成的) HTML 中现有元素的 ID 匹配,并且您未拼写错误 (ID 区分大小写!)。

    如果您使用 getElementById,请确保您只提供元素的 ID (例如,document.getElemntById("the-id"))。如果您使用接受 CSS 选择器的方法 (例如 querySelector),请确保在 ID 前包含 #,以表示您正在查找 ID (例如,document.querySelector("#the-id"))。您不得在 getElementById 中使用 #,必须在 querySelector 等方法中使用它。此外,请注意,如果 ID 中有 CSS 标识符 中不合法的字符(例如 .;包含 . 字符的 id 属性是不好的做法,但是合法的),则必须在使用 querySelector 时对其进行转义(例如,document.querySelector("#the\\.id"))但是在使用 getElementById 时不需要转义(例如,document.getElementById("the.id"))。

  2. 在调用 getElementById 时,该元素此时不存在。

  3. 尽管您在页面上可以看到该元素,但它不在您正在查询的文档中,因为它位于一个 iframe 中(它有自己的文档)。当您搜索包含它们的文档时,iframes 中的元素不会被搜索。

如果问题是第三个原因(它在一个iframe或类似的地方),您需要查看iframe中的文档,而不是父文档,可能通过获取iframe元素并使用其contentDocument属性来访问其文档(仅限同源)。本答案的其余部分解决了前两个原因。
第二个原因——它还没有出现——非常普遍。浏览器从上到下解析和处理HTML。这意味着在DOM元素出现在HTML之前发生的任何对DOM元素的调用都将失败。
考虑以下示例:
<script>
    var element = document.getElementById('my_element');
</script>

<div id="my_element"></div>
div 出现在 script 之后。在脚本执行的时候,该元素还不存在,getElementById 将返回 null
jQuery
对于所有使用 jQuery 的选择器也是如此。如果你拼写错误或者试图在元素实际存在之前选择它们,jQuery 将无法找到这些元素。
另一个问题是当你没有使用协议加载脚本并且从文件系统运行时,jQuery 找不到。
<script src="//somecdn.somewhere.com/jquery.min.js"></script>

这种语法用于允许脚本在协议为https://的页面上通过HTTPS加载,在协议为http://的页面上通过HTTP加载。但不幸的是,它会试图并失败地加载file://somecdn.somewhere.com...。

解决方案

在调用getElementById(或任何DOM方法)之前,请确保您要访问的元素存在,即DOM已加载。

这可以通过将JavaScript放置在相应的DOM元素之后来确保。

<div id="my_element"></div>

<script>
    var element = document.getElementById('my_element');
</script>

如果这样,您还可以将代码放在关闭body标签(</body>)之前(所有DOM元素都将在执行脚本时可用)。

其他解决方案包括侦听load [MDN]DOMContentLoaded [MDN]事件。在这些情况下,您放置JavaScript代码的位置并不重要,您只需要记住将所有DOM处理代码放在事件处理程序中。

例如:

window.onload = function() {
    // process DOM elements here
};

// or

// does not work IE 8 and below
document.addEventListener('DOMContentLoaded', function() {
    // process DOM elements here
});

请参阅quirksmode.org的文章,了解有关事件处理和浏览器差异的更多信息。 jQuery 首先确保正确加载jQuery。使用浏览器的开发者工具查找jQuery文件是否被找到,如果没有,请纠正URL(例如,在开头添加http:https:方案,调整路径等)。
监听load/DOMContentLoaded事件正是jQuery使用.ready() [docs]所做的。所有影响DOM元素的jQuery代码都应该在该事件处理程序内部。
事实上,jQuery教程明确表示:
几乎我们在使用jQuery时所做的一切都是读取或操作文档对象模型(DOM),因此我们需要确保尽早在DOM准备就绪时开始添加事件等。

To do this, we register a ready event for the document.

$(document).ready(function() {
   // do stuff when DOM is ready
});

或者您也可以使用简写语法:

$(function() {
    // do stuff when DOM is ready
});

两者等效。


19

id选择器不起作用的原因

  1. 指定的元素/DOM不存在。
  2. 元素存在,但未在DOM中注册(针对通过Ajax响应动态添加的HTML节点)。
  3. 存在多个具有相同ID的元素,导致冲突。

解决方案

  1. 尝试在声明后访问元素,或者使用类似于$(document).ready();的内容。

  2. 对于来自Ajax响应的元素,请使用jQuery的.bind()方法。旧版本的jQuery使用.live()

  3. 使用工具(例如,浏览器的Web Developer插件)查找重复的ID并将其删除。


16

如果您要访问的元素在一个iframe内,而您试图在iframe之外的上下文中访问它,这也会导致访问失败。

如果您想在iframe中获取一个元素,请参阅此处


14

正如@FelixKling所指出的,最有可能的情况是您正在寻找的节点尚不存在。

然而,现代开发实践通常可以使用DocumentFragments或直接分离/重新附加当前元素来操作文档树之外的文档元素。这些技术可能作为JavaScript模板的一部分使用,或者用于避免在大量更改相关元素时出现过多的重绘/回流操作。

同样,新的“Shadow DOM”功能正在现代浏览器中推出,允许元素成为文档的一部分,但不可通过document.getElementById和其所有兄弟方法(querySelector等)进行查询。这是为了封装功能并明确隐藏它。

但是,再次强调,最有可能的原因是您要查找的元素尚未(或永久)存在于文档中,您应该像Felix建议的那样去做。但是,您还应该意识到,这不再是元素无法查找(暂时或永久)的唯一原因。


12
如果脚本执行顺序不是问题,那么问题的另一个可能原因是元素没有被正确选择: - getElementById要求传递的字符串必须是ID本身,而且不能带其他字符。如果你在传递的字符串前面加上#符号,并且该ID不以#符号开头,则什么也不会被选中:
  <div id="foo"></div>
  // Error, selected element will be null:
  document.getElementById('#foo')
  // Fix:
  document.getElementById('foo')
同样地,对于getElementsByClassName,不要在传递的字符串前加上.前缀:
  <div class="bar"></div>
  // Error, selected element will be undefined:
  document.getElementsByClassName('.bar')[0]
  // Fix:
  document.getElementsByClassName('bar')[0]
使用querySelector、querySelectorAll和jQuery,要匹配具有特定类名的元素,请在类名前直接添加“.”。同样,要匹配具有特定ID的元素,请在ID前直接添加“#”:
  <div class="baz"></div>
  // Error, selected element will be null:
  document.querySelector('baz')
  $('baz')
  // Fix:
  document.querySelector('.baz')
  $('.baz')

这里的规则在大多数情况下与CSS选择器的规则相同,详细信息可以在此处查看。

  • 要匹配具有两个或更多属性(例如两个类名或一个类名和data-属性)的元素,请将每个属性的选择器放在选择器字符串中彼此相邻,不要用空格分隔它们(因为空格表示后代选择器)。例如,要选择:

      <div class="foo bar"></div>
    

    使用查询字符串.foo.bar来选择。

      <div class="foo" data-bar="someData"></div>
    
    使用查询字符串.foo[data-bar="someData"]来选择下面的<span>
      <div class="parent">
        <span data-username="bob"></span>
      </div>
    

    使用div.parent > span[data-username="bob"]

  • 所有上述内容的大小写和拼写都很重要。如果大小写不同或拼写不同,元素将不会被选择:

  •   <div class="result"></div>
    
      // Error, selected element will be null:
      document.querySelector('.results')
      $('.Result')
      // Fix:
      document.querySelector('.result')
      $('.result')
    
  • 您还需要确保方法的大小写和拼写正确。可使用以下任一方式:

  • $(selector)
    document.querySelector
    document.querySelectorAll
    document.getElementsByClassName
    document.getElementsByTagName
    document.getElementById
    

    任何其他不正确的拼写或大小写都将导致无法正常工作。例如,document.getElementByClassName 将抛出错误。

    确保向这些选择器方法传递字符串。如果您将非字符串内容传递给 querySelectorgetElementById 等方法,几乎肯定无法正常工作。

    如果您要选择的元素的 HTML 属性由引号包围,则必须是纯直引号(单引号或双引号);弯引号(如)在尝试按 ID、类或属性选择时将不起作用。


    0
    我之前在使用Apache Tapestry的时候,对于我的情况上面提到的答案都没有帮助。最后,我发现错误是由于将'id'和't:id'使用相同的值引起的。 希望这能有所帮助。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接