👻

HTMLInputElementについて調べてみた

2024/09/11に公開

https://zenn.dev/unkeleven/articles/ad0e43b0179519
この記事を書いた時に、HTMLInputElementなるものに出会い、この周辺の知識が曖昧だと感じたので調べてみた。

MDN - HTMLInputElementは、DOMの一部ですが、DOM自体の記事はたくさんあるので、HTMLInputElement特にvalueプロパティにフォーカスして以下書いていきます。

具体的には以下の内容に触れながらHTMLInputについて調べていきます。

  • プロトタイプ(prototype)
  • アクセサプロパティ(ゲッターとセッター)
  • Object.getOwnPropertyDescriptor()
  • Object.defineProperty()

input要素(オブジェクト)を作成してvalueプロパティについて調べてみる

とりあえず、input要素を作成してみて、console.logでどのようなものか確認してみたいと思います。

const inputElement = document.createElement("input");
console.log(inputElement);

Chromeのdeveloper tools では <input>としか表示されず、具体的な中身がどうなっているのかよく分かりません。
では、以下だとどうでしょうか。
MDN - console.dir()

指定された JavaScript オブジェクトのプロパティをすべてコンソール上で見る方法

console.dir(inputElement);

value含め、いろいろリストされます。

上記のMDNのドキュメントの中の「親インターフェイスである HTMLElement から継承したプロパティもあります。」とのことなので、たくさんプロパティを持っているんだな、と思いつつ、以下を実行してみるとどうでしょうか?
MDN - Object.hasOwnを使って、valueを「自身のプロパティ」として持っているかをチェックしてみます。

console.log(Object.hasOwn(inputElement, 'value')); 

falseとなります。
え、valueを持っていない??

valueプロパティはどこにあるか

となると、どこにあるのでしょうか?
継承とプロトタイプチェーンの仕組みから考えると、自身(inputElement)が指定のプロパティ(value)を持っていない場合はプロトタイプのvalueをチェックしにいくはずです。

console.log(Object.hasOwn(Object.getPrototypeOf(inputElement),"value")) //true;

ありました。
自身のプロパティとしては持っていないけど、プロトタイプには持っている、ということになります。

ちなみに、以下のように__proto__を使ってもtrueになりますが、非推奨です。MDN - Object.prototype.proto

console.log(Object.hasOwn(inputElement.__proto__,"value")); //true

では、中身はどうなっているか、調べようとしますが、以下の方法ではうまくいきません。

console.log(Object.getPrototypeOf(inputElement).value)); 

chrome では、VM1632:1 Uncaught TypeError: Illegal invocation (日本語訳: 不正な呼び出し) と表示されますが、
Firefoxでは、もう少し詳しいエラーが出ます。Uncaught TypeError: 'get value' called on an object that does not implement interface HTMLInputElement. と表示されます。 (日本語訳: HTMLInputElementインターフェースが実装されてないオブジェクトでget valueが呼ばれています)

これらのエラーから分かることは、

  • Object.getPrototypeOf(inputElement).valueによって、valueプロパティにアクセス自体はできている。
  • Object.getPrototypeOf(inputElement)にアクセスすると、get valueという名前のメソッドが呼ばれている。

このようなプロパティにアクセスすると特定のメソッドが実行されるというプロパティがあります。
「アクセサプロパティ」と呼ばれるプロパティです。(データプロパティと対になる概念です)

アクセサプロパティとしてのvalueプロパティ

MDN - JavaScript のデータ型とデータ構造 - プロパティ
現代の JavaScript チュートリアル - プロパティ getters と setters

プロパティには、データプロパティとアクセサ(ー)プロパティという2種類のプロパティがあります。

データプロパティというのは、以下のように、プロパティに対して値が格納されています。
これは初学者でも知っている形式かと思います。

const user = {
  name: "unk",
}
console.log(user.name); //unk

一方で、アクセサプロパティについては、以下のような形で、データプロパティのように.nameでアクセスしますが、そうすると上記のようにプロパティに設定されている値を返すのではなく、getに設定されたnameメソッドやsetに設定されたnameメソッドを実行します。getやsetのことをゲッターセッターと呼びます。

