はじめに (thisの謎を解き明かす!)

JavaScriptのthisキーワードは、その挙動の多様さから多くの開発者にとって理解が難しい概念の一つです。特に、プロトタイプチェーン上のメソッドを変数に代入して呼び出そうとすると、予期せぬTypeErrorに遭遇することがあります。

この記事は、JavaScriptのthisの挙動やプロトタイプチェーンに疑問を持つ、初学者から中級者の方を対象としています。具体的には、Function.prototype.callを変数に代入して呼び出すとエラーになる現象に焦点を当て、その原因と背後にあるJavaScriptのメカニズムを深掘りします。

この記事を読み終えることで、あなたはJavaScriptにおけるthisの決定ルール、プロトタイプメソッドの呼び出し方、そして特定のTypeErrorに遭遇した際のデバッグ方法が明確に理解できるようになります。もうthisで悩むことは少なくなるでしょう!

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * JavaScriptの基本的な構文(変数、関数、オブジェクト) * 関数の呼び出しとスコープ * thisキーワードの基本的な概念

JavaScriptにおけるthisとメソッド呼び出しの仕組み

JavaScriptの関数はオブジェクトであり、他のオブジェクトと同様にプロパティやメソッドを持つことができます。Function.prototype.callもその一つで、全ての関数オブジェクトが継承するメソッドです。

callメソッドの主な役割は、関数を呼び出しつつ、その関数の実行コンテキスト(つまりthisが指す値)を明示的に指定することです。

Javascript
function greet(name) { console.log(`Hello, ${name}! I am ${this.value}.`); } const person = { value: 'a JavaScript developer' }; // greet関数をpersonオブジェクトのコンテキストで呼び出す greet.call(person, 'World'); // 出力: Hello, World! I am a JavaScript developer.

ここで重要なのは、thisの決定ルールです。JavaScriptでは、関数の呼び出し方によってthisが自動的に決定されます。

  1. 通常の関数呼び出し: func() の形式で呼び出された場合、厳格モードではthisundefinedに、非厳格モードではグローバルオブジェクト(ブラウザならwindow)に束縛されます。
  2. メソッド呼び出し: obj.method() の形式で呼び出された場合、thisobjに束縛されます。
  3. コンストラクタ呼び出し: new MyClass() の形式で呼び出された場合、thisは新しく作成されたインスタンスに束縛されます。
  4. call, apply, bind: これらのメソッドを使用すると、thisの値を明示的に指定できます。

Function.prototype.callは、上記2番目の「メソッド呼び出し」のルールが特に重要になります。これはFunction.prototypeオブジェクトに定義されたメソッドであり、呼び出される際には「自身が関数オブジェクトであること」を前提としています。

なぜFunction.prototype.callを変数に入れるとエラーになるのか?

それでは、いよいよ本題です。なぜFunction.prototype.callを変数に代入して呼び出そうとするとエラーになるのでしょうか?この現象は、JavaScriptにおけるthisの挙動とメソッドの呼び出し方が密接に関連しています。

ステップ1: 問題の再現とエラーの確認

まず、実際にエラーが発生するコードを見てみましょう。

Javascript
const myCall = Function.prototype.call; // この行を実行するとエラーが発生します try { myCall(() => console.log('Hello'), null); } catch (error) { console.error(error); // TypeError: Function.prototype.call called on null or undefined // または TypeError: Function.prototype.call called on incompatible receiver }

上記のコードを実行すると、TypeErrorが発生します。これは「Function.prototype.callnullまたはundefinedに対して呼び出された」という旨のエラーです。myCallは間違いなく関数を指しているはずなのに、なぜnullundefinedに対して呼び出されたというエラーになるのでしょうか?

ステップ2: Function.prototype.callの正体とメソッドの期待

この問題を理解するためには、Function.prototype.callが何であるかを改めて考える必要があります。 Function.prototype.callは、その名の通りFunction.prototypeオブジェクトに定義されているメソッドです。これは、全ての関数オブジェクト(例:myFunc)が myFunc.call(...) のように、自身のメソッドとしてcallを呼び出せることを意味します。

callメソッドは、内部的に「自身(つまりthis)が関数オブジェクトであること」を期待して動作します。Function.prototype.call(thisArg, arg1, ...)という形式で、callの最初の引数(thisArg)が呼び出される関数のthisとなり、call自身がどの関数に対して呼び出されたかは、callメソッド内のthisが示します。

ステップ3: 変数代入がthisに与える影響

