为什么jQuery或诸如getElementById之类的DOM方法找不到元素?

2022-01-29 00:00:00 jquery dom javascript

document.getElementById$("#id") 或任何其他 DOM 方法/jQuery 选择器找不到元素的可能原因是什么?p>

示例问题包括:

  • jQuery 静默地绑定事件处理程序失败
  • jQuerygetter"方法(.val().html().text())返回 undefined
  • 返回 null 的标准 DOM 方法会导致以下几种错误:
<块引用>

未捕获的类型错误:无法设置属性 '...' of null
未捕获的 TypeError:无法设置 null 的属性(设置...")
未捕获的类型错误:无法读取 null
的属性..."未捕获的类型错误:无法读取 null 的属性(正在读取 '...')

最常见的形式是:

<块引用>

未捕获的类型错误:无法设置属性 'onclick' of null
未捕获的类型错误:无法读取 null
的属性addEventListener"未捕获的类型错误:无法读取 null 的属性样式"

解决方案

您尝试查找的元素不在 DOM.

依赖于 DOM 的脚本的位置可能对其行为产生深远的影响.浏览器从上到下解析 HTML 文档.元素被添加到 DOM 中,并且脚本(通常)在遇到时执行.这意味着顺序很重要.通常,脚本无法找到稍后出现在标记中的元素,因为这些元素尚未添加到 DOM.

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

<script>console.log("脚本 #1:", document.getElementById("test"));//空值</脚本><div id="test">测试div</div><脚本>console.log("脚本 #2:", document.getElementById("test"));//<div id="测试" ...</script>

那么,你应该怎么做?你有几个选择:

<小时>

选项 1:移动脚本

鉴于我们在上面的示例中看到的情况,一个直观的解决方案可能是简单地将您的脚本移到标记下方,越过您想要访问的元素.事实上,长期以来,将脚本放在页面底部被认为是 最佳实践 出于各种原因.以这种方式组织,将在执行脚本之前解析文档的其余部分:

<body><button id="test">点我</button><脚本>document.getElementById("test").addEventListener("click", function() {console.log("点击:", this);});</脚本></body><!-- 结束正文标签-->

虽然这是有道理的,并且是旧版浏览器的可靠选择,但它是有限的,并且有更灵活、更现代的方法可用.

<小时>

选项 2:defer 属性

虽然我们确实说过脚本是,(通常)在遇到它们时执行,"现代浏览器允许您指定不同的行为.如果您要链接外部脚本,则可以使用 defer 属性.

<块引用>

[defer,一个布尔属性,] 被设置为向浏览器指示脚本是在文档被解析之后,但在触发 DOMContentLoaded.

这意味着您可以将带有 defer 标记的脚本放置在任何地方,甚至是 <head>,并且它应该可以访问完全实现的 DOM.p>

<script src="https://gh-canon.github.io/misc-demos/log-test-click.js"延迟></脚本><button id="test">点我</button>

请记住...

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

<小时>

选项 3:模块

根据您的要求,您可以使用 JavaScript 模块.与标准脚本的其他重要区别(在此注明),模块会自动延迟,并且不限于外部源.

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

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

<小时>

选项 4:延迟事件处理

为在您的文档被解析后触发的事件添加一个监听器.

DOMContentLoaded 事件

DOMContentLoaded在从初始解析完全构建 DOM 后触发,无需等待样式表或图像等内容加载.

<script>document.addEventListener("DOMContentLoaded", function(e){document.getElementById("test").addEventListener("click", function(e) {console.log("点击:", this);});});</脚本><button id="test">点我</button>

窗口:加载事件

加载 事件在 DOMContentLoaded 和其他资源(如样式表和图像)已加载后触发.出于这个原因,它的触发时间比我们预期的要晚.不过,如果您正在考虑使用 IE8 等较旧的浏览器,则支持几乎是普遍的.当然,您可能需要 polyfill 用于 addEventListener().

<script>window.addEventListener("加载", function(e){document.getElementById("test").addEventListener("click", function(e) {console.log("点击:", this);});});</脚本><button id="test">点我</button>

jQuery 的 ready()

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

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

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></脚本><脚本>$(函数(){$("#test").click(function() {console.log("点击:", this);});});</脚本><button id="test">点我</button>

<小时>

选项 5:事件委托

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

当一个元素引发一个事件时(前提是它是一个 冒泡 事件并且没有任何东西阻止它的传播),该元素祖先中的每个父元素,一直到 window,也接收到该事件.这允许我们将处理程序附加到现有元素并采样事件,因为它们从其后代中冒出......甚至是在附加处理程序后添加的后代.我们所要做的就是检查事件以查看它是否由所需元素引发,如果是,则运行我们的代码.

通常,此模式保留给在加载时不存在的元素或避免附加大量重复的处理程序.为了效率,选择目标元素最近的可靠祖先而不是附加到 document.

原生 JavaScript

<div id="ancestor"><!-- 我们脚本可用的最近祖先 --><脚本>document.getElementById("ancestor").addEventListener("click", function(e) {如果(e.target.id ===后代"){console.log("点击:", e.target);}});</脚本><button id="descendant">点我</button></div>

jQuery 的 on()

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

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></脚本><div id="ancestor"><!-- 我们脚本可用的最近祖先--><脚本>$("#ancestor").on("点击", "#descendant", function(e) {console.log("点击:", this);});</脚本><button id="descendant">点我</button></div>

What are the possible reasons for document.getElementById, $("#id") or any other DOM method / jQuery selector not finding the elements?

Example problems include:

  • jQuery silently failing to bind an event handler
  • jQuery "getter" methods (.val(), .html(), .text()) returning undefined
  • A standard DOM method returning null resulting in any of several errors:

Uncaught TypeError: Cannot set property '...' of null
Uncaught TypeError: Cannot set properties of null (setting '...')
Uncaught TypeError: Cannot read property '...' of null
Uncaught TypeError: Cannot read properties of null (reading '...')

The most common forms are:

Uncaught TypeError: Cannot set property 'onclick' of null
Uncaught TypeError: Cannot read property 'addEventListener' of null
Uncaught TypeError: Cannot read property 'style' of null

解决方案

The element you were trying to find wasn’t in the DOM when your script ran.

The position of your DOM-reliant script can have a profound effect upon its behavior. Browsers parse HTML documents from top to bottom. Elements are added to the DOM and scripts are (generally) executed as they're encountered. This means that order matters. Typically, scripts can't find elements which appear later in the markup because those elements have yet to be added to the DOM.

Consider the following markup; script #1 fails to find the <div> while script #2 succeeds:

<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>

So, what should you do? You've got a few options:


Option 1: Move your script

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 -->

While this makes sense, and is a solid option for legacy browsers, it's limited and there are more flexible, modern approaches available.


Option 2: The defer attribute

While we did say that scripts are, "(generally) executed as they're encountered," modern browsers allow you to specify a different behavior. If you're linking an external script, you can make use of the defer attribute.

[defer, a Boolean attribute,] is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded.

This means that you can place a script tagged with defer anywhere, even the <head>, and it should have access to the fully realized DOM.

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

Just keep in mind...

  1. defer can only be used for external scripts, i.e.: those having a src attribute.
  2. be aware of browser support, i.e.: buggy implementation in IE < 10


Option 3: Modules

Depending upon your requirements, you may be able to utilize JavaScript modules. Among other important distinctions from standard scripts (noted here), modules are deferred automatically and are not limited to external sources.

Set your script's type to module, e.g.:

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


Option 4: Defer with event handling

Add a listener to an event which fires after your document has been parsed.

DOMContentLoaded event

DOMContentLoaded fires after the DOM has been completely constructed from the initial parse, without waiting for things like stylesheets or images to load.

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

Window: load event

The load event fires after DOMContentLoaded and additional resources like stylesheets and images have been loaded. For that reason, it fires later than desired for our purposes. Still, if you're considering older browsers like IE8, the support is nearly universal. Granted, you may want a polyfill for addEventListener().

<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's ready()

DOMContentLoaded and window:load each have their caveats. jQuery's ready() delivers a hybrid solution, using DOMContentLoaded when possible, failing over to window:load when necessary, and firing its callback immediately if the DOM is already complete.

You can pass your ready handler directly to jQuery as $(handler), e.g.:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script>
  $(function() {
    $("#test").click(function() {
      console.log("clicked:", this);
    });
  });
</script>
<button id="test">click me</button>


Option 5: Event Delegation

Delegate the event handling to an ancestor of the target element.

When an element raises an event (provided that it's a bubbling event and nothing stops its propagation), each parent in that element's ancestry, all the way up to window, receives the event as well. That allows us to attach a handler to an existing element and sample events as they bubble up from its descendants... even from descendants added after the handler was attached. All we have to do is check the event to see whether it was raised by the desired element and, if so, run our code.

Typically, this pattern is reserved for elements which don't exist at load-time or to avoid attaching a large amount of duplicate handlers. For efficiency, select the nearest reliable ancestor of the target element rather than attaching to document.

Native 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's on()

jQuery makes this functionality available through on(). Given an event name, a selector for the desired descendant, and an event handler, it will resolve your delegated event handling and manage your this context:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></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>

相关文章