6 ポイント 投稿者 GN⁺ 2024-04-27 | 3件のコメント | WhatsAppで共有
  • 同じ要素に付いていても、HTML属性DOMプロパティは別々の保存領域のように振る舞うため、片方の変更がもう片方に自動反映されないことがある
  • 属性はHTMLのシリアライズ結果や開発者ツールの要素パネルに表示されるが、プロパティはDOM専用の状態なので、outerHTMLのような結果には現れない
  • 属性値は常に文字列で、名前は大文字小文字を区別しないが、プロパティはオブジェクトなどさまざまな型を持てて、名前は大文字小文字を区別する
  • id, className, htmlFor, ariaLabelのように属性を反映するプロパティがある一方、input.valueのように現在の状態とデフォルト設定が分離されている例外もある
  • フレームワークごとに設定対象が異なり、PreactとVueJSは概ねプロパティを、Reactは概ね属性を優先し、Litは.value構文で両者を明示的に区別する

属性とプロパティは同じ名前でも別物になりうる

  • 属性(attributes)プロパティ(properties) は、互いに別の保存領域のように振る舞うことがある
    • ``で getAttribute('foo')'bar' を返すが、div.fooundefined のことがある
    • その後 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.testundefined のことがある
  • 属性名と異なり、属性は大文字小文字を区別する

属性を反映するプロパティ

  • 一部のプロパティは特定の属性を 反映(reflect) する
    • div.idid 属性を読み書きする getter/setter として動く
    • div.id = 'bar' を実行すると、id 属性も 'bar' に変わる
  • 反映関係では 属性 がデータの源泉
    • プロパティを設定すると属性が更新される
    • プロパティを読むと属性値が読まれる
  • 仕様で定義された属性には、利便性のため対応するプロパティが作られることが多い
    • 任意の foo 属性は仕様で定義された属性ではないため、対応する foo プロパティが自動的に生えるわけではない
  • `` 仕様では、「Content attributes」で属性を、「DOM interface」でプロパティを定義している
    • reversedtype のIDL属性は、同名のコンテンツ属性を reflect しなければならない

名前が変わる反映関係

  • 反映する属性とプロパティの名前が異なる場合もある
  • JavaScriptのプロパティスタイルに合わせるために名前が変わることがある
    • `` の el.crossOrigincrossorigin 属性を反映する
    • すべての要素の el.ariaLabelaria-label 属性を反映する
    • ARIA反映プロパティは2023年末にクロスブラウザ対応となり、それ以前は属性しか使えなかった
  • 昔のJavaScript予約語の都合で名前が変わったケースもある
    • すべての要素の el.classNameclass 属性を反映する
    • `` の el.htmlForfor 属性を反映する

検証、型変換、デフォルト値

  • プロパティには 検証とデフォルト値 を持たせられるが、属性そのものにはそうした振る舞いはない
  • input.type は属性とプロパティの違いをよく示している
    • input.getAttribute('type')null でも、input.type はデフォルト値 'text' を返す
    • input.type = 'number' は属性とプロパティの両方で 'number' のように見える
    • input.type = 'foo' は属性には 'foo' を入れるが、プロパティgetterは無効値とみなして 'text' を返す
  • 一部のプロパティは 型変換 を行う
    • `` では details.getAttribute('open') は空文字列で、details.opentrue
    • details.open = falseopen 属性を削除する
    • details.open = 'hello' は文字列をそのまま属性に入れるのではなく、booleanに変換して open 属性が存在する状態にする
  • img.height のようなプロパティは属性値を数値に変換する
    • setter は入力値を数値化し、負の値は 0 として扱う

input.value は現在値とデフォルト値を分離する

  • input には value プロパティと value 属性があるが、value プロパティは value 属性を反映しない
  • value 属性を反映するプロパティは defaultValue
    • defaultValue getter は value 属性または空文字列を返す
    • defaultValue setter は value 属性を文字列として設定する
  • value プロパティは最初は defaultValue に従うが、JavaScriptやユーザー操作で設定されると内部値を使う
    • 初期状態の `` では、valuedefaultValuegetAttribute('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は、一部の事前定義された例外を除けば 属性 を設定する
    • この方式は文字列レンダリングメソッドに近いロジックになる
    • カスタム要素のプロパティはReactの事前定義リストにないため属性として設定され、プロパティ専用APIは動かない可能性がある
    • React 19では、カスタム要素についてPreact/VueJSモデルへ移行する予定
    • Reactは className というプロパティ名を属性風の構文として普及させたが、内部的には class 属性 を設定している
    • 関連実装: React の setProp
  • lit-htmlは属性とプロパティの違いを構文として保持している
    • `` のように名前の前に . を付けると、属性ではなく プロパティ を設定する
    • 関連文書: Lit expressions

3件のコメント

 
GN⁺ 2024-04-27
Hacker News の意見
  • data- 属性*を使うと、記事で扱われている方式の間のちょうどよい妥協案になる
    これらの属性は JavaScript では dataset の読み書き可能なプロパティとして自動的に公開され、document.getElementById('myDiv').dataset.payload == "something" のようにアクセスできる
    ただし HTML 属性は kebab-case、JavaScript は camelCase なので、data-my-cool-data-attributedataset.myCoolDataAttribute になる点には注意が必要

    • 大文字・小文字表記の自動変換は本当に嫌い
      camelCase を使う複数のエコシステム全体で、大文字の略語をどう扱うかについて確立された慣例がないため、dataset.myID のようなケースが曖昧になる
    • kebab-casecamelCase の変換を避けるために、Element.hasAttribute('data-thing')Element.getAttribute('data-thing')Element.setAttribute('data-thing', '...') のようなメソッドを使っている
      DOM で検査したりデバッグしたりするときに不自然な変換を挟まずに済むので、意図がずっと明確になる
    • MDN の data attributes ドキュメントは、HTML 構文、JavaScript からのアクセス、CSS からのアクセス、アクセシビリティ上の注意点を扱っている: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Us...
      また https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att... では、data-* グローバル属性を「スクリプトが HTML と DOM 表現の間で独自の情報を交換できるようにするカスタムデータ属性」と説明している
    • これらの属性は、ある程度 CSS からもアクセス可能
    • JavaScript の実行コンテキスト間で通信する、もう一つの興味深い方法でもある
      ここで言うコンテキストは、必ずしも別々の iframe を意味するわけではなく、ブラウザ拡張機能のように同じ DOM にアクセスする別個の JavaScript 実行コンテキストを作る場合を指す
      開発者ツールプロトコルの用語では、こうしたものを isolated worlds とも呼ぶ
  • 記事で扱われているものは、人生の役にはほとんど立たない複雑さと挙動を示している
    こうした挙動には利点がなく欠点だけがあり、仕様は単に DOM オブジェクトと JavaScript オブジェクトを同じものとして定義し、属性とプロパティも同じ概念にすべきだった

    • そんな制約が「単に」存在していたなら、現代の UI フレームワークのほとんどはまったく動かなかったはず
      DOM ノードに 文字列ではない値を保存できることには明確な利点があり、軽く流せる話ではない
    • 「単に」という部分を除けば同意する
      すべてのプロパティが文字列だけでなければならないと要求しない限り、まったく単純ではない
      数値との変換、オブジェクトを設定できるプロパティ、属性に対応しないプロパティまで考慮する必要がある
      それでも 100% 固定された慣例やルールがあればもっとよかっただろうが、それは単純なことではない
    • そうすると DOM インターフェイスに 文字列ではない値を置けず、メソッドも含まれるため、DOM 機能の大半が失われる
    • 属性とプロパティを互いに対称にするのは、既存の DOM API では難しい
      addEventListener のようなメソッドは個々の DOM 要素のプロトタイププロパティだが、文字列属性として見ると意味をなさない
      そうなると DOM 要素のメソッドは HTMLElement.addEventListener(foo, 'some-event', e => console.log(e)); のような自由関数になるはずだが、好みの問題ではあるにせよ、JavaScript ではオブジェクトメソッド呼び出しを使えるほうがよい
    • なぜ両者が異なる必要があるのかよく分からない
      DOM 要素には setAttribute で状態を変える非常に具体的なインターフェイスがあり、そのインターフェイスを通じて DOM にメッセージを送るために作られた JavaScript オブジェクトにただプロパティを付けたとき、なぜ自動的にルーティングされると期待すべきなのか疑問だ
      そういう API のほうが良い体験になり得るかもしれないが、いくつか難題があり、それでもよりすっきりはしそうだ
      バックエンドコードを書きすぎてきたせいか、こういうインターフェイスを見ると内部で奇妙なことがいろいろ起きていて、昔の親切な開発者ができる限り安全なラッパーを渡してくれたように感じる
  • 理解の助けになる見方は、狭義の属性が DOM ノード、つまり Attr ノードだという点
    このノードは namevalueownerElementprefix のようなプロパティを持つことができ、全体としては配列ではなく 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

    • その説明は正しいが、setAttributegetAttributegetAttributeNameshasAttribute ではできず、属性ノードel.attributes でしかできない実用的なことがあるのかは分からない
      属性ノードを要素間で移動することもできないし、要素内で属性ノードの順序を変えることもできない
  • Jake、それならこれはどうするのか: https://jakearchibald.com/2023/against-self-closing-tags-in-...

    • 「私のブログでこの構文を目にすることになるでしょうが、それは Prettier がそうするからで、私は Prettier が本当に好きです」
  • プロパティを更新しないようにしようという理屈は理解できるが、CSS で対象を指定できる点は有用

    • だから入力要素の :checked と同じように、疑似クラスもあるべきだと言ったということ
    • 自分にとっても、ほかのどの理由よりこのケースが大きい
      デバッガを経由するより、DOM を即座に確認するほうがデバッグがはるかに簡単
  • おそらく最も重要な違いは永続性だが、記事では扱われていない
    属性は、ページを離れないなど、その DOM ツリーの同じルートオブジェクトにアクセスできる間は維持される
    オブジェクトプロパティは、そのオブジェクトがガベージコレクションされるまでしか維持されない

    • オブジェクトは属性を持つ DOM ノードが存在する間は存在するので、ライフサイクルは同じ
      DOM ノードが複製されたり、シリアライズ/デシリアライズを経たりすると、同じ属性を持つまったく別の DOM ノードになるため、別のノードのデフォルト値ではないプロパティは持たなくなる
    • ここでは属性とプロパティの既存の違い以外に、実質的な挙動の違いはない
    • その違いが実際にどう現れるのか気になる
  • 興味深い記事
    JavaScript の DOM 要素をいつもプロキシのように見ていた
    実際の DOM 要素を操作できる一方で、同時にただの JavaScript オブジェクトでもあるので、実際の要素とやり取りする関数を使う以外にも、値を代入して好きなことができるように感じていた

  • 例とともにうまく説明されていて読みやすく、特に見落とされがちな細部がいくつもあるので、多くの人の役に立ちそう

  • DOM プロパティは IDL attributes とも呼ばれる

    • その通りだが、意図的にその用語は避けた
      仕様の著者以外で IDL attributes と呼ぶ人は少なく、どちらも属性と呼ぶと非常に紛らわしい
  • 良い記事で、より良い表現方法がどのようなものになるのか気になる
    HTML が文字列ではない属性を持てて、その値がスコープを持つ script タグのローカル値を参照するような形なら面白そう
    属性をデフォルト設定として見る考え方は少し奇妙に感じた
    たとえば class 属性がデフォルト値だけを示すなら、クラスをトグルしたときに DOM インスペクタや文字列化された HTML で有効なクラスが見えなかったり、無効なクラスが見えたりして、かなり奇妙なことになるはず

    • デフォルト設定と言ったのではなく、単に設定と言っただけ
      class 属性はこの仕組みの中でうまく機能する
      文書の所有者である自分が class 属性を設定し、時間の経過とともにその設定を変えることはできるが、それを行うのは自分だけ
      別の仕組みなら、ブラウザが hover や focus の状態に反応して自分でクラスを追加・削除することになるだろうが、幸いそうはならず、疑似クラスが使われている
 
superwoou 2024-04-27

attributeproperty をどちらも同じ「属性」と訳してしまいましたね..