【JavaScript入門講座】 関数に状態を持たせる!? JavaScriptでできる意外な活用法

はじめに

JavaScriptの関数は、実は単なる命令の集合ではなく「オブジェクト」である。そのため、関数自体にプロパティを自由に追加して、状態を持たせたり設定値を保存したりすることができる。これは一見地味だが、設計次第では非常に強力で、モジュールのような役割も果たせる。

本記事では、「関数にプロパティを持たせて使う技術」について、基本の使い方から内部的な仕組み、利点・欠点、そして具体的な活用例まで網羅的に解説する。

実装例

function Log(msg) {
  if (!Log.enabled) return;
  console.log(msg);
}
Log.enabled = true;

Log("Hello!"); // 表示される
Log.enabled = false;
Log("Goodbye!"); // 出力されない

【基本】JavaScriptの関数はObject

JavaScriptでは、関数は実際には Function 型のオブジェクト。ゆえに、プロパティやメソッドを追加できる。

これは正式な文法:

function myFunc() {
  console.log("called");
}

myFunc.count = 0;
myFunc.count++;

【使い方】例素的なパターン

1. 関数の状態を保持

function counter() {
  counter.count++;
  console.log(counter.count);
}
counter.count = 0;

counter(); // 1
counter(); // 2

2. ログ関数に設定を付加

function log(msg) {
  if (!log.enabled) return;
  console.log(msg);
}
log.enabled = true;

3. キャッシュを持つAPI

function fetchData(url) {
  if (fetchData.cache[url]) return fetchData.cache[url];
  const result = fetch(url);
  fetchData.cache[url] = result;
  return result;
}
fetchData.cache = {};

【内部構造】なぜこれが可能なのか

  • JavaScriptの関数は「呼び出せるオブジェクト」
  • そのため Log.enabledmyFunc.count などの「プロパティ追加」が可能
  • typeof Log"function" でありながら、 Log instanceof Objecttrue

言わば、「操作の動作を持ったモジュール」


【メリット】利点

  • 関数に設定や状態を一体にして持たせられる
  • 外部に別変数を作らなくても良い
  • グローバル変数が溜らない
  • import { Log } だけで全部控制できる

【デメリット】注意点

  • thisではない ことに注意。あくまで Log.enabledLog自身のプロパティ
  • 毎回助長に加えるのはパフォーマンスが落ちる原因になる
  • 読み手には「関数なのに状態あるの笑」って言われる可能性

【実用例】

ゲーム内ログ出力用 Log()

function Log(typeOrLabel, labelOrData, ...rest) {
  if (!Log.enabled) return;
  // 出力ロジック
}
Log.enabled = true;
Log.level = "debug";

【応用テクニック】

1. Proxyやgetterとの組み合わせ

関数のプロパティに getter を仕込むと、動的に値を返す関数オブジェクトを作れる。

function Config() {}
Object.defineProperty(Config, "timestamp", {
  get() { return Date.now(); }
});

console.log(Config.timestamp); // 呼ぶたびに現在時刻

Proxyを使えば、存在しないプロパティへのアクセスを横取りして挙動を変えられる。

const Dynamic = new Proxy(function() {}, {
  get(target, prop) {
    return `Property '${prop}' was accessed!`;
  }
});

console.log(Dynamic.hello); // "Property 'hello' was accessed!"

2. Function.prototypeやbind()との併用注意点

  • Function.prototype を直接いじるのは避けるべき。全関数に影響が及び、予期せぬバグの原因になる。
  • bind() を使うと、新しい関数オブジェクトが生成される。そのため boundFunc.enabled のようなプロパティはコピーされず失われるので注意。
function Log(msg) { console.log(msg); }
Log.enabled = true;

const Bound = Log.bind(null);
console.log(Bound.enabled); // undefined !!

3. ライブラリ的応用(設定付きユーティリティ)

関数に設定を持たせることで、小さなライブラリ風の設計ができる。

function i18n(key) {
  return i18n.dict[key] || key;
}
i18n.dict = { hello: "こんにちは", bye: "さようなら" };

console.log(i18n("hello"));

【パフォーマンス面】Hidden Classの最適化

JavaScriptエンジン(V8など)は、動的オブジェクトを高速に扱うために Hidden Class という仕組みを持つ。

  • オブジェクトに最初にプロパティが追加された時、その形状に応じた「隠れたクラス」が生成される
  • 同じ形状ならそのHidden Classを再利用するので高速
  • しかし、プロパティを後から追加・削除すると Hidden Class が差し替わり、最適化が外れる(deopt)

具体例

function Example() {}
Example.a = 1;   // Hidden Class生成
Example.b = 2;   // 形が変化し、新しいHidden Class生成

こうした変化が頻繁に起きると、エンジンが最適化できずパフォーマンスが落ちる。

実践的な注意点

  • プロパティは初期化時にまとめて設定しておくと良い
  • 頻繁に追加・削除を繰り返すのは避ける
  • Log.enabled = true のように一度設定したらそのまま使い続けるのはほぼ問題なし

Inline Cache(インラインキャッシュ)

V8などはプロパティアクセスを最適化する際、インラインキャッシュを利用する。つまり「このオブジェクトのこのプロパティはここにある」という情報をキャッシュして、高速に再利用する。

  • ブジェクト構造が安定していれば超高速
  • しかし途中でHidden Classが変わるとキャッシュが無効化され、遅くなる

配列最適化解除

配列も内部的には「packed array(密な配列)」として最適化されている。

  • arr[0] = 1; arr[1] = 2; のように順序良く詰めると高速
  • 途中で arr[1000] = 42; のように大きく飛んだ添字を入れると「sparse array(疎な配列)」に変換されてしまい、最適化が解除される
  • さらに、配列に非数値プロパティを付与すると(例: arr.foo = 123)、配列としての最適化も外れる
const arr = [1, 2, 3];
arr.foo = "bar"; // 配列最適化が外れる

実測ベンチマーク例

以下のようなコードで性能差を体感できる。

console.time("stable");
for (let i = 0; i < 1e7; i++) {
  const obj = { a: 1, b: 2 };
  obj.a++;
}
console.timeEnd("stable");
console.time("unstable");
for (let i = 0; i < 1e7; i++) {
  const obj = {};
  obj.a = 1;
  obj.b = 2; // Hidden Class変更が発生
  obj.a++;
}
console.timeEnd("unstable");

多くの場合 stable の方が数倍速くなる。Hidden Classが安定しているかどうかが、JIT最適化に直結するためだ。

配列でも同様に:

console.time("packed");
const arr1 = [];
for (let i = 0; i < 1e7; i++) arr1.push(i);
console.timeEnd("packed");
console.time("sparse");
const arr2 = [];
for (let i = 0; i < 1e7; i++) arr2[i * 2] = i; // 飛び飛びに代入
console.timeEnd("sparse");

sparse の方は劇的に遅くなるはず。


【まとめ】

JavaScriptの関数は「命令の集合」にとどまらず、「サービス属性を持つオブジェクト」として使うことができる。

  • 状態や設定を抱え込ませる
  • Proxyやgetterと組み合わせて動的に拡張
  • bind() ではプロパティが消える点に注意
  • Hidden Classの仕組みを理解しておくとパフォーマンス上も安心
  • 小さなライブラリ設計にも応用可能

関数の設計を「その手札だけで完結させる」ための指向性のひとつとして、この技術を理解しておくことは大きな利点になる。