命名元素 ID 可作为 JavaScript 全局变量引用

Avatar of Matteo Mazzarolo
Matteo Mazzarolo

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

您是否知道具有 ID 的 DOM 元素可以在 JavaScript 中作为全局变量访问?这就像一直存在的东西,但我才第一次深入研究。

如果您是第一次听说这个,请做好心理准备!我们可以通过在 HTML 中添加元素的 ID 来实际看到它的工作原理

<div id="cool"></div>

通常情况下,我们会使用 querySelector("#cool")getElementById("cool") 来定义一个新的变量来选择该元素

var el = querySelector("#cool");

但实际上,我们已经可以访问 #cool,而无需进行这些繁琐的操作。

因此,HTML 中的任何 id(或 name 属性)都可以使用 window[ELEMENT_ID] 在 JavaScript 中访问。同样,这并非完全“新”,但很少见。

正如您可能猜到的,使用命名引用访问全局作用域并不是最好的主意。有些人开始称之为“全局作用域污染者”。我们将在下文中探讨其原因,但首先……

一些背景

这种方法在 HTML 规范 中有 概述,其中将其描述为“Window 对象上的命名访问”。

Internet Explorer 是第一个实现此功能的浏览器。所有其他浏览器也添加了它。Gecko 当时是唯一一个在标准模式下不支持它的浏览器,而是选择将其作为一项实验性功能。他们对是否实现它持犹豫态度,但它 为了浏览器兼容性而继续发展(Gecko 甚至试图 说服 WebKit将其从标准模式中移除),最终在 Firefox 14 中进入标准模式。

可能鲜为人知的一点是,浏览器不得不采取一些预防措施(成功程度各不相同)以确保生成的全局变量不会破坏网页。其中一项措施是……

变量遮蔽

可能最有趣的部分是,命名元素引用不会 遮蔽现有的全局变量。因此,如果 DOM 元素的 id 已被定义为全局变量,它不会覆盖现有的全局变量。例如

<head>
  <script>
    window.foo = "bar";
  </script>
</head>
<body>
  <div id="foo">I won't override window.foo</div>
  <script>
    console.log(window.foo); // Prints "bar"
  </script>
</body>

反之亦然

<div id="foo">I will be overridden :(</div>
<script>
  window.foo = "bar";
  console.log(window.foo); // Prints "bar"
</script>

这种行为至关重要,因为它消除了诸如 <div id="alert" /> 之类的危险覆盖,否则它会通过使 alert API 无效而产生冲突。这种保护技术很可能是您(如果像我一样)第一次了解它的原因。

反对命名全局变量的理由

之前,我说使用命名元素作为全局变量引用可能不是最好的主意。有很多原因,TJ VanToll 在他的博客中很好地介绍了这些原因,我将在下文中进行总结。

  • 如果 DOM 发生变化,那么引用也会发生变化。 这会导致一些非常“脆弱”(规范中使用的术语)的代码,其中 HTML 和 JavaScript 之间的关注点分离可能过于严格。
  • 意外引用太容易了。 一个简单的拼写错误很可能会引用一个命名全局变量,并给您带来意想不到的结果。
  • 它在不同浏览器中的实现方式不同。 例如,我们应该能够访问带有 id 的锚点(例如 <a id="cool">),但一些浏览器(主要是 Safari 和 Firefox)会在控制台中返回 ReferenceError
  • 它可能不会返回您想要的结果。 根据规范,当 DOM 中存在多个相同命名元素的实例时(例如,两个 <div class="cool"> 实例),浏览器应该返回一个包含实例数组的 HTMLCollection。但是,Firefox 只返回第一个实例。再者,规范指出,我们应该在一个元素树中只使用一个 id 实例。但是,这样做不会阻止页面正常工作。
  • 可能存在性能损失吗? 我的意思是,浏览器必须创建那个引用列表并维护它。一些人进行了测试 在这个 StackOverflow 线程中,其中命名全局变量在 一项测试中表现更好,但在 最近的一项测试中表现更差

其他注意事项

假设我们抛开对使用命名全局变量的批评,并继续使用它们。这很好。但在这样做时,您可能需要考虑一些因素。

Polyfill

听起来可能很边缘,但这类全局检查是 Polyfill 的典型设置要求。请查看以下示例,我们使用新的 CookieStore API 设置 Cookie,在不支持它的浏览器上为它提供 Polyfill

<body>
  <img id="cookieStore"></img>
  <script>
    // Polyfill the CookieStore API if not yet implemented.
    // https://mdn.org.cn/en-US/docs/Web/API/CookieStore
    if (!window.cookieStore) {
      window.cookieStore = myCookieStorePolyfill;
    }
    cookieStore.set("foo", "bar");
  </script>
</body>

此代码在 Chrome 中运行良好,但在 Safari 中会引发以下错误。

TypeError: cookieStore.set is not a function

截至目前,Safari 不支持 CookieStore API。因此,由于 img 元素 ID 创建了一个与 cookieStore 全局变量冲突的全局变量,所以没有应用 Polyfill。

JavaScript API 更新

我们可以反转这种情况,并发现另一个问题,即浏览器 JavaScript 引擎的更新可能会破坏命名元素的全局引用。

例如

<body>
  <input id="BarcodeDetector"></input>
  <script>
    window.BarcodeDetector.focus();
  </script>
</body>

该脚本获取对输入元素的引用,并对其调用 focus()。它可以正常工作。但我们不知道它能继续正常工作多久。

您会发现,我们用于引用输入元素的全局变量将在浏览器开始支持 BarcodeDetector API 时停止工作。届时,window.BarcodeDetector 全局变量将不再是输入元素的引用,.focus() 将抛出“window.BarcodeDetector.focus 不是函数”错误。

结论

让我们总结一下我们是如何走到这一步的

  • 所有主要浏览器都会自动创建对每个带有 id(或某些情况下带有 name 属性)的 DOM 元素的全局引用。
  • 通过全局引用访问这些元素不可靠且可能很危险。请改用 querySelectorgetElementById
  • 由于全局引用是自动生成的,它们可能会对您的代码产生一些副作用。这是一个避免使用 id 属性的好理由,除非您真的需要它。

归根结底,避免在 JavaScript 中使用命名全局变量可能是个好主意。我之前引用了规范中关于它会导致“脆弱”代码的内容,但以下列出了完整的文本,以强调这一点

一般来说,依赖这一点会导致脆弱的代码。最终映射到此 API 的 ID 会随着时间的推移而变化,例如,当 Web 平台添加新功能时。请改用 document.getElementById()document.querySelector()

我认为 HTML 规范本身建议避免使用此功能就说明了一切。