mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

詳細 ECMA-262-3 第3章 this

どうもおつかれさまでございます。たんぽぽグループの大形尚弘でございます。好きな言語は Dylan です。好きな声優は五十嵐裕美さんです。

さて、週刊のはずが月刊になってしまった、 Dmitry 先生の ECMA-262-3 シリーズの第3章をお送りします。文中、未だ訳出の終わっていないスコープチェーンや関数の章への参照がありますが、特にスコープチェーンにおいてこの時点である程度理解しておきたいとお感じになる方もいらっしゃるかと思います。その辺りは、以前私個人のブログで翻訳・公開させいただいたコア・JavaScript ( JavaScript. The Core. )でも簡単に触れられておりますので、適宜ご参照ください。

また、本章とは全然関係ないのですが、先日 JavaScript Advent Calendar 2011 (オレ標準コース)に参加させていただき、 ECMAScript におけるオブジェクト指向について、また次期バージョンの ES.next におけるオブジェクト指向周辺の新しいシンタックスについてご紹介させていただきましたので、ぜひご印刷の上、おトイレの時間つぶしなどにご利用いただければ幸いです。

それでは本章、 JavaScript / ECMAScript で最も面白いところの一つ、「 this 」についてご高覧ください。

詳細 ECMA-262-3 第3章 this

目次

  1. はじめに
  2. 定義
  3. グローバルコード上の this 値
  4. 関数コード上の this 値
    1. Reference 型
    2. 関数呼び出しと非 Reference 型
    3. Reference 型と null の this 値
    4. コンストラクタとして呼び出された関数での this 値
    5. 関数呼び出し時に手動で this を指定する
  5. 結論
  6. 参考文献

はじめに

この章では、実行コンテキストに直接に関係するもうひとつのテーマについて考察します。議題は this キーワードです。

ご承知の通りこのトピックはとても難しく、時に異なる実行コンテキストにおける this 値の決定について議論の的になるポイントです。

多くのプログラマにとって、プログラム言語における this キーワードとは、オブジェクト指向プログラミングに密接に結びつき、コンストラクタによって新たに生成されるオブジェクトまさにそれ自体を参照するものとして考えられてきました。 ECMAScript でもその考え方は実装されているものの、これから見ていくように、ここでは生成されるオブジェクトのみには限らないのです。

それでは、 ECMAScript の this 値について、詳しく見てゆきましょう。

定義

this 値は、実行コンテキストのプロパティです。

activeExecutionContext = {
  VO: {...},
  this: thisValue
};

ここで VO とは、前の章でお話しした変数オブジェクトです。

this は、実行可能コードの種類に直接に関係するものです。その値はコンテキスト進入時に決定され、コードがコンテキスト中を実行している間は不変です。

各ケースについて、さらに詳しく見てゆきましょう。

グローバルコード上の this 値

ここでは話は単純です。グローバルコード上では、 this 値は常にグローバルオブジェクトそのものです。従って間接的な参照が可能です。

// グローバルオブジェクトへの
// 明示的なプロパティ定義
this.a = 10; // global.a = 10
alert(a); // 10

// 制約されていない識別子への代入による、
// 暗黙のプロパティ定義
b = 20;
alert(this.b); // 20

// 変数定義によるこれも暗黙的な例。
// グローバルコンテキストの変数オブジェクトは、
// グローバルオブジェクトそのものである。
var c = 30;
alert(this.c); // 30

関数コード上の this 値

関数コード内で this が用いられるとき、話はもっと興味深くなります。このケースは最も複雑で、さまざまな議論の的になってきました。

この種類のコードにおける、最初の(そしておそらくは、一番重要な) this 値の機能は、関数には静的に束縛されないということです。

前述したように、 this 値はコンテキスト進入時に決定されます。つまり、関数コードの場合においては、その値は都度完全に異なる値を取りうるのです。

しかしながら、コードの実行時には、 this 値は不変です。すなわち、新しい値を代入することはできません。なぜなら、 this は変数ではないからです。(対照的に、プログラミング言語 Python では明確に定義された self オブジェクトというものがあり、これはランタイムに変更可能です。)

