HTML2024-07-02

HTML attributes 和 DOM properties

虽然都翻译为 '属性', 但 HTML 属性和 DOM 属性是根本不同的东西.

关键不同点

HTML 序列化

Attributes 会序列化, 但 properties 不会, 因此在开发者工具中, 只能看到 attributes 而不是 properties.

const div = document.createElement('div');

div.setAttribute('foo', 'bar');
div.hello = 'world';

console.log(div.outerHTML); // <div foo="bar"></div>
console.log(div.hello); // undefined

值类型

为了可以序列化, attributes 必须是字符串, 而 properties 可以是任何类型.

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' }

大小写敏感

Attributes 大小写不敏感, 但 properties 大小写敏感.

<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 中存在一个 idgettersetter 来映射 id attribute.

当 property 对应 attribute 时, attribute 是数据源. 当设置 property 时, 更新 attribute. 当读取 property 时, 读取 attribute.

为了方便, 大多数规范会为每个定义的 attribute 创建对应的 property.

命名区别

有时 property 和 attribute 会有不同的命名.

有时只是为了区分大小写:

  • <img> 中, el.crossOrigin 会映射为 crossorigin attribute.
  • 在所有元素中, el.ariaLabel 会映射为 aria-label attribute.

有时由于旧的 JavaScript 保留字, property 和 attribute 会有不同的命名:

  • el.className 会映射为 class attribute.
  • <label> 中, el.htmlFor 会映射为 for attribute.

验证, 强制类型和默认值

Properties 有验证和默认值, 但 attributes 没有.

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'.

一些 properties 执行强制类型:

<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')); // ''
    console.log(details.open); // false

    details.open = 'hello';
    console.log(details.getAttribute('open')); // ''
    console.log(details.open); // true
</script>

input 中的 value

value property 不映射 value attribute, 实际上 value property 不映射任何 attribute.

最初 value property 遵循 defaultValue property. 一旦通过 JavaScript 或用户交互设置新的 value property, 就会使用内部 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);
    }

    // reset
    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'

    input.value = 'hello';
    console.log(input.getAttribute('value')) // 'hello'
    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>

Attributes 应当用于配置

Attributes 应该用于配置, 而 properties 可以包含状态.

<input value> 就是对的, value attribute 配置默认值, 而 value property 给出当前状态.

验证时 getter/setter 的是 property, 而不是 attribute.

<detail><dialog> 通过 open attribute 代表元素状态, 并且浏览器通过自己添加 / 删除这个属性响应用户交互. 这种设计打破了 attributes 应该用于配置的设计.

框架如何处理不同

<input className="..." type="..." aria-label="..." value="..." />

Preact 和 VueJS

如果 propName in element, 它们将 prop 设置为 property, 否则设置为 attribute.

基本上它们更喜欢 property 而不是 attribute.

  • setProperty in Preact
  • shouldSetAsProps in VueJS

React

React 设置 attribute, 这使它们的 render-to-string 方法更符合逻辑.

这也解释了为什么自定义元素在 React 中似乎没有效果. 因为它们是自定义的, 它们的属性不在 React 的预定义列表中, 所以它们都设置为 attribute.

React 使用 className 替代 class, 看起来像一个 attribute. 但尽管使用 property, React 也会在底层设置 class attribute.

  • setProp in React