HTMLInputElementについて調べてみた
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を使います。
get
とset
を上書きしますが、元々のvalue
プロパティのget
とset
の処理はそのまま使いたいので、originalInputDescriptor.get
、originalInputDescriptor.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