JavaScriptのクラス変数

javaScriptのクラス構文にはクラス変数を定義する構文がありません。
私はWebComponentsのカスタムタグを作っていた時にこれのおかげで大変困りました。
Webでも色々検索して見ましたがclass構文を使った時のクラス変数の定義の仕方を見つけられませんでした。
方法を見つけたのでメモしておきます。

サンプルコード

大分省略しますが困ったのは次の様なコードです。

HogeTag.html

<template id="HogeTag">
	タグのテンプレートが書いてある。
</template">
<script">
(function(){
	class HogeTag extends HTMLElement {
		constructor(){
			super();

			const sr = this.attachShadow({mode: "open"});
			const doc = document.currentScript.ownerDocument;
			const tmpl = doc.getElementById("HogeTag");
			const clone = document.importNode(tmpl.content, true);
			sr.appendChild(clone);
		}
	}

	customeElements.define("hoge-tag", HogeTag);
})();
</script">

index.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>sample</title>
	<link rel="import" href="HogeTag.html">
</head>
<body>
	<div id="Main">
		<hoge-tag">何か子要素</hoge-tag>
	</div>
	<script>
	const h = document.createElement("hoge-tag");
	document.getElementById("Main").appendChild(h);
	</script>
</body>
</html>

上のコードはindex.htmlでHogeTag.htmlで定義した<hoge-tag>をJSで動的生成し、<div id="Main">の子要素として追加しようとしているだけです。

このコードはindex.htmlのJavaScript実行時にHogeTag.htmlの12行目でエラーになります。
理由はdocがnullだからgetElementById()が無いためです。

ちなみにindex.htmlからJavaScriptのコードを消せばHTML中に書いた<hoge-tag>は正しく表示されます。

何で?

理由はすぐに分かりました。

HTML中に書いた<hoge-tag>の時は、Hoge.htmlの11行目のdocはHoge.htmlです。
しかし、index.htmlのJavaScriptからcreateElement()した時のHoge.htmlの11行目のdocはindex.htmlです。
何故なら実行しているスクリプト(document.currentScript)がindex.html内にあるからです。

思いついた解決方法は次の3つでした。

  1. 何らかの方法でHoge.htmlを取得してその中を見に行く
  2. HTMLテンプレートをやめてclass HogeTag内で文字列で定義する
  3. Javaのstatic initializerの様な仕組みでクラスロード時にテンプレートを確保する

1は非常に困難です。documentがHogeTag.htmlでなく、に<link rel="import" href="〜〜〜">があったらそれを見に行くことで解決できそうに思えるかもしれません。
しかし、<link rel="import" href="〜〜〜">が2以上あった場合、どれが目的のテンプレートを定義したファイルかを決定できるとは限らないのです。class HogeTagにファイル名"HogeTag.html"を持つ必要がありますが、ファイル名が変更されていた瞬間ダメになります。

2は良い方法です。確実に解決できます。
しかし、<template>とHTMLインポートは要らない子になってしまいます。

3はJavaScriptのclassにその様な構文が用意されていません。

しかし、3の方法は実現可能です。
良い書き方かどうかは分かりませんが。

どうやって?

まず、必要なのは次の前提知識です。

ES6のclass構文は、それ以前のfunctionを使ったクラス定義のシンタックスシュガーである。

この知識から次の結論が導けます。

  1. ES6のclass構文は、それ以前のfunctionを使ったクラス定義のシンタックスシュガーである。
  2. class構文で作ったクラスもfunctionである。
  3. functionはオブジェクトである。
  4. オブジェクトはプロパティを持てる。
  5. オブジェクトのプロパティは後から「obj.prop = value」の書き方で作ることができる。
  6. 結論。「SomeClass.prop = value」とクラスにプロパティを定義できる。これはクラス変数として使える。

更に、「obj.prop = func()」と何らかの関数を呼んでその戻り値を入れることもできるので、Javaのstatic initializerの様なこともできます。むしろ、制限がないのでJavaScriptの方が自由にやれます。

コード例

コード例を示します。

<div id="HogeTarget">ホゲホゲ</div>
<script>
class Hoge {
	get testStr(){
		return Hoge.testStr;
	}
}

Hoge.testStr = document.getElementById("HogeTarget").textContent.trim();

document.write(`Hoge.testStr: ${Hoge.testStr}<br>`);
const a = new Hoge();
document.write(`a.testStr: ${a.testStr}<br>`);
</script>

実行結果は次の通りです。

ホゲホゲ

ちゃんとクラス変数として機能しています。

結論

つまり、JavaScriptのclass構文で定義したクラスはオブジェクトなので、普通にオブジェクトにプロパティを追加する様にすればクラス変数として使える様になるというわけです。
本文に書いた通り、理屈の上で問題は無いように思えます。
しかし、怪しい。あまり良い書き方には思えません。class構文からは推奨されない手段に思えます。
とはいえ、私が困ったカスタムタグのテンプレート取得みたいに他に手段が無いなら仕方ないと思います。少なくとも、理屈の上では問題ないはずなのでこれが原因でエラーになることも無いと思いますし。

追記
次の記事の様にstaticなgetter/setterを用意してそれを使うべきです。