これでできる! クロスブラウザJavaScript入門

第16回 JavaScriptのthisとcall

この記事を読むのに必要な時間:およそ 4 分

こんにちは,太田です。前々回前回とJavaScriptにおける継承について学習しました。今回はそれに深く関わるthisについて学んでいきます。

JavaScriptのthisはややクセのある動作をするように思えるかもしれませんが,仕組みをしっかり把握すれば実に簡単です。特に重要なのは次の2点です。

  • thisが何を指すかは関数の呼び出し方で決定する
  • thisは関数スコープに存在する特殊な変数である

インスタンスとしてのthis

では,まずはコンストラクタ内でのインスタンスとしてのthisを見てみましょう。

コンストラクタとthis

function A(name){
  this.name = name;
}
A.prototype.getThis(){
  return this;
};
var a = new A('aaa');
console.log(a);
console.log(a === a.getThis()); // true

new演算子を使用すると,コンストラクタ関数が呼び出され,その中のthisが戻り値となります。上記コードではnew Aの結果がaに代入されます。ここでは,Aのメソッド(getThis)内のthisはa自身と一致します。つまり,ここでは「this===インスタンス」となっています。

ただし,コンストラクタが必ずthisを返すというわけではありません。

コンストラクタでthisを返さないケース

function AA(name){
  return {name:'bbb'};
}
var aa = new AA('aaa');
console.log(aa);
console.log(aa instanceof AA); // false

このように,コンストラクタ内でオブジェクトを返すと例えnew演算子を使用していたとしてもそのオブジェクト自身が戻り値になります。これでは継承ができないので,あえて戻り値にオブジェクトを指定するメリットはないでしょう。なお,オブジェクト以外,つまりプリミティブ値をreturnした場合はそのプリミティブ値は無視され,thisが返されます。new演算子の返り値は必ずオブジェクトです。いずれにしても,コンストラクタではreturnしないほうがよいという点は覚えておきましょう。

thisが指すもの

さて,thisがインスタンス以外になるケースはまだまだあります。

  • メソッドを変数に代入してから呼び出した場合
  • イベントとして呼ばれた場合
  • setTimeout,setIntervalで呼ばれた場合

具体的には下記のようなケースです。

メソッドを変数に代入するケース

function A(name){
  this.name = name;
}
A.prototype.logThis=function(){
  console.log(this);
};
var a = new A('aaa');
var a_logThis = a.logThis;
a_logThis(); // Global
setTimeout(a.logThis, 0); // Global

実はJavaScriptにおいてthisは非常に簡単なルールで決定されます。そのルールとは,「呼び出した関数の手前(ドットの前)のオブジェクトがthisになる。ただし,手前にドットがない場合はグローバルオブジェクトがthisになる」というものです。

つまり,thisはメソッドの定義時ではなく,それを呼び出すときに決定されます。関数を呼び出すたびにthisが変わる可能性がある,ということです。

callとapply

さて,このthisを呼び出す側からより自由にコントロールする方法があります。関数のプロトタイプがもつメソッド(すなわちすべての関数オブジェクトが持つ関数)のcallとapplyです。JavaScriptでは関数が(ファーストクラス)オブジェクトであるという説明は何度かしてきましたが,オブジェクトだから関数自身もメソッドを持つことができます。関数が関数を持っているというと少々ややこしいですが,オブジェクトがメソッドを持っているだけに過ぎません。

callによるthisの指定

function A(name){
  this.name = name;
}
A.prototype.logThis=function(){
  console.log(this);
};
var a = new A('aaa');
var a_logThis = a.logThis;
a_logThis.call(a); // {name:'aaa'}

callは第1引数にthisとなるオブジェクトを,第2引数以降は呼び出される関数の引数に渡されます。

applyは第1引数にthisとなるオブジェクトを,第2引数には配列を渡し,その配列が呼び出される関数の引数に展開されます。

callとapply

var log = function(){
  console.log(this);
  console.log(arguments);
};
log.call({id:1}, 2, 3);
// {id:1} , [2, 3]
log.apply({id:2}, [2, 3]);
// {id:2} , [2, 3]

callとapplyの違いは引数を個別に指定するか,配列でまとめて指定できるかの違いです。引数の数が可変でもよいapplyのほうが柔軟なので,applyだけでも事は足ります。

ただ,callとapplyでベンチマークを取るとcallのほうが軒並み高速です。もちろん数回程度の呼び出しでは1msほどの差にもならないので,そこまで気にするほどのことではありません。

thisとarguments

ところで,関数を呼び出すときに決定されるモノといえば,引数つまりargumentsがあります。実はこのargumentsとthisは非常によく似ています。argumentsは引数ですから,関数を呼び出すたびに値が変わるのは当然のことですが,それと一緒に「thisも関数ごとにそれぞれのthisを持っている」ということです。

クロージャについて説明した際にJavaScriptの変数は関数単位であると説明しましたが,thisやargumentsも変数と同じということです。そう考えるとJavaScript(ECMAScript)の仕様で一貫している部分が見えてくると思います。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/

コメント

コメントの記入