Zenn

JavaScript で関数はオブジェクト? なにそれ?

2025/02/23に公開
2
36

はじめに

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 バインディングを制御したり、可変長引数を配列形式で受け取るなどが簡単に行えるようになります。

関数特有のプロパティ

関数はオブジェクトとしてプロパティを持ちますが、その中でも関数特有のプロパティは重要です。

  1. length プロパティ
    仮引数の数(レストパラメータやデフォルト値をもたない引数の数)を返します。

    function example(a, b, c) {}
    console.log(example.length); // 3
    

    Arrow Function でも同様に length は利用可能です。

  2. name プロパティ
    関数名を返します。名前付き関数の場合はその名前、無名関数の場合は空文字列、ただし ES6 以降の推論によって const obj = { method() {} }; のような場合には method という名前が付与されるケースがあります。

    function foo() {}
    console.log(foo.name); // "foo"
    
    const bar = function () {};
    console.log(bar.name); // "bar" (ブラウザによっては空文字列の場合もあり)
    
  3. 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)との違いがあります。

  1. this バインディングが静的
    Arrow Function 内の this は、定義されたときのレキシカルスコープを参照します。通常の関数は呼び出し時に this が決定されるため、bind などを使わない限りオブジェクトメソッドとして呼び出せばそのオブジェクトが this になります。アロー関数はそれが変わりません。

  2. arguments オブジェクトがない
    Arrow Function では arguments はスコープチェーンの外から参照されるため、通常の関数のように暗黙的に arguments を使うことはできません。代わりにレストパラメータ(...args)を活用します。

  3. new による呼び出しが不可
    Arrow Function は [[Construct]] スロットを持たないので、コンストラクタとして使用できません。prototype プロパティも持ちません。

拡張

JavaScript の関数にはさらに拡張形として Generator FunctionAsync FunctionAsync 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 functionprototype プロパティを持ちますが、通常コンストラクタとして呼び出すことはありません。

Async Generator Function

Async Generator Functionasync function* 構文で定義され、非同期イテレーターを返す関数です。awaityield を組み合わせて、非同期のデータストリームを逐次処理することが可能です。

async function* は、非同期イテレーターを返す関数を定義します。この関数内では awaityield を組み合わせて使用でき、非同期操作の結果を逐次 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!

36

Discussion

ログインするとコメントできます