普通の業務系Java PGでなくても一度はハマる?JavaScriptのthisの奇妙な振る舞い

先日書いた普通の業務系PGには意外と知られていないJavaとJavaScriptの相違点10選 - 達人プログラマーを目指してでは、これからJavaScriptを本格的に勉強する層のプログラマーの人を対象に、JavaJavaScriptの違いを理解する上で重要な10個のポイントについて説明しました。いただいたコメントの中には、JavaScriptJavaは当然まったく別の言語で、比較すること自体問題であるという趣旨のご指摘もいただきました。確かにその通りなのですが、実際、業務で本格的なプログラムの開発はJavaでしかしたことがないという開発者は結構自分のまわりにはたくさんいますし、時代の流れから言って、これから初めて本格的にJavaScriptを書くという人も今後たくさん出てくるのではないかと思います。そういう人にとっては、やはり、違いを意識するところから入っていくというのは学習のアプローチの一つとしてはあるのではないかと思います。
ところで、前回の記事は初心者向けということもあり、書き漏らしたのですが、主にJavaで開発してきた自分の経験から言って、JavaScriptを本格的に使う上で最も注意すべき相違点として、JavaSciptにおけるthisの振る舞いがJavaとは大きく異なっているという点があります。私もこのポイントを理解するまで、実際に何回か同じような問題にハマりました。今回は、この注意点に関して補足します。

JavaScriptのthisは省略できない

Java言語でメソッドを定義する際に、thisはそのメソッドが定義されているクラス自身のインスタンスを指します。そして、通常はこのthisは省略しても意味が変わりません。(同一名のパラメーターなどで名前がかぶる場合を除く)

public class Person {
    private String firstName;	
    private String lastName;
	
    public Person(String firstName, String lastName) {
        // 名前が重なるときのみthisを付ける
        this.firstName = firstName;
        this.lastName = lastName;
    }
	
    public String getName() {
     // return this.lastName + " " + this.firstName; 同じ意味
        return lastName + " " + firstName;
    }
}

同様のPersonオブジェクトをJavaScriptでも定義できます。JavaScriptでクラスのようなオブジェクトを定義する方法はいろいろありますが、たとえば、以下のように記述できます。*1

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// プロトタイプに対してメソッドを追加
Person.prototype.getName = function() {
    return this.lastName + " " + this.firstName;
};

var madoka = new Person("まどか", "鹿目");

console.log(madoka.getName()); // "鹿目 まどか"

JavaScriptの場合、以上のgetName()メソッドの定義で、正しくfirstNameとlastNameのプロパティの値を参照するためには、絶対にthisを省略できません。実際、省略すると、未定義変数エラーとなって実行できません。

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// プロトタイプに対してメソッドを追加
Person.prototype.getName = function() {
    return lastName + " " + firstName; // この行で未定義変数エラー
};

var madoka = new Person("まどか", "鹿目");

console.log(madoka.getName());

getName()はPersonオブジェクトのコンテキストで呼び出せるメソッドなのですが、定義する場所はthisなしで変数を参照すると、グローバル変数として解釈されてしまいます。この場合、当然firstNameなどのグローバル変数が宣言されていませんから、エラーとなるのです。以上の例ではあえて、プロトタイプを使い、コンストラクタ関数の外部でメソッドを追加したのですが、コンストラクタ関数内でメソッドを定義することもできます。この場合も、うっかりgetName()メソッドの定義中でthisを忘れると、関数がpersonオブジェクトのプロパティを参照せず、代わりにパラメーターの変数を参照するようになってしまいます。関数はクロージャーになるため、パラメーターなどのローカル変数の値を参照できることに注意してください。

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.getName = function() {
        return lastName + " " + firstName; //thisの付け忘れ
    };
}

var madoka = new Person("まどか", "鹿目");

console.log(person.getName()); // "鹿目 まどか"
madoka.lastName = "魔法少女";  // プロパティを変更

console.log(madoka.getName()); // "鹿目 まどか" 変更が効かない!

この場合、正しくthisを付けることで意図した動作になります。

// コンストラクタ関数
function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.getName = function() {
        return this.lastName + " " + this.firstName;
    };
}

var madoka = new Person("まどか", "鹿目");

console.log(madoka.getName()); // "鹿目 まどか"
madoka.lastName = "魔法少女";  // フィールドを変更

console.log(madoka.getName()); // "魔法少女 まどか"

このように、JavaScriptのメソッド定義中で、オブジェクト自身のプロパティやメソッドを参照する場合には常にthisが省略できないという点に注意する必要があります。Javaの癖でthisを省略してしまうと、以上のようなエラーとなり、場合によっては結構見つけにくいバグとなることがあるため、注意が必要です。

JavaScriptのthisは関数の呼び出し時のコンテキストで指す対象が動的に変化する

