您是否知道具有 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 元素的全局引用。 - 通过全局引用访问这些元素不可靠且可能很危险。请改用
querySelector
或getElementById
。 - 由于全局引用是自动生成的,它们可能会对您的代码产生一些副作用。这是一个避免使用
id
属性的好理由,除非您真的需要它。
归根结底,避免在 JavaScript 中使用命名全局变量可能是个好主意。我之前引用了规范中关于它会导致“脆弱”代码的内容,但以下列出了完整的文本,以强调这一点
一般来说,依赖这一点会导致脆弱的代码。最终映射到此 API 的 ID 会随着时间的推移而变化,例如,当 Web 平台添加新功能时。请改用
document.getElementById()
或document.querySelector()
。
我认为 HTML 规范本身建议避免使用此功能就说明了一切。
此功能似乎不适用于 Shadow DOM 内部的元素。
非常有趣!我实际上在一段时间前遇到了这个问题,但我不知道发生了什么事。我们有一个类似这样的 Script 标签
但是,每当我尝试访问 `window.config` 时,我得到的都是脚本标签的引用,而不是内联对象。我完全不知道发生了什么,最终只能删除 `id` 属性(因为它没有起到任何作用)。了解到至少有一些逻辑可以解释为什么会发生这种情况,真是太好了。
嘿,Ben!:D
奇怪!`window.config` 在你的例子中应该可以正常工作。想知道可能是什么原因。
嗯?特定标签名称没什么特殊之处,任何不工作的浏览器都违反了规范。当然它在 Firefox 中可以完美运行。我不知道这从哪里来的。
事实上,上周我确实在这个特性中发现了一个明显的规范违反(目前还没有进行提交):如果文档使用 XML 语法加载,Chromium 不会实现它。所以第一个 URL 在 Chromium 中会抛出 ReferenceError 错误(与 Firefox 不同,或者在第二个 URL 中也不同)。
总的来说,它是我最喜欢的激进压缩/代码缩减技巧之一(我在自己使用的临时脚本中经常使用它),但它并不是最健壮的。
这是完全错误的。`window["hello-world"]` 和 `item1` 都能正常工作。(证明:访问 `data:text/html,<p id=hello-world><p id=item1>` 并尝试一下。)
这以及文章中的一些其他内容表明作者并没有理解整个过程的实现方式(如果你不了解 JavaScript 的原型继承模型或 `window` 全局对象的本质,从规范中也看不出来)。不赘述细节:这不是浏览器在你提供带有特定 ID 的元素时设置属性,而是回退,这样当你尝试访问全局对象上不存在的属性时,它会尝试找到合适的对象给你,而不是直接给你 `undefined`。
是的,这个额外说明不应该漏掉 - 我的错。感谢你的反馈!我还会仔细检查你在其他评论中提到的内容。
不,他是对的,它不起作用,你可以在 Chrome 上自己试试,老兄。
另一个“有趣的事实”:相同的机制在 HTMLFormElement 接口上可用,即在所有 `<form>` 节点上。如果你不幸地给任何按钮或输入元素赋予了 `submit` 的 ID,然后尝试调用 `form.submit()`,你会得到一个非常糟糕的惊喜。
哦,微软 IE 文档 `document.all` 对象模型的甜蜜继承。真可惜我们没有在有机会的时候把它移除掉。
我不得不编写一种像这样的解决方法
我知道代码很丑。这是我用来解决 WordPress 评论中一些糟糕的客户端电子邮件过滤器实现的书签代码。
我想知道你的案例中到底发生了什么,因为根据博文的说法,`window.config` 的值应该是内联对象。
老程序员会记得,使用 ID 作为全局变量是使用 Internet Explorer 在 JavaScript 中编程的主要方式。其他浏览器拒绝实现对它的支持。整个行业不得不重写所有 JavaScript 应用程序以改用 `getElementById()`,因为这是符合标准的方式。
但现在我们已经来到这里,没有理由回到旧的方式。
我很难理解“它可能不会返回你所想的东西”。
它不应该说:`<div id="cool">` 而不是 “class”?我知道你不会这样做,但这似乎是你所说的方式?
遗憾的是,这个问题/特性甚至存在于 ES6 模块中。
这实际上是一个很棒的功能!
关于这篇文章中的这个例子
解决此问题的一种方法是,从 `<img>` 元素中删除 `id` 属性。但是,如果无法这样做怎么办?有没有办法修复 JavaScript,使其能够抵抗带有 `cookieStore` 值的 `id` 属性的元素的存在?
这很酷,但使用 ID 本身就存在争议。它与任何其他单例具有相同的缺点:你认为对象是唯一的,并且将永远存在,然后突然你面临着重写代码的必要性。我宁愿通过类名来查询元素集合 - jQuery 的工作方式。