var foo = {x: 10};
 
var bar = {
  x: 20,
  test: function () {
 
    alert(this === bar); // true
    alert(this.x); // 20
 
    this = foo; // エラー。 this 値を変更することはできない。
 
    alert(this.x); // 上のエラーが無かったとすれば、20ではなく、10になっただろう。
 
  }
};

// コンテキスト進入時に
// this 値は "bar" オブジェクトに決定される。
// 理由は後ほど詳しく説明する。
bar.test(); // true, 20
 
foo.test = bar.test;

// しかし、同じ関数を呼び出しているのだが、
// ここでは this 値は "foo" を参照する。
foo.test(); // false, 10

関数コードにおいて、 this 値が参照する先には一体何が影響しているのでしょうか?。これにはいくつかの要因があります。

まず、通常の関数呼び出しでは、 this はそのコンテキストのコードをアクティベートする呼び出し元によって与えられます。つまりそれは、関数を呼び出す親コンテキストです。 this 値は呼び出し式の形によって決定されます(言い換えれば、構文的に見て関数がどのように呼び出されたかという形式です)。

どんなコンテキストでも問題なく this 値を決定できるようになるためには、これは心に留めておくべきとても重要なポイントです。まさに、呼び出し式の形、すなわち関数を呼び出す方法こそが呼び出されたコンテキストにおける this 値を決定します。それ以外にはあり得ません。

( JavaScript に関するいくつかの記事や、時に書籍でさえ、「 this 値は関数がどのように定義されているかで決まる。もしグローバル関数であれば this 値はグローバルオブジェクトに、もし関数があるオブジェクトのメソッドであれば常にそのオブジェクトにセットされる」と主張していますが、これは完全に誤りです)。一歩踏み込んでみれば、一般的なグローバル関数でさえ、異なる形式の呼び出し式によってアクティベートされ得、異なる this 値を取り得るのです。

function foo() {
  alert(this);
}
 
foo(); // グローバル
 
alert(foo === foo.prototype.constructor); // true

// しかし、同じ関数であっても
// 異なる呼び出し式の形式によっては、
// this 値もまた異なる。
foo.prototype.constructor(); // foo.prototype

同様に、あるオブジェクトのメソッドとして定義された関数を、 this 値がそのオブジェクトにセットされない形で呼び出すことも可能です。

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};
 
foo.bar(); // foo, true
 
var exampleFunc = foo.bar;
 
alert(exampleFunc === foo.bar); // true

// もう一度、同じ関数であっても、
// 異なる呼び出し式の形によって、
// 異なる this 値を取ります。
exampleFunc(); // global, false

それでは、この呼び出し式の形式が、どのように this 値に影響するのでしょうか?。 this 値の決定について十分に理解するには、ある内部型、 Reference 型について詳しく考察する必要があります。

Reference 型

擬似コードを用いれば、 Reference 型の値は二つのプロパティを持ったオブジェクトとして表せます。 base(プロパティが属するオブジェクト)と、その base における propertyName です。

var valueOfReferenceType = {
  base: ,
  propertyName: <プロパティ名>
};

Reference 型の値が現れるのは、次の二つの場合です。

  1. 識別子を取り扱うとき
  2. あるいは、プロパティアクセサを取り扱うとき

識別子解決の手順については、第4章 スコープチェーンにて詳しく検討します。ここでは、このアルゴリズムからの戻り値は、必ず Reference 型の値( this 値の決定にとって重要な)になると理解しておいてください。

識別子とは、変数名、関数名、関数の仮引数名、グローバルオブジェクトの非制約プロパティの名前です。例えば、下記の識別子の値に対し...

var foo = 10;
function bar() {}

演算の中間結果では、対応する Reference 型の値は次の通りです。

var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
var barReference = {
  base: global,
  propertyName: 'bar'
};

Reference 型の値からオブジェクトの実際の値を取得するには GetValue というメソッドがあり、擬似コードでは次のように表せます。

