HTML 属性与 DOM 属性

@高效码农  June 28, 2024

属性和特性是完全不同的东西。您可以将同名的属性和特性设置为不同的值。例如:

<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 也会在后台设置属性classclass

lit-html

Lit 的做法有些不同:

<input type="…" .value="…" />

.它保留了属性 (attribute) 和特性 (property) 之间的区别,如果您想设置特性 (property) 而不是属性 (attribute),则需要在名称前加上前缀。

这就是我所知道的关于属性和特性之间的区别的全部内容。如果我遗漏了什么,或者您有疑问,请在下面的评论中告诉我!



添加新评论