HTML 属性与 DOM 属性
属性和特性是完全不同的东西。您可以将同名的属性和特性设置为不同的值。例如:
<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 序列化
属性 (Attribute) 序列化为 HTML,而属性 (Property) 则不会:
const div = document.createElement('div');
div.setAttribute('foo', 'bar');
div.hello = 'world';
console.log(div.outerHTML); // '<div foo="bar"></div>'
因此,当您查看浏览器开发人员工具中的元素面板时,您只会看到元素的属性,而不是属性。
值类型
为了以序列化格式工作,属性值始终是字符串,而属性可以是任何类型:
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
属性来反映它。
这是 的规范<ol>
。“内容属性”部分定义属性,“DOM 接口”定义属性。如果您reversed
在 DOM 接口中单击 ,它将带您进入以下内容:
reversed
和IDL属性type
必须反映同名的各自内容属性。
但有些反射器更复杂……
命名差异
好的,这是相对较小的事情,但有时属性的名称与它所反映的属性的名称不同。
在某些情况下,它只是添加您期望从属性中获得的大小写类型:
- 在 上
<img>
,el.crossOrigin
体现的是crossorigin
属性。 - 在所有元素上,
el.ariaLabel
反映aria-label
属性(aria 反射器在 2023 年底成为跨浏览器的。在此之前,您只能使用属性)。
在某些情况下,由于旧的 JavaScript 保留字,必须更改名称:
- 在所有元素上,
el.className
体现class
属性。 - 在 上
<label>
,el.htmlFor
体现的是for
属性。
验证、类型强制和默认值
属性 (Property) 带有验证和默认值,而特性 (Attribute) 则没有:
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'
在这种情况下,验证由 getter 处理type
。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。
value
在输入字段上
value
很有趣。有一个value
属性和一个value
特性。但是,value
属性并不反映value
特性。相反,defaultValue
属性反映了value
特性。
我知道我知道。
事实上,该value
属性并不反映任何属性。这并不罕见,有很多这样的属性(出于某种原因,复选框上有offsetWidth
、,等等)。parentNode
`indeterminate`
最初,value
属性服从于defaultValue
属性。然后,一旦value
设置了属性(无论是通过 JavaScript 还是通过用户交互),它就会切换到内部值。它的实现大致如下:
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
。现在太晚了。
属性应该用于配置
我认为,属性应该用于配置,而属性可以包含状态。我还认为 light-DOM 树应该只有一个所有者。
从这个意义上来说,我认为<input value>
它是正确的(除了命名之外)。value
属性配置默认值,而value
属性提供当前状态。
在获取/设置属性时应用验证,但在获取/设置特性时从不应用验证,这也是有道理的。
我说“在我看来”,因为最近有几个 HTML 元素做得不同。
<details>
和元素<dialog>
通过属性表示其打开状态open
,并且浏览器将响应用户交互自行添加/删除该属性。
我认为这是一个设计错误。它打破了属性用于配置的理念,但更重要的是,这意味着负责维护 DOM 的系统(框架或原生 JS)需要为 DOM 的自我改变做好准备。
我认为应该是:
<details defaultopen>…</details>
还有一个details.open
用于获取/设置当前状态的属性,以及用于定位该状态的 CSS 伪类。
更新:西蒙·彼得斯(Simon Peters)挖掘了有关此问题的一些早期设计讨论。
我想contenteditable
这也违反了合同,但是......好吧......这是对很多破坏行为的选择。
框架如何处理差异
回到之前的例子:
<input className="…" type="…" aria-label="…" value="…" />
框架如何处理这个问题?
Preact 和 VueJS
除了预定义的一组偏好属性的情况外,它们会将 prop 设置为属性propName in element
,否则它们会设置属性。基本上,它们更喜欢属性而不是属性。它们的渲染到字符串方法则相反,会忽略仅与属性有关的东西。
反应
React 的做法则恰恰相反。除了预定义的一组偏好属性的情况外,它们还会设置属性。这使得它们的渲染到字符串方法在逻辑上类似。
这解释了为什么自定义元素似乎在 React 中不起作用。由于它们是自定义的,它们的属性不在 React 的“预定义列表”中,因此它们被设置为属性。自定义元素上的任何仅属性都不起作用。这将在 React 19 中得到修复,届时他们将切换到自定义元素的 Preact/VueJS 模型。
有趣的是,React 推广了在看起来像className
属性的地方使用而不是。但是,即使你使用的是属性名称而不是属性名称,React 也会在后台设置属性。class
class
lit-html
Lit 的做法有些不同:
<input type="…" .value="…" />
.
它保留了属性 (attribute) 和特性 (property) 之间的区别,如果您想设置特性 (property) 而不是属性 (attribute),则需要在名称前加上前缀。
这就是我所知道的关于属性和特性之间的区别的全部内容。如果我遗漏了什么,或者您有疑问,请在下面的评论中告诉我!