HTML属性(Attributes)とDOMプロパティ(Properties)の違い
(jakearchibald.com)- 同じ要素に付いていても、HTML属性とDOMプロパティは別々の保存領域のように振る舞うため、片方の変更がもう片方に自動反映されないことがある
- 属性はHTMLのシリアライズ結果や開発者ツールの要素パネルに表示されるが、プロパティはDOM専用の状態なので、
outerHTMLのような結果には現れない - 属性値は常に文字列で、名前は大文字小文字を区別しないが、プロパティはオブジェクトなどさまざまな型を持てて、名前は大文字小文字を区別する
id,className,htmlFor,ariaLabelのように属性を反映するプロパティがある一方、input.valueのように現在の状態とデフォルト設定が分離されている例外もある- フレームワークごとに設定対象が異なり、PreactとVueJSは概ねプロパティを、Reactは概ね属性を優先し、Litは
.value構文で両者を明示的に区別する
属性とプロパティは同じ名前でも別物になりうる
- 属性(attributes) と プロパティ(properties) は、互いに別の保存領域のように振る舞うことがある
- ``で
getAttribute('foo')は'bar'を返すが、div.fooはundefinedのことがある - その後
div.foo = 'hello world'を設定しても、foo属性の値は'bar'のまま
- ``で
- フレームワークのテンプレートで `` のような属性風の構文を使っていても、内部では場合によって プロパティ を設定していることがある
- どのケースでプロパティを設定するかはフレームワークごとに異なる
- 設定によってはプロパティと属性の両方を変更する副作用が起きることもある
HTMLシリアライズに残るのは属性
- 属性は HTMLとDOM の両方に存在するが、プロパティはDOMにしか存在しない
div.setAttribute('foo', 'bar')とdiv.hello = 'world'を両方実行しても、div.outerHTMLには `
` のように属性だけが現れる
- ブラウザの開発者ツールの要素パネルは、要素の属性を表示するのであって、プロパティ全体を表示するわけではない
値の型と大文字小文字の扱い
- 属性値はシリアライズされた形式で機能する必要があるため、常に 文字列
- オブジェクトを
setAttributeで渡すと、文字列"[object Object]"として保存される - 同じオブジェクトをプロパティに入れれば、オブジェクト型のまま保持できる場合がある
- オブジェクトを
- 属性名は 大文字小文字を区別しない
HeLlO="world"はDOM上でhello属性に正規化されることがあるsetAttribute('FOO', 'bar')もfoo属性として現れる
- プロパティ名は 大文字小文字を区別する
div.TeSt = 'value'を設定しても、div.testはundefinedのことがある
- 属性名と異なり、属性値は大文字小文字を区別する
属性を反映するプロパティ
- 一部のプロパティは特定の属性を 反映(reflect) する
div.idはid属性を読み書きする getter/setter として動くdiv.id = 'bar'を実行すると、id属性も'bar'に変わる
- 反映関係では 属性 がデータの源泉
- プロパティを設定すると属性が更新される
- プロパティを読むと属性値が読まれる
- 仕様で定義された属性には、利便性のため対応するプロパティが作られることが多い
- 任意の
foo属性は仕様で定義された属性ではないため、対応するfooプロパティが自動的に生えるわけではない
- 任意の
- `` 仕様では、「Content attributes」で属性を、「DOM interface」でプロパティを定義している
reversedとtypeのIDL属性は、同名のコンテンツ属性を reflect しなければならない
名前が変わる反映関係
- 反映する属性とプロパティの名前が異なる場合もある
- JavaScriptのプロパティスタイルに合わせるために名前が変わることがある
- `` の
el.crossOriginはcrossorigin属性を反映する - すべての要素の
el.ariaLabelはaria-label属性を反映する - ARIA反映プロパティは2023年末にクロスブラウザ対応となり、それ以前は属性しか使えなかった
- `` の
- 昔のJavaScript予約語の都合で名前が変わったケースもある
- すべての要素の
el.classNameはclass属性を反映する - `` の
el.htmlForはfor属性を反映する
- すべての要素の
検証、型変換、デフォルト値
- プロパティには 検証とデフォルト値 を持たせられるが、属性そのものにはそうした振る舞いはない
input.typeは属性とプロパティの違いをよく示しているinput.getAttribute('type')がnullでも、input.typeはデフォルト値'text'を返すinput.type = 'number'は属性とプロパティの両方で'number'のように見えるinput.type = 'foo'は属性には'foo'を入れるが、プロパティgetterは無効値とみなして'text'を返す
- 一部のプロパティは 型変換 を行う
- `` では
details.getAttribute('open')は空文字列で、details.openはtrue details.open = falseはopen属性を削除するdetails.open = 'hello'は文字列をそのまま属性に入れるのではなく、booleanに変換してopen属性が存在する状態にする
- `` では
img.heightのようなプロパティは属性値を数値に変換する- setter は入力値を数値化し、負の値は 0 として扱う
input.value は現在値とデフォルト値を分離する
inputにはvalueプロパティとvalue属性があるが、valueプロパティはvalue属性を反映しないvalue属性を反映するプロパティはdefaultValuedefaultValuegetter はvalue属性または空文字列を返すdefaultValuesetter はvalue属性を文字列として設定する
valueプロパティは最初はdefaultValueに従うが、JavaScriptやユーザー操作で設定されると内部値を使う- 初期状態の `` では、
value、defaultValue、getAttribute('value')はすべて'default' input.defaultValue = 'new default'を実行すると、属性とvalueがともに'new default'のように見える- その後
input.value = 'hello!'を実行すると、valueは'hello!'になるが、属性とdefaultValueは'new default'のまま setAttribute('value', 'another new default')を実行しても、現在のvalueは'hello!'のまま維持される
- 初期状態の `` では、
- フォームがリセットされると、内部の
value状態が初期化される形で動作しうる
属性は設定、プロパティは状態により自然
- 属性は 設定(configuration)、プロパティは 状態(state) に使うという区別が自然
- `` は名前を除けばこの区別に合った例
value属性はデフォルト値を設定するvalueプロパティは現在の状態を提供する
- プロパティの読み書きでは検証が適用され、属性の読み書きでは検証が適用されないという違いも、この考え方に合う
とは開いた状態をopen属性で表し、ブラウザがユーザー操作に応じてこの属性を直接追加・削除する- この設計は、属性が設定のためのものだという区別を崩している
- DOMを管理するフレームワークや通常のJavaScriptコードは、DOMが自ら変化する状況を考慮する必要がある
- 代替設計としては、`` のようなデフォルト設定属性、現在状態を読み書きする
details.openプロパティ、状態を対象にするCSS疑似クラスに分けることができる - Simon Peters がこれに関する 初期設計議論 を見つけている
contenteditableもこの区別を崩すが、多くの破綻を受け入れる opt-in とみなされている
フレームワークごとの扱い
- PreactとVueJSは、一部の事前定義された例外を除けば
propName in elementが真のとき プロパティ を設定し、そうでなければ属性を設定する- 基本的には属性よりプロパティを優先する
- 文字列レンダリングメソッドは逆に振る舞い、プロパティ専用の項目は無視する
- 関連実装: Preact の
setProperty, VueJS のshouldSetAsProp
- Reactは、一部の事前定義された例外を除けば 属性 を設定する
- lit-htmlは属性とプロパティの違いを構文として保持している
- `` のように名前の前に
.を付けると、属性ではなく プロパティ を設定する - 関連文書: Lit expressions
- `` のように名前の前に
3件のコメント
Hacker News の意見
data- 属性*を使うと、記事で扱われている方式の間のちょうどよい妥協案になる
これらの属性は JavaScript では
datasetの読み書き可能なプロパティとして自動的に公開され、document.getElementById('myDiv').dataset.payload == "something"のようにアクセスできるただし HTML 属性は
kebab-case、JavaScript はcamelCaseなので、data-my-cool-data-attributeがdataset.myCoolDataAttributeになる点には注意が必要camelCaseを使う複数のエコシステム全体で、大文字の略語をどう扱うかについて確立された慣例がないため、dataset.myIDのようなケースが曖昧になるkebab-caseとcamelCaseの変換を避けるために、Element.hasAttribute('data-thing')、Element.getAttribute('data-thing')、Element.setAttribute('data-thing', '...')のようなメソッドを使っているDOM で検査したりデバッグしたりするときに不自然な変換を挟まずに済むので、意図がずっと明確になる
また https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att... では、
data-*グローバル属性を「スクリプトが HTML と DOM 表現の間で独自の情報を交換できるようにするカスタムデータ属性」と説明しているここで言うコンテキストは、必ずしも別々の
iframeを意味するわけではなく、ブラウザ拡張機能のように同じ DOM にアクセスする別個の JavaScript 実行コンテキストを作る場合を指す開発者ツールプロトコルの用語では、こうしたものを isolated worlds とも呼ぶ
記事で扱われているものは、人生の役にはほとんど立たない複雑さと挙動を示している
こうした挙動には利点がなく欠点だけがあり、仕様は単に DOM オブジェクトと JavaScript オブジェクトを同じものとして定義し、属性とプロパティも同じ概念にすべきだった
DOM ノードに 文字列ではない値を保存できることには明確な利点があり、軽く流せる話ではない
すべてのプロパティが文字列だけでなければならないと要求しない限り、まったく単純ではない
数値との変換、オブジェクトを設定できるプロパティ、属性に対応しないプロパティまで考慮する必要がある
それでも 100% 固定された慣例やルールがあればもっとよかっただろうが、それは単純なことではない
addEventListenerのようなメソッドは個々の DOM 要素のプロトタイププロパティだが、文字列属性として見ると意味をなさないそうなると DOM 要素のメソッドは
HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));のような自由関数になるはずだが、好みの問題ではあるにせよ、JavaScript ではオブジェクトメソッド呼び出しを使えるほうがよいDOM 要素には
setAttributeで状態を変える非常に具体的なインターフェイスがあり、そのインターフェイスを通じて DOM にメッセージを送るために作られた JavaScript オブジェクトにただプロパティを付けたとき、なぜ自動的にルーティングされると期待すべきなのか疑問だそういう API のほうが良い体験になり得るかもしれないが、いくつか難題があり、それでもよりすっきりはしそうだ
バックエンドコードを書きすぎてきたせいか、こういうインターフェイスを見ると内部で奇妙なことがいろいろ起きていて、昔の親切な開発者ができる限り安全なラッパーを渡してくれたように感じる
理解の助けになる見方は、狭義の属性が DOM ノード、つまり
Attrノードだという点このノードは
name、value、ownerElement、prefixのようなプロパティを持つことができ、全体としては配列ではなくNamedNodeMapの形で、所有要素のattributesプロパティ値としてアクセスできるそのため
getAttribute(name)という名前は誤解を招きやすく、実際には属性ノードそのものではなく、その属性ノードのvalueプロパティを返すので、getAttributeValue(name)に近い参考: https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Attr
setAttribute、getAttribute、getAttributeNames、hasAttributeではできず、属性ノードやel.attributesでしかできない実用的なことがあるのかは分からない属性ノードを要素間で移動することもできないし、要素内で属性ノードの順序を変えることもできない
Jake、それならこれはどうするのか: https://jakearchibald.com/2023/against-self-closing-tags-in-...
プロパティを更新しないようにしようという理屈は理解できるが、CSS で対象を指定できる点は有用
:checkedと同じように、疑似クラスもあるべきだと言ったということデバッガを経由するより、DOM を即座に確認するほうがデバッグがはるかに簡単
おそらく最も重要な違いは永続性だが、記事では扱われていない
属性は、ページを離れないなど、その DOM ツリーの同じルートオブジェクトにアクセスできる間は維持される
オブジェクトプロパティは、そのオブジェクトがガベージコレクションされるまでしか維持されない
DOM ノードが複製されたり、シリアライズ/デシリアライズを経たりすると、同じ属性を持つまったく別の DOM ノードになるため、別のノードのデフォルト値ではないプロパティは持たなくなる
興味深い記事
JavaScript の DOM 要素をいつもプロキシのように見ていた
実際の DOM 要素を操作できる一方で、同時にただの JavaScript オブジェクトでもあるので、実際の要素とやり取りする関数を使う以外にも、値を代入して好きなことができるように感じていた
例とともにうまく説明されていて読みやすく、特に見落とされがちな細部がいくつもあるので、多くの人の役に立ちそう
DOM プロパティは IDL attributes とも呼ばれる
仕様の著者以外で
IDL attributesと呼ぶ人は少なく、どちらも属性と呼ぶと非常に紛らわしい良い記事で、より良い表現方法がどのようなものになるのか気になる
HTML が文字列ではない属性を持てて、その値がスコープを持つ
scriptタグのローカル値を参照するような形なら面白そう属性をデフォルト設定として見る考え方は少し奇妙に感じた
たとえば
class属性がデフォルト値だけを示すなら、クラスをトグルしたときに DOM インスペクタや文字列化された HTML で有効なクラスが見えなかったり、無効なクラスが見えたりして、かなり奇妙なことになるはずclass属性はこの仕組みの中でうまく機能する文書の所有者である自分が
class属性を設定し、時間の経過とともにその設定を変えることはできるが、それを行うのは自分だけ別の仕組みなら、ブラウザが hover や focus の状態に反応して自分でクラスを追加・削除することになるだろうが、幸いそうはならず、疑似クラスが使われている
https://emewjin.github.io/html-attribute-dom-property/
attributeとpropertyをどちらも同じ「属性」と訳してしまいましたね..