JavaScriptにおけるthisとは?
JavaScriptでは、thisという特殊なオブジェクトがあります。例を挙げてみましょう。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log("My name is " + this.name + ". I am " + this.age + " years old.");
};
var person = new Person("Alice", 20);
person.say(); // -> My name is Alice. I am 20 years old.
この例で、personオブジェクトに格納されたnameとageは、thisを用いることによってアクセスできます。ここで、「思い出す」というメソッドを追加してみましょう。思い出すためには時間が必要ですね。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log("My name is " + this.name + ". I am " + this.age + " years old.");
};
Person.prototype.remember = function(){
console.log("Hmm...");
setTimeout(function(){
this.say();
}, 1000);
};
var person = new Person("Alice", 20);
person.remember();
こうすると、TypeErrorが発生します。匿名関数を利用することで、関数スコープであるthisの参照先が変わってしまったわけです。これを解決するために、さまざまな方法が考えられました。
問題解決へのアプローチ
self = this
thisは関数(厳密にいえば、呼び方)によって異なります。先のsetTimeoutの例では、thisは指定しない限りwindowオブジェクトを指すようになります。であれば、それらに束縛されない、匿名関数の外側でスコープ変数として定義してやる、というのが古典的かつ最もポピュラーな手法として長らく使われてきました。
変数名はself、_this、scopeなどいろいろありますが、JavaScriptライブラリとして最も有名なjQueryは、selfを利用しています。おそらくもっとも使われている変数名は、「self」でしょう。
さて、先の例を、本項の手段を用いて修正してみることにしましょう。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log("My name is " + this.name + ". I am " + this.age + " years old.");
};
Person.prototype.remember = function(){
console.log("Hmm...");
var self = this;
setTimeout(function(){
self.say();
}, 1000);
};
var person = new Person("Alice", 20);
person.remember(); // -> Hmm... My name is Alice. I am 20 years old.
これで想定どおりの結果が得られます。
bind(this)
続いて、bind(this)の方法を考えます。bind()関数は、Function.prototypeに定義されており、単に「thisを束縛する」関数として使われています。詳細はMDNのFunction.prototype.bind()をご覧頂くとして、まずは本項の手法を用いて先ほどの例を修正してみましょう。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log("My name is " + this.name + ". I am " + this.age + " years old.");
};
Person.prototype.remember = function(){
console.log("Hmm...");
setTimeout(function(){
this.say();
}.bind(this), 1000);
};
var person = new Person("Alice", 20);
person.remember();
こちらも想定通りの結果となりました。
比較する
それでは、本題の「どちらを使うべきか?」を利点・欠点を交えた観点から考察してみたいと思います。
self = this
利点
- 可読性が高く、コードリーディングを妨げない
selfという変数を定義して、thisを格納していることは、コードを読めば一目瞭然です。ごく自然な流れでコードを読むことができます。
- コストが低い
変数にthis参照を格納しているだけなので、余計な処理が必要ありません。
欠点
- やや煩雑
匿名関数中で1回しか使わないようなthisのためにわざわざ変数定義するのは、些か煩雑な気はします。
bind(this)
利点
- self = thisより簡潔な実装が期待できる
変数を定義しなくてよい分、簡潔に実装できます。
欠点
- 長い関数の場合は可読性が下がる
匿名関数の中身を読んでいくときに、脈絡なくthisが出てきたとき、出所を探らなくてはなりません。スクロールしなければならないほど長い匿名関数は設計上の問題もありますが、そういった場合に関数の最後を見なくてはならず、円滑なコードリーディングに支障を来します。
- コストが高い
ネイティブ実装とはいえ、新しいFunctionを生成するコストが掛かります。最近の高スペックな端末では問題にもなりませんが、余計な処理であることは頭の片隅に置いておく必要がありそうです。
どちらを使うべきか
総合的に見て、特段の理由がなければself = thisで統一してしまうのが良いと思います。bindは比較的最近(とは言っても5年以上前ですが)実装されたメソッドですが、thisの束縛というより、むしろ別の用途に使うべきメソッドに感じました。
しかし、MDNのAPI Documentの「部分的に適用された関数」項にあるような使い方は、動きがややこしく、むしろ可読性を下げてしまいます。こういったケースが必要な場合は、ラッパー関数などを活用するべきでしょう。
(おまけ)ES2015 (ES6)からの実装
そろそろES2015 (ES6)が浸透してきそうな気配があります。ES2015 (ES6)では、匿名関数にアロー関数式が使えますが、このアロー関数式は親のthisオブジェクトがそのまま使えます。試しに書いてみましょう。
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
say(){
console.log("My name is " + this.name + ". I am " + this.age + " years old.");
}
remember(){
console.log("Hmm...");
setTimeout(() => {
this.say();
}, 1000);
}
}
let person = new Person("Alice", 20);
person.remember();
いろいろと考えなくてよくなりましたね。
まとめ
ES2015 (ES6)を前に、過渡期とも言えるJavaScript開発です。こういった話題も最近では古臭い話題になってきましたが、改修案件などでまだまだ以前の構文を使わざるを得ないのも事実です。改修を行うときは、メンテナンス性に細心の注意を払いながら開発を進めたいところです。