function GetValue(value) {
 
  if (Type(value) != Reference) {
    return value;
  }
 
  var base = GetBase(value);
 
  if (base === null) {
    throw new ReferenceError;
  }
 
  return base.Get(GetPropertyName(value));
 
}

内部 [[Get]] メソッドは、プロトタイプチェーンから継承されるプロパティの解析も含め、オブジェクトのプロパティの実の値を取り出します。

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

みなさんご存じの通り、プロパティへのアクセス式(プロパティアクセサ)には二つのバリエーションがあります。 ドット記法(プロパティ名が正しい識別子であり、予め分かっている場合)と 括弧記法です。

foo.bar();
foo['bar']();

演算の途上の戻り値は、ここでも Reference 型の値を持ちます。

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};
 
GetValue(fooBarReference); // function object "bar"

さてそれでは、最も重要な意味において、関数コンテキストの this 値と Reference 型の値は、どのように関係するのでしょうか?。この点が、この章の主題です。関数コンテキストにおける this 値決定の原則は、次のようになります。

関数コンテキストにおける this 値は、呼び出し元によって与えられ、呼び出し式の形式(関数呼び出しが構文的にどのように書かれているかということ)によって決定される

呼び出し括弧 ( ... ) の左辺が Reference 型の値を取る場合は、 this 値は Reference 型の値の base オブジェクトにセットされる。

その他すべての場合においては(すなわち Reference 型と区別されるその他すべての値を取る場合)、 this 値は常に null となる。しかし、 this 値が null となることは意味を成さないため、暗黙にグローバルオブジェクトに変換される。

例を見てみましょう。

function foo() {
  return this;
}
 
foo(); // グローバルオブジェクト

呼び出し括弧の左辺には Reference 型の値が取られていることが分かります(なぜなら foo は識別子だからです)。

var fooReference = {
  base: global,
  propertyName: 'foo'
};

このようにして、 this 値は、この Reference 型の値の base オブジェクトにセットされます。この場合、グローバルオブジェクトです。

同様にプロパティアクセサの場合も...

var foo = {
  bar: function () {
    return this;
  }
};
 
foo.bar(); // foo

ここでも、 base が foo オブジェクトである Reference 型の値を取りますので、 bar 関数のアクティベーション時の this 値には foo オブジェクトがセットされます。

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

しかし、全く同じ関数違う形式の呼び出し式でアクティベートすると、異なる this 値を取ることになります。

var test = foo.bar;
test(); // グローバルオブジェクト

なぜならば、識別子である test が、異なる Reference 型の値を取るためです。ここではグローバルオブジェクトたる base が、 this 値として用いられます。

var testReference = {
  base: global,
  propertyName: 'test'
};
補足: ES5 の strict mode(未訳) では、 this 値はグローバルオブジェクトに強制変更されません。その代わり、 undefined がセットされます。

このようにして、なぜ異なる形式の呼び出し式でアクティベートされた同じ関数が異なる this 値を取るのか、という理由を正確に説明することができます。中間計算値である Reference 型の値が、異なる値を取るため、ということがその答えです。

function foo() {
  alert(this);
}
 
foo(); // グローバルオブジェクト。なぜなら、
 
var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
alert(foo === foo.prototype.constructor); // true
 
// 異なる形式の呼び出し式
 
foo.prototype.constructor(); // foo.prototype 。なぜなら、
 
var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

もう一つ、呼び出し式の形による this 値の動的な決定の、別の古典的な例を挙げておきます。

function foo() {
  alert(this.bar);
}
 
var x = {bar: 10};
var y = {bar: 20};
 
x.test = foo;
y.test = foo;
 
x.test(); // 10
y.test(); // 20

関数呼び出しと非 Reference 型

ところで、既に述べたように、呼び出し括弧の左辺が Reference 型ではなくその他すべての 型の値を取る場合には、 this 値は自動的に null にセットされ、結果として、グローバルオブジェクトになります。

