本文译者为 360 奇舞团前端开发工程师
原文标题:HTML attributes vs DOM properties
原文作者:Jake
原文地址:https://jakearchibald.com/2024/attributes-vs-properties/
特性和属性在本质上是不同的东西。您可以将相同名称的特性和属性设置为不同的值。例如:
<div foo="bar">…div>
<script>
const div = document.querySelector('div[foo=bar]');
console.log(div.getAttribute('foo')); // 'bar'
console.log(div.foo); // undefined
div.foo = 'hello world';
console.log(div.getAttribute('foo')); // 'bar'
console.log(div.foo); // 'hello world'
script>
似乎越来越少的开发人员知道这一点,部分原因归功于框架:
<input className="…" type="…" aria-label="…" value="…" />
如果在框架的模板语言中执行以上操作,您将使用类似特性的语法,但在底层,它有时会设置属性,而不是特性,以及何时执行此操作会因框架而异。在某些情况下,它会设置特性和属性作为副作用,但这不是框架的错。
大多数情况下,这些区别并不重要。我认为开发人员可以在不关心特性和属性之间的区别的情况下拥有长期而愉快的职业生涯是件好事。但是,如果您需要深入了解DOM的更低级别,则有所帮助。即使您认为自己了解差异,也许我会涉及您尚未考虑过的一些细节。因此,让我们深入了解一下...
关键区别
在我们深入研究有趣内容之前,让我们先解决一些技术上的差异:
HTML序列化
特性序列化为HTML,而属性不会:
const div = document.createElement('div');
div.setAttribute('foo', 'bar');
div.hello = 'world';
console.log(div.outerHTML); // ''
因此,当您在浏览器开发者工具中查看元素面板时,您只会看到元素上的特性,而不会看到属性。
值类型
为了在序列化格式中工作,特性值始终为字符串,而属性可以是任何类型:
const div = document.createElement('div');
const obj = { foo: 'bar' };
div.setAttribute('foo', obj);
console.log(typeof div.getAttribute('foo')); // 'string'
console.log(div.getAttribute('foo')); // '[object Object]'
div.hello = obj;
console.log(typeof div.hello); // 'object'
console.log(div.hello); // { foo: 'bar' }
大小写区分
特性名称不区分大小写,而属性名称区分大小写。
<div id="test" HeLlO="world">div>
<script>
const div = document.querySelector('#test');
console.log(div.getAttributeNames()); // ['id', 'hello']
div.setAttribute('FOO', 'bar');
console.log(div.getAttributeNames()); // ['id', 'hello', 'foo']
div.TeSt = 'value';
console.log(div.TeSt); // 'value'
console.log(div.test); // undefined
script>
但是,特性值区分大小写。
好吧,事情开始变得模糊的地方是:
反射
看看这个:
<div id="foo">div>
<script>
const div = document.querySelector('#foo');
console.log(div.getAttribute('id')); // 'foo'
console.log(div.id); // 'foo'
div.id = 'bar';
console.log(div.getAttribute('id')); // 'bar'
console.log(div.id); // 'bar'
script>
这似乎与文章中的第一个示例相矛盾,但是上述内容之所以有效,仅因为Element具有一个id getter和setter,可以“反射”id特性。
当属性反射特性时,特性是数据的源。当您设置属性时,将更新特性。当您读取属性时,将读取特性。
为了方便起见,大多数规范将为每个定义的特性创建一个等效的属性。在文章开头的示例中无效,因为foo不是规范定义的特性,所以没有指定的foo属性来反射它。
命名差异
这是相对次要的,但有时属性的名称与它反射的特性的名称不同。
在某些情况下,仅仅和属性有大小写区分:
-
在
上,el.crossOrigin反射crossorigin属性。
-
在所有元素上,el.ariaLabel反射aria-label属性(aria反射器在2023年末变为跨浏览器。在此之前,您只能使用属性)。
在某些情况下,由于旧JavaScript保留字,名称必须更改:
-
在所有元素上,el.className反射class属性。
-
验证、类型强制和默认值
属性带有验证和默认值,而特性没有:
const input = document.createElement('input');
console.log(input.getAttribute('type')); // null
console.log(input.type); // 'text'
input.type = 'number';
console.log(input.getAttribute('type')); // 'number'
console.log(input.type); // 'number'
input.type = 'foo';
console.log(input.getAttribute('type')); // 'foo'
console.log(input.type); // 'text'
在这种情况下,验证由type getter处理。 setter允许无效值'foo',但是当getter看到无效值或无值时,它返回'text'。
有些属性执行类型强制:
<details open>…details>
<script>
const details = document.querySelector('details');
console.log(details.getAttribute('open')); // ''
console.log(details.open); // true
details.open = false;
console.log(details.getAttribute('open')); // null
console.log(details.open); // false
details.open = 'hello';
console.log(details.getAttribute('open')); // ''
console.log(details.open); // true
script>
在这种情况下,open属性是一个布尔值,返回特性是否存在。 setter还会强制类型 - 即使setter给出'hello',它也会转换为布尔值,而不是直接转到特性。
像img.height之类的属性将属性值强制为数字。setter将传入的值转换为数字,并将负值视为0。
input上的value
value是一个有趣的属性。有一个value属性和一个value特性。但是,value属性不反射value特性。相反,defaultValue属性反射value特性。
实际上,value属性不反射任何特性。这并不罕见,有很多这样的属性(出于某种原因复选框上的offsetWidth,parentNode,indeterminate,以及许多其他属性)。
最初,value属性遵从defaultValue属性。然后,一旦通过JavaScript或用户交互设置了value属性,它就会切换到内部值。它就好像实现大致如下:
class HTMLInputElement extends HTMLElement {
get defaultValue() {
return this.getAttribute('value') ?? '';
}
set defaultValue(newValue) {
this.setAttribute('value', String(newValue));
}
#value = undefined;
get value() {
return this.#value ?? this.defaultValue;
}
set value(newValue) {
this.#value = String(newValue);
}
// This happens when the associated form resets
formResetCallback() {
this.#value = undefined;
}
}
所以:
<input type="text" value="default" />
<script>
const input = document.querySelector('input');
console.log(input.getAttribute('value')); // 'default'
console.log(input.value); // 'default'
console.log(input.defaultValue); // 'default'
input.defaultValue = 'new default';
console.log(input.getAttribute('value')); // 'new default'
console.log(input.value); // 'new default'
console.log(input.defaultValue); // 'new default'
// Here comes the mode switch:
input.value = 'hello!';
console.log(input.getAttribute('value')); // 'new default'
console.log(input.value); // 'hello!'
console.log(input.defaultValue); // 'new default'
input.setAttribute('value', 'another new default');
console.log(input.getAttribute('value')); // 'another new default'
console.log(input.value); // 'hello!'
console.log(input.defaultValue); // 'another new default'
script>
如果value特性被命名为defaultvalue,这将更有意义。现在为时已晚。
特性应用于配置
我认为,特性应该用于配置,而属性可以包含状态。我还相信,轻量级DOM树应该有一个单一的所有者。
从这个意义上说,我认为
做得很好(除了命名)。 value特性配置默认值,而value属性提供当前状态。
当获取/设置属性时,应用验证,但获取/设置特性时不应用验证,这也是有道理的。
我说“在我看来”,因为最近的一些HTML元素有所不同。
和
我认为这是一个设计错误。它破坏了特性用于配置的想法,但更重要的是,这意味着负责维护DOM的系统(框架或原生JS)需要为DOM自身的更改做好准备。
我认为它应该是:
<details defaultopen>…details>
并且具有details.open属性以获取/设置当前状态,以及用于针对该状态进行定位的CSS伪类。
更新:Simon Peters找到了一些有关此问题的早期设计讨论。
我想contenteditable也违反了这一合约,但...嗯...这是一种选择,会带来很多破坏。
框架如何处理差异
回到前面的示例:
<input className="…" type="…" aria-label="…" value="…" />
框架如何处理这个?
Preact和VueJS
除了预定义的一组他们偏爱特性的情况外,如果元素中有propName,他们会将prop设置为属性,否则将设置为特性。基本上,他们更喜欢属性而不是特性。他们的render-to-string方法正好相反,并且会忽略仅限于属性的内容。
Preact中的setProperty。VueJS中的shouldSetAsProp。
React
React的做法正好相反。除了预定义的一组他们偏爱属性的情况外,将设置成特性。这使得它们的render-to-string方法在逻辑上类似。
这解释了为什么自定义元素在React中似乎不起作用。由于它们是自定义的,它们的属性不在React的“预定义列表”中,因此它们被设置为特性。任何在自定义元素上仅限于属性的内容都不起作用。这个问题将得到在React 19中得到修复,为了自定义元素,他们将切换到Preact/VueJS模型。
有趣的是,React在一个看起来像特性的地方流行使用className而不是class。但是,即使您使用属性名称而不是特性名称,React也会在幕后设置class特性。
React中的setProp。
lit-html
Lit的处理方式略有不同:
<input type="…" .value="…" />
它保留了特性和属性之间的区别,如果要设置属性而不是特性,则需要在名称前加上.。
Lit的表达式文档。
结论
这基本上是我知道的有关属性和特性之间区别的所有内容。如果有我漏掉的内容,或者您有任何问题,请在下面的评论中告诉我!