後で、valueプロパティのゲッター、セッターをカスタマイズしますが、このゲッター、セッターという仕組みを利用するメリットとしては、値に取得したり、値を設定する際にその他の処理を挟むことができるという点です。(以下の例は、単にconsole.log()を実行しているだけですが)

const user = {
  get name() {
    console.log(`現在の名前は ${this._name} です`);
    return this._name;
  },

  set name(val) {
    this._name = val;
    console.log(`新しい名前 ${this._name} を設定しました`);
  },
};

user.name = 'unk';
console.log(user.name); // unk

上記のvalueプロパティはこのようなアクセサプロパティです。

Object.getOwnPropertyDescriptor()を使って、valueプロパティの構成を確認する。

では、アクセサプロパティであるvalueプロパティがどのようなものが確認するにはどうしたら良いでしょうか?

先ほどのMDNのドキュメントの以下の部分にもありますが、Object.getOwnPropertyDescriptor() を使います。

それぞれの属性は、JavaScript エンジンが内部でアクセスしますが、Object.defineProperty() で設定したり、Object.getOwnPropertyDescriptor() で読み取ったりすることができます。

MDN - Object.getOwnPropertyDescriptor() というメソッドがあります。与えられたオブジェクトの特定のプロパティの構成を記述したオブジェクトを返してくれます。

console.log(Object.getOwnPropertyDescriptor(inputElement.__proto__, 'value')); 

ちなみに、inputElement.__proto__ === HTMLInputElement.prototype //true なので、以下のようにしても同じ結果が得られます。というか、こちらの表現の方が一般的な気がします。

console.log(Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'));

出力内容についてはざっくりこんな感じになります。

{
  get: f value(),
  set: f value(),
  enumerable: false,
  configurable: true
}

では、さらに、getの中身も見てみましょう。

const descripter = Object.getOwnPropertyDescriptor(inputElement.__proto__, 'value');
console.log(descripter.get);
ƒ value() { [native code] }

「native code」というのは、ブラウザで実装しているコードとなります。これだけみてもどうなっているかわかりませんが、chromeだとオープンソースとして公開されているので見ることができるはずです。c++で書かれているようです。

では、最後に、ゲッターとセッターをカスタマイズしてみます。

valueプロパティのgetterとsetterをカスタマイズする

inputのvalueプロパティをカスタマイズするにあたっては、一応、「valueプロパティのゲッターセッターが元々持っている機能は維持する」という要件にしておきます。(値をセットしたらフォームのテキストボックスに反映される等)

以下は、html内に、
<input id="textInput">という要素がある前提のコードです。
この<input>をinputElementとして取得して、このvalueプロパティをカスタマイズします。
カスタイマイズするためには、MDN - Object.definePropertyを使います。

getsetを上書きしますが、元々のvalueプロパティのgetsetの処理はそのまま使いたいので、originalInputDescriptor.getoriginalInputDescriptor.setというメソッドをそれぞれcallしています。

const originalInputDescriptor = Object.getOwnPropertyDescriptor(
  HTMLInputElement.prototype,
  'value'
);

const inputElement = document.getElementById('textInput');

Object.defineProperty(inputElement, 'value', {
  get: function () {
    console.log('inputのテキストをgetします');
    return originalInputDescriptor.get.call(this); // 元のgetterを呼び出し
  },
  set: function (newValue) {
    originalInputDescriptor.set.call(this, newValue); // 元のsetterを呼び出し
    console.log('newValue: ', newValue);
  },
});

動くものも一応リンクしておきます。

まとめ

  • HTMLInputElementインターフェースが実装されたオブジェクトのvalueプロパティはオブジェクト自身にはなく、プロトタイプに設定されている。
  • valueプロパティは、データプロパティではなくアクセサプロパティであり、ゲッターとセッターを持っている。
  • valueプロパティのゲッターとセッターはObject.definePropertyを使ってカスタマイズできる。なお、元々のゲッターとセッターの機能を保持したい場合は、Object.getOwnPropertyDescriptorを使って取得しておく。

以上です。

Discussion