そうした式の例を見てみましょう。

(function () {
  alert(this); // null => グローバルオブジェクト
})();

この場合、呼び出し括弧の左辺は関数オブジェクトであり、 Reference 型のオブジェクトではありません(識別子でも無ければ、プロパティアクセサでもありません)。従って、結果として this 値はグローバルオブジェクトにセットされます。

もう少し複雑な例を見てみましょう。

var foo = {
  bar: function () {
    alert(this);
  }
};
 
foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo
 
(foo.bar = foo.bar)(); // グローバル?
(false || foo.bar)(); // グローバル?
(foo.bar, foo.bar)(); // グローバル?

さて、中間計算値が Reference 型の値になるべきプロパティアクセサでありながら、なぜ、ある呼び出しにおいては base オブジェクト(すなわち foo )ではなくグローバルオブジェクトthis 値に取るのでしょうか?

問題となるのは最後の三つです。ある演算を行った後、呼び出し括弧の左辺の値が、 Reference 型の値ではなくなっているのです。

一番最初の例では、すべては明白です。そこには間違いなく Reference 型が存在し、その結果として、 this 値はその base オブジェクト、つまり foo になります。

二番目の例は、グループ演算子です。しかしこのグループ演算子は、既に見たような Reference 型の値からオブジェクトの実値を取得するメソッド、すなわち GetValue呼び出しません(仕様 11.1.6の注釈(邦訳)を参照してください)。従って、グループ演算子の評価の戻り値は、依然として Reference 型の値であるため、 this 値は再び base オブジェクトである、 foo にセットされます。

三番目のケースは、代入演算子です。代入演算子はグループ演算子と異なり、 GetValue メソッドを呼び出します(仕様 11.13.1 のステップ3(邦訳)を参照してください)。結果として、この代入演算子の戻り値は Reference 型の値ではなく関数オブジェクトになるため、 this 値は null にセットされ、結果として、グローバルオブジェクトになるのです。

四番目、五番目のケースも同様です。カンマ演算子論理和式GetValue メソッドを呼び出します。従って、 Reference 型の値を失い、関数 型の値を得ます。そして再び、 this 値はグローバルオブジェクトになります。

Reference 型と null の this 値

ある呼び出し式が、呼び出し括弧の左辺を Reference 型の値と決定しながら、 this 値が null 、結果としてグローバルオブジェクトになる場合があります。これは、 Reference 型の値の base オブジェクトが、アクティベーションオブジェクトである場合に関係しています。

こうした場面は、内部関数が親から呼び出された場合の例に見ることができます。第2章で学んだように、ローカル変数、内部関数、及び仮引数は与えられた関数のアクティベーションオブジェクトに保管されます。

function foo() {
  function bar() {
    alert(this); // グローバルオブジェクト
  }
  bar(); // AO.bar() に等しい
}

アクティベーションオブジェクトは、 this 値として常に null を返します(擬似コード AO.bar() は、 null.bar() に等しくなります)。ここでまた再び前述した仕組みに基づいて、 this 値はグローバルオブジェクトにセットされます。

with オブジェクトが関数名のプロパティを持つとき、 with 文のブロック内部で関数が呼び出された場合に、例外があり得ます。 with 文は、独自の with オブジェクトをスコープチェーンの先頭に付け加えます。つまり、アクティベーションオブジェクトのにです。よって、(識別子またはプロパティアクセサに) Reference 型の値を持ちますので、 base オブジェクトはアクティベーションオブジェクトではなく、 with 文のオブジェクトになるのです。ちなみに、この件は内部関数だけでなく、グローバル関数にも影響する問題です。なぜなら with オブジェクトは、スコープチェーン中のより高位なオブジェクト(グローバルオブジェクトやアクティベーションオブジェクト)を隠してしまうからです。

var x = 10;
 
with ({
 
  foo: function () {
    alert(this.x);
  },
  x: 20
 
}) {
 
  foo(); // 20
 
}
 
// なぜなら
 
var  fooReference = {
  base: __withObject,
  propertyName: 'foo'
};

同様の場面は、 catch 節の実引数である関数を呼び出す際にも起こりえます。この場合は、 catch オブジェクトがスコープチェーンの先頭に追加されます。つまり with の場合と同様に、アクティベーションオブジェクトやグローバルオブジェクトの手前に、です。しかし、この挙動は ECMA-262-3 のバグであると認識されており、標準の新しいバージョン ECMA-262-5 では、このような関数のアクティベーションでは、 this 値は catch オブジェクトではなく、グローバルオブジェクトにセットされます。

try {
  throw function () {
    alert(this);
  };
} catch (e) {
  e(); // __catchObject / ES3 、 (修正されて正しくは)グローバルオブジェクト / ES5
}
 
// 考え方としては
 
var eReference = {
  base: __catchObject,
  propertyName: 'e'
};
 
// しかしこれはバグであるため、
// this 値は global に強制される。
// null => global
 
var eReference = {
  base: global,
  propertyName: 'e'
};

さらに、名前付き関数式(関数についての詳細は第5章 関数を参照ください)を再帰的に呼び出す際も同様です。関数の最初の呼び出し時には、 base オブジェクトは親アクティベーションオブジェクト(またはグローバルオブジェクト)になるわけですが、再帰呼び出し時には、 base オブジェクトが関数式の任意な名前を保管する、特別なオブジェクトとなるのです。ただし、このケースでは、 this 値は常にグローバルオブジェクトにセットされるようになっています。

(function foo(bar) {
 
  alert(this);
 
  !bar && foo(1); // (再帰呼び出し)特別なオブジェクトである"べき"だが、常にグローバルオブジェクトとなる
 
})(); // (1回目:通常の呼び出し)グローバルオブジェクト

コンストラクタとして呼び出された関数での this 値

関数コンテキストの this 値にまつわるケースがもう一つあります。関数をコンストラクタとして呼び出した場合です。

function A() {
  alert(this); // 下で新しく作られるオブジェクト - "a" オブジェクト
  this.x = 10;
}
 
var a = new A();
alert(a.x); // 10

この場合、 new 演算子邦訳)が関数 A の内部 [[Construct]]邦訳)メソッドを呼び出し、続いて、オブジェクト生成後に、まったく同じ関数 A の内部 [[Call]]邦訳)メソッドを呼び出し、 this 値として新しく生成されたオブジェクトを渡します。

関数呼び出し時に手動で this を指定する

Function.prototype (すなわちすべての関数からアクセス可能)には二つのメソッドがあり、これらを用いれば、関数呼び出し時の this 値を手動で指定することができます。 .apply.call メソッドです。

これらは共に、呼び出されるコンテキストで使われる this 値を第一引数として受け取ります。差異はあまり重要ではありません。 .apply は必ず配列(または配列様オブジェクト、例えば arguments 等)を第二引数として受け取り、 .call はどんな引数でも受け取ります。この二つのメソッドに共通して必須なのは、第一引数、 this値です。

例です。

var b = 10;
 
function a(c) {
  alert(this.b);
  alert(c);
}
 
a(20); // this === グローバルオブジェクト, this.b == 10, c == 20
 
a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40

結論

この章では、 ECMAScript における this キーワードの機能について考察しました(そしてそれらは、 C++ や Java のものとは対照的なものでした)。この章が、 ECMAScript における this キーワードの働きをより正確に理解する手助けになることを期待します。そしていつものように、みなさんの質問にはコメントにて喜んでお答えします(訳注:このブログではコメントをいただけないのですが、日本語で拙ブログにコメントいただければ、わかる限りで訳者も喜んでお答えします)。

参考文献

英語版翻訳: Dmitry A. Soshnikov 、 Stoyan Stefanov の助力を借りて [英語版].

英語版公開日時: 2010-03-07

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]

補足と訂正: Zeroglif

オリジナルロシア語版公開日時: 2009-06-28; 更新:2010-03-07

本シリーズはすべて英語版からの訳出です。