そして、次に注意が必要な点はこのthisの指す対象のオブジェクトについてです。Javaの場合は、thisはそのメソッドが定義されているクラスのオブジェクト以外を指し示すことは絶対にありません。しかし、JavaScriptの場合そのメソッドが呼び出される実行時の動的なコンテキストによって、thisの指すオブジェクトが変化します。実際、関数定義中のthisは、

  • new演算子の後でコンストラクタ関数として呼び出された場合はthisは生成対象オブジェクトを指す
  • オブジェクト.メソッド()の形で呼び出された場合、そのメソッド中のthisは呼び出し対象のオブジェクトを指す
  • Functionオブジェクトのcall(コンテキスト、パラメーター)を使って呼び出した場合thisはコンテキストを指す
  • それ以外の形式で呼び出された場合、thisはグローバルオブジェクトを指す

というルールがあります。このルール自体は自然に理解できるものです。*2

function QB() {
    this.requestContract = function(person) {
        return person.getName() + " 僕と契約して魔法少女になってよ。";
    };
}

var madoka = new Person("まどか", "鹿目");
var qb = new QB();

console.log(qb.requestContract(madoka)); // "鹿目 まどか 僕と契約して魔法少女になってよ。"

ここで、少し設計を変更し、requestContractメソッドがPersonのオブジェクトではなく、関数そのものが渡されるように変更してみます。JavaScriptのメソッドは単に関数が特定のオブジェクトに保持されているだけであり、このメソッドを普通の関数オブジェクトとまったく同様に別のオブジェクトに代入したり、別の関数のパラメーターとして渡したりすることができます。
実際、ちょっと不自然な例ですが、以下のコードを考えてみましょう。

function QB() {
    this.requestContract = function(nameFunc) {
       return nameFunc() + " 僕と契約して魔法少女になってよ。";
    };
}


var madoka = new Person("まどか", "鹿目");
var qb = new QB();

// ちょっと不自然だけど、メソッドをパラメーターとして直接渡してみる
console.log(qb.requestContract(madoka.getName)); // undefined undefined 僕と契約して魔法少女になってよ。

この場合、madokaオブジェクトのgetName()メソッドそのものをパラメーターとしてrequestContract()に渡して実行しようとしているのですが、requestContract()メソッド中で、パラメーターとして渡されたnameFuncは対象オブジェクトなしで呼び出されているため、getName()メソッド中のthisはmadokaオブジェクトではなく、グローバルオブジェクトを指すことになってしまうのです。したがって、firstNameもlastNameも未定義のため、以上の結果となります。*3この例だと、ちょっと人工的で、やらないと思われるかもしれませんが、コールバック関数をとるライブラリーの機能を呼び出す場合に、うっかりメソッドを渡してしまい、動作がおかしくなるという失敗はこの原理をしらないとよく起こしますね。
この場合、メソッドをパラメーターとして渡しつつ、かつ、thisを正しいオブジェクトにバインドした状態で呼び出すには関数オブジェクトのcall(コンテキスト、パラメーター)を使うことができます。実際、以下のように記述することができます。

function QB() {
	
    this.requestContract = function(person, nameFunc) {
        return nameFunc.call(person) + " 僕と契約して魔法少女になってよ。"; // callで渡したpersonをthisが指す
    };
}


var madoka = new Person("まどか", "鹿目");
var qb = new QB();

// ちょっと不自然だけど、メソッドをパラメーターとして直接渡してみる
console.log(qb.requestContract(madoka, madoka.getName)); // "鹿目 まどか 僕と契約して魔法少女になってよ。"

Groovyでも.&演算子を使って、オブジェクトのメソッドをクロージャーとして別メソッドのパラメーターとして渡すことができますが、メソッド呼び出し時のコンテキストによって、thisの指す対象が動的に変化するということはありません。

class Person {
    String firstName
    String lastName
    
    def getName() {
        lastName + ' ' + firstName // this.lastName + ' ' + this.firstNameと同じ
    }
}

class QB {
    def requestContract(closure) {
        closure.call() + ' 僕と契約して魔法少女になってよ。'
    }
}

def sayaka = new Person(lastName:'美樹', firstName:'さやか')
def qb = new QB();

println sayaka.name

println qb.requestContract(sayaka.&getName) //'美樹 さやか 僕と契約して魔法少女になってよ。'

まとめ

JavaScriptのメソッド定義で、thisを使う場合

  • Javaと違ってthisを省略できない(省略するとオブジェクトのプロパティやメソッドを参照できない)
  • thisの指す内容は関数の定義時には決まらず、実行時のコンテキストで動的に変化する。

という点をしっかりと理解する必要があります。

*1:まどか☆マギカは今晩ようやく1話を観たところでドメインモデルが全くわかっていないため、そこはつっこまないでください。まどマギjava-jaデザインパターンの勉強会で例として頻繁に取り上げられていました。http://togetter.com/li/128336

*2:やはり、いくら言語の知識があっても、ドメイン知識がないうちは、まともな設計ができませんorz。

*3:オブジェクトのプロパティはvar宣言なしでもエラーとならないという点も実は隠れたポイント