JavaScript で関数はオブジェクト? なにそれ?
はじめに
JavaScript をある程度書いていると、「関数はオブジェクトでもある」という話を耳にしますよね。そして、「そんなの知らないよ。はいはい。」と右から左に聞き流しているあなたのためにこの記事を書きました。
Function オブジェクトのプロトタイプチェーンや内部スロット、コンストラクタとしての使い方、bind/call/apply の仕組み、Arrow Function や Generator Function、Async Function など、JavaScript の関数が持つさまざまな特徴を解説します。
関数がオブジェクト??
まず最初に、JavaScript の関数は Object を継承した「呼び出せるオブジェクト(callable object)」です。つまり関数として呼び出し可能であるだけではなく、プロパティを持つことができます。関数名を変数のように扱ったり、他の関数に渡したり、あるいはプロパティを追加したりといった柔軟な操作が可能です。
内部スロット [[Call]]/[[Construct]]
JavaScript 仕様(ECMAScript 仕様)では、関数オブジェクトは内部的に [[Call]] というスロットを持っています。これにより、functionObj() のように関数呼び出しができるようになっています。さらに、コンストラクタとして利用可能な関数は [[Construct]] スロットも持ち、new functionObj() のように呼び出すとインスタンスを生成できます。
なお、Arrow Function は [[Call]] は持っていますが [[Construct]] は持たないため、new キーワードを使ってインスタンス化することはできません。
function normalFunction() {}
const arrowFunction = () => {};
new normalFunction(); // OK
new arrowFunction(); // TypeError: arrowFunction is not a constructor
関数オブジェクトのプロトタイプチェーン
関数オブジェクトのプロトタイプチェーンは以下のようになっています。
functionObj
↑ [[Prototype]]
Function.prototype
↑ [[Prototype]]
Object.prototype
↑ [[Prototype]]
null
Function.prototype オブジェクトもまた、Object.prototype を継承しています。したがってすべての関数は Function.prototype からプロパティやメソッドを継承します。よく知られている call, apply, bind などは Function.prototype 上に定義されています。
Function.prototype のメソッド
-
call:
func.call(thisArg, ...args)のように、明示的にthisや引数を指定して呼び出せる。 -
apply:
func.apply(thisArg, [argsArray])のように、引数を配列(もしくは配列風オブジェクト)で指定して呼び出せる。 -
bind:
func.bind(thisArg, ...args)のように、あらかじめthisと引数を束縛した新しい関数を返す。
これらのメソッドは、JavaScript の柔軟性を支える重要な仕組みです。this バインディングを制御したり、可変長引数を配列形式で受け取るなどが簡単に行えるようになります。
関数特有のプロパティ
関数はオブジェクトとしてプロパティを持ちますが、その中でも関数特有のプロパティは重要です。
-
length プロパティ
仮引数の数(レストパラメータやデフォルト値をもたない引数の数)を返します。function example(a, b, c) {} console.log(example.length); // 3Arrow Function でも同様に
lengthは利用可能です。 -
name プロパティ
関数名を返します。名前付き関数の場合はその名前、無名関数の場合は空文字列、ただし ES6 以降の推論によってconst obj = { method() {} };のような場合にはmethodという名前が付与されるケースがあります。function foo() {} console.log(foo.name); // "foo" const bar = function () {}; console.log(bar.name); // "bar" (ブラウザによっては空文字列の場合もあり) -
prototype プロパティ
すべての「通常の」関数はデフォルトでprototypeプロパティを持ち、コンストラクタとして利用したときに生成されるオブジェクトのプロトタイプになります。
Arrow Function やメソッド構文で定義した関数はprototypeを持たないため、コンストラクタとして利用できません。function Foo() {} console.log(Foo.prototype); // Foo オブジェクトのプロトタイプオブジェクト const arrow = () => {}; console.log(arrow.prototype); // undefined
Arrow Function の挙動
Arrow Function (アロー関数) は ES6 で導入された新しい関数の書き方ですが、以下のような従来の関数(function)との違いがあります。
-
thisバインディングが静的
Arrow Function 内のthisは、定義されたときのレキシカルスコープを参照します。通常の関数は呼び出し時にthisが決定されるため、bindなどを使わない限りオブジェクトメソッドとして呼び出せばそのオブジェクトがthisになります。アロー関数はそれが変わりません。 -
argumentsオブジェクトがない
Arrow Function ではargumentsはスコープチェーンの外から参照されるため、通常の関数のように暗黙的にargumentsを使うことはできません。代わりにレストパラメータ(...args)を活用します。 -
newによる呼び出しが不可
Arrow Function は[[Construct]]スロットを持たないので、コンストラクタとして使用できません。prototypeプロパティも持ちません。
拡張
JavaScript の関数にはさらに拡張形として Generator Function、Async Function 、Async Generator Function があります。
Generator Function
Generator Function(ジェネレーター関数)は function* 構文で定義されます。呼び出すとイテレーター(Generator オブジェクト)を返し、yield を使って実行を一時停止・再開できます。
function* genFunc() {
yield 1;
yield 2;
yield 3;
}
const g = genFunc();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
console.log(g.next()); // { value: undefined, done: true }
- Generator Function は
[[Call]]と[[Construct]]を持っているため、new演算子で呼び出すことも technically 可能ですが、あまり一般的な使い方ではありません(Generator がインスタンス化されるわけではないので非推奨に近いです)。 -
prototypeプロパティは通常の関数と同様に持ちますが、実際にはGeneratorFunctionのインスタンスであり、Generatorオブジェクトを返す特殊な関数です。
Async Function
Async Function は async function 構文で定義され、await キーワードを使用できます。非同期処理(プロミスを返す処理)をより直感的に書けるようになります。
async function asyncFunc() {
const result = await fetchSomeData();
return result;
}
- Async Function も
[[Call]]と[[Construct]]スロットを持っています。ただし、Async Function は呼び出すと常にPromiseを返します。 -
async functionもprototypeプロパティを持ちますが、通常コンストラクタとして呼び出すことはありません。
Async Generator Function
Async Generator Function は async function* 構文で定義され、非同期イテレーターを返す関数です。await と yield を組み合わせて、非同期のデータストリームを逐次処理することが可能です。
async function* は、非同期イテレーターを返す関数を定義します。この関数内では await と yield を組み合わせて使用でき、非同期操作の結果を逐次 yield することができます。
async function* asyncGenerator() {
const urls = ['url1', 'url2', 'url3'];
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
yield data;
}
}
(async () => {
for await (const value of asyncGenerator()) {
console.log(value);
}
})();
この例では、asyncGenerator 関数が各 URL からデータを非同期に取得し、その結果を yield しています。呼び出し側では、for await...of ループを使用して、非同期イテレーターから順次データを取得しています。
-
[[Call]]と[[Construct]]: Async Generator Function は[[Call]]スロットを持ち、関数として呼び出すことができますが、[[Construct]]スロットは持たないため、new演算子でインスタンス化することはできません。 -
prototypeプロパティ: Async Generator Function はprototypeプロパティを持ちますが、通常の関数とは異なり、返されるオブジェクトは非同期イテレーターとしての特性を持ちます。
Async Generator Function を活用することで、非同期データのストリーム処理や逐次的な非同期操作を効率的に行うことができます。これにより、リアルタイムデータの処理や大規模データの分割処理など、さまざまな非同期シナリオに対応できます。生成AIのリアルタイム系 API を使うときに重宝しますね。
new Function コンストラクタ
JavaScript には、Function コンストラクタを直接呼び出して関数を動的に生成する手法も存在します。
const func = new Function("a", "b", "return a + b;");
console.log(func(2, 3)); // 5
このように動的に文字列をパースして新しい関数を生成しますが、セキュリティやパフォーマンス面で注意が必要です。
-
evalのように動的にコードを実行するため、ユーザー入力を直接渡すような場合は危険です。 - JS エンジンの最適化が効きづらい場合があるため、頻繁に利用するとパフォーマンスが悪化する可能性があります。
関数のプロパティとしての活用例
関数はオブジェクトでもあるため、プロパティを自由に設定できます。たとえば、メモ化関数(キャッシュ)を作る際に、キャッシュ用のオブジェクトを関数そのものに持たせるという手法があります。
function memoizedFib(n) {
if (!memoizedFib.cache) {
memoizedFib.cache = {};
}
if (n < 2) return n;
if (memoizedFib.cache[n] != null) {
return memoizedFib.cache[n];
}
const result = memoizedFib(n - 1) + memoizedFib(n - 2);
memoizedFib.cache[n] = result;
return result;
}
console.log(memoizedFib(10)); // 55
console.log(memoizedFib.cache); // キャッシュオブジェクトが保持されている
ここでは memoizedFib.cache というプロパティを定義して、再帰計算で得られた値をキャッシュとして保存しています。こうした設計が簡単にできるのは、関数がオブジェクトであることの強みです。
深掘り
以下、プロダクションレベルのコードではあまり登場しないかもしれませんが、JavaScript を深く理解するうえで知っておくと役立つ内容を紹介します。
関数の toString() 挙動
JavaScript の関数オブジェクトは、toString() メソッドを呼ぶと関数の本体のソースコードを文字列として返すよう仕様で定義されています。
function example(a, b) {
return a + b;
}
console.log(example.toString());
/*
function example(a, b) {
return a + b;
}
*/
ただし、ブラウザや環境によって厳密に同じ文字列が返ってくるとは限りません。
ES2019 以降は、実行環境による詳細な差分は最小限にするという仕様になっていますが、厳密にソースコードを保証するわけではないので注意が必要です。
arguments.callee は非推奨
従来の JavaScript では arguments.callee を使って無名関数が再帰的に自分自身を呼ぶ手法がありましたが、現在では非推奨とされています。関数式に名前をつけて再帰呼び出しを行うことが推奨されています。
// 非推奨
const factorial = function(n) {
if (n <= 1) return 1;
return n * arguments.callee(n - 1);
};
// 推奨
const factorial2 = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1);
};
caller プロパティも非推奨
functionObj.caller で呼び出し元の関数を取得することもできましたが、セキュリティ上の理由などから同様に非推奨となっています。関数オブジェクトをトレースしてデバッグに活用するようなケースがあったものの、現在は推奨されていません。
まとめ
JavaScript の関数は、ただ「呼び出せる存在」というだけではなく、「オブジェクト」であるがゆえの多くの特性を持っています。
-
[[Call]]/[[Construct]]という内部スロットを介して呼び出しやnewができる -
Function.prototypeのメソッドとしてcall,apply,bindなどを継承 -
length,name,prototypeなどのプロパティを持ち、オブジェクトとしてプロパティを拡張可能 - Arrow Function や Generator Function, Async Function のように、ECMAScript の仕様拡張でさまざまな呼び出しモデルや非同期モデルを提供
-
new Functionのように動的に関数を生成するコンストラクタが存在
これらを活用することで、JavaScript における「関数」の持つ表現力や柔軟性を最大限に引き出すことができます。
これらの仕組みをしっかりと理解した上でパフォーマンスやセキュリティ、コードの可読性に配慮しながら、適切な設計と実装を行うことが重要です。
記事は以上です。この記事が JavaScript の理解に少しでも役に立てれば嬉しいです。
Happy JavaScripting!
Discussion
async functionもfunction*も説明するのであればasync function*も説明したらいいのではないでしょうか……?いつもご助言ありがとうございます!!
追加してみます!