const myCall = Function.prototype.call;という行が実行されるとき、myCall変数にはFunction.prototype.callというメソッドへの参照(ポインタ)が代入されます。この時点では、myCallは単なる関数の参照であり、「Function.prototypeに属するメソッドである」という情報は失われています。

次に、myCall(() => console.log('Hello'), null); と呼び出した場合、これは前述のthisの決定ルールにおける「通常の関数呼び出し」に該当します。この呼び出し方では、myCall内部のthisは厳格モードではundefinedになります。

結果として、Function.prototype.callメソッドが、期待する関数オブジェクトではなくundefined(または非厳格モードではグローバルオブジェクト)に対して呼び出される形になります。callメソッドは、「thisが関数オブジェクトでない」という状況をエラーと判断し、TypeErrorをスローするのです。

補足:メソッド呼び出しとthisの自動束縛

obj.method()のようにドット記法でメソッドを呼び出すと、JavaScriptエンジンは自動的にmethod内部のthisobjに束縛します。これは、メソッドがどのオブジェクトに属しているかを認識し、それに応じたコンテキストを提供する仕組みです。

しかし、const func = obj.method; func(); のように、一度変数に代入してから呼び出すと、この自動的なthisの束縛は行われず、単なる関数呼び出しとして扱われるため、thisundefinedやグローバルオブジェクトに設定されてしまうのです。Function.prototype.callも例外ではありません。

ハマった点やエラー解決

多くの開発者は、「関数を変数に入れただけなのに、なぜ動かないのか?」とこの問題で戸惑います。これは、JavaScriptの「関数はファーストクラスオブジェクトである」という特性と、「メソッド呼び出しにおけるthisの特別な束縛ルール」が混同されやすい点です。

Function.prototype.callは、あくまで「関数オブジェクトに対して、そのthisを指定して呼び出すためのメソッド」であり、汎用的なユーティリティ関数としてFunction.prototypeから切り離して使うことを意図していません。

解決策

Function.prototype.callは、常に呼び出したい関数オブジェクトのメソッドとして使用するのが正しい使い方です。

例えば、引数オブジェクトやNodeListのような配列ライクなオブジェクトを本物の配列に変換したい場合によく使われます。

Javascript
// 関数に対してcallメソッドを直接呼び出す正しい例 function sum() { // argumentsは配列ライクなオブジェクト // Array.prototype.slice.callを使って、argumentsを本物の配列に変換 const argsArray = Array.prototype.slice.call(arguments); return argsArray.reduce((acc, val) => acc + val, 0); } console.log(sum(1, 2, 3, 4)); // 出力: 10

この例では、Array.prototype.sliceというメソッドをargumentsオブジェクトのコンテキスト(this)で呼び出しています。ここでsliceFunction.prototypeを継承しているため、slice.call()が可能になります。

もし、何らかの理由でFunction.prototype.call自体を変数に格納して利用したい非常に特殊なケースがあったとしても、それは通常、Function.prototype.call.bind(Function.prototype)のようにbindを使ってthisを明示的にFunction.prototypeに固定することで実現できますが、これは非常に稀なケースであり、通常は推奨されません。

重要なのは、callメソッドがどの関数オブジェクトに対して実行されるか、その「レシーバー」こそがcallメソッド内のthisになる、という点です。

まとめ

本記事では、JavaScriptのFunction.prototype.callを変数に代入して呼び出すとTypeErrorが発生する原因について掘り下げました。

  • 要点1: Function.prototype.callは、Function.prototypeオブジェクトに定義されたメソッドです。このメソッドは、自身(this)が関数オブジェクトであることを期待して動作します。
  • 要点2: メソッドを単に変数の代入し、ドット記法なしで通常の関数として呼び出すと、そのメソッドが本来属していたオブジェクトのコンテキスト(this)が失われます。厳格モードでは、このthisundefinedになります。
  • 要点3: 結果として、Function.prototype.callundefinedに対して呼び出された状態となり、期待されるレシーバー(関数オブジェクト)ではないためTypeErrorが発生します。

この記事を通して、JavaScriptのthisの奥深い挙動とメソッド呼び出しの仕組みについて理解を深められたことでしょう。プログラミングにおけるTypeErrorは、一見すると不可解でも、その原因を深掘りすることで言語仕様の理解を大きく進める機会となります。

今後は、Function.prototype.applyFunction.prototype.bindとの違い、さらにはカスタムオブジェクトにおけるthisの挙動についても記事にする予定です。

参考資料