jQueryの使われたコードからjQueryを外そうとしてふと手が止まるものの1つ、$().data
について掘り下げてみます。
なお、コードベースとしては、公式サイトにおいてある、開発版の3.2.1を使います。
2つの機能
$().data
は、元来は「DOM要素にJavaScriptの値を紐付ける」だけの機能だったのですが、あとからHTMLの方にdata-*
属性が出現した結果、以下の2ステップを経るようなメソッドとなってしまいました。
- 読み出し時点で値がなければ、
data-*
属性を参照する - それ以降は、従来通りjQuery内部のデータだけ参照する
ということで、この2つが中途半端につながって、混乱を招く結果となっています。
data-*
属性の読み出し
まず、data-*
属性が読み出されるのは、以下のような場合に限られます。
-
$(elem).data()
と全データをリクエストした場合(ただし、1要素あたり1回限りかつ、その時点でデータがセットされていない場合のみ) -
key
に対応するデータをセットせずに$(elem).data('key')
で値を参照した場合(かつ、jQuery側にundefined
以外のデータが入っていない場合)
以上の条件に当てはまる場合、data-*
属性から値を読み出しますが(camelCase
というキーはdata-camel-case
のように変換されます)、取り出した文字列には以下のような変換が入ります。
-
true
、false
、null
の場合…対応するJavaScriptの値になる - 数値…JavaScriptの数値と同じ表記の場合に限って、数値に変換される(たとえば、
3
は数値に変換されるけど、3.0
や+3
はされない、など) -
{...}
や[...]
の形…JSONとみなして変換を試みる(失敗してもtry-catchして文字列そのままになる)
データの保存/読み出し
では、次はDOMノードへのデータ保存/読み出しをどのようにしているか見ていきましょう。jQueryのソースにはこんなコメントが付いています。
// Implementation Summary
//
// 1. Enforce API surface and semantic compatibility with 1.9.x branch
// 2. Improve the module's maintainability by reducing the storage
// paths to a single mechanism.
// 3. Use the same single mechanism to support "private" and "user" data.
// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
// 5. Avoid exposing implementation details on user objects (eg. expando properties)
// 6. Provide a clear path for implementation upgrade to WeakMap in 2014
ということで、「WeakMap
の導入も視野に入れた」「シンプルな仕組み」が構築されています。
具体的に仕組みを追いかけてみると、Data
というクラスが存在して、インスタンスそれぞれにユニークなIDであるexpando
プロパティを持っています。そして、データをセットできるのは「ノード以外のJavaScriptオブジェクト」「エレメントノード」「ドキュメントノード」に限定されていて、コメントノードやテキストノードは処理から除外されます。
昔はDOMオブジェクトにそのまま値を割り当ててしまうと、DOMとJavaScriptをまたいだ循環参照が発生して、メモリリークしてしまうという問題があったため、expando
名のプロパティにはキーだけ入れて、実際の中身は別に保管する、というような仕組みが必要だったのですが、jQuery 3.2.1はIE 9以上が対象ということで、そのような問題が起きないからか、DOMにexpando
の名前で追加したプロパティに直接値を書き込む、というシンプルな仕組みとなっていました。
…というか、モダンブラウザが前提なら、勝手にDOMへプロパティを増やしても(名前の衝突を別にすれば)問題にならない、ということなんですね。