サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

JavaScriptでオブジェクトに設定した関数のスコープ

ID: FN1203006 Technique: JavaScript

JavaScriptでイベントハンドラやコールバックなど、オブジェクトに関数を設定することがあります。その関数の中で変数がどのように参照されるかをご説明します。


01 定義した関数をオブジェクトに設定する
関数を定義して、その参照をオブジェクトに設定することが考えられます。

オブジェクト.関数名 = コールバック関数;
function コールバック関数(MouseEventオブジェクト) {
  // 処理内容
}

つぎのコード001は、forループで複数のObjectインスタンスをつくり(第7行目)、別に定義してある関数(method())をそれぞれに設定します(第9行目)。関数は3つの変数を参照して、配列に入れて返します(第13〜15行目)。これらの変数値はどの値になるのかが問題です。

コード001■定義した関数を複数のオブジェクトに設定する
  1. var i = 100;
  2. var n = 200;
  3. var _array = [];
  4. function test() {
  5.   for (var i = 0; i < 3; i++) {
  6.     var n = i;
  7.     var myObject = {};
  8.     myObject.i = i;
  9.     myObject.method = method;
  10.     _array.push(myObject);
  11.   }
  12. }
  13. function method() {
  14.   return [i, this.i, n];
  15. }
  16. test();

以下のようなスクリプトを加えれば、配列に納められた3つのObjectインスタンスを取出してその関数を呼出し、戻り値の配列の内容を確かめることができます。3つの配列のエレメント値は、つぎのように示されます。

100,0,200
100,1,200
100,2,200

var nLength = _array.length
for (var j = 0; j < nLength; j++) {
  var result = _array[j].method();
  document.write(result + "<br />");
}

3つのObjectインスタンスには同じ関数(method())を設定しました。関数にはローカル変数がありませんので、thisをつけない変数(iとn)についてはグローバルに定められた値(第1〜2行目)が参照されます。thisキーワードは、関数の設定されたオブジェクトの参照になります。したがって、thisを変数につければ、オブジェクトに設定された値(第8行目)が得られます。


02 名前のない関数をオブジェクトに設定する
名前のない関数をオブジェクトに直接設定することもできます。

オブジェクト.関数名 = function() {
  // 処理内容
}

つぎのコード002は、forループでつくったObjectインスタンスに、名前のない関数をそれぞれ設定しています(第9〜11行目)。関数の中身は、前掲コード001と同じです。3つの変数値をエレメントに納めた配列が返されます。

コード002■名前のない関数を複数のオブジェクトに設定する
  1. var i = 100;
  2. var n = 200;
  3. var _array = [];
  4. function test() {
  5.   for (var i = 0; i < 3; i++) {
  6.     var n = i;
  7.     var myObject = {};
  8.     myObject.i = i;
  9.     myObject.method = function() {
  10.       return [i, this.i, n];
  11.     };
  12.     _array.push(myObject);
  13.   }
  14. }
  15. test();

3つのオブジェクトの関数が返す配列のエレメント値は、つぎのように変わります。名前のない関数は、オブジェクトの処理を行う関数(test())の中でつくられて設定されました。つまり、関数が入れ子になっています。すると、入れ子の関数はグローバルな変数より先に、親関数のローカル変数を参照します。そのため、thisのつかない変数(iとn)は、ローカル変数の値(コード002第5〜6行目)をとるのです[*1]thisキーワードが関数の設定されたオブジェクトを参照することは変わりません。

3,0,2
3,1,2
3,2,2

前掲コード001との違いをもうひとつつけ加えます。コード001は3つのObjectインスタンスに同じ関数(method())を定めました。しかし、コード002では、3つのオブジェクトに同じ内容の名前のない関数をそれぞれつくって設定しているのです。つまり、名前のない関数はオブジェクトと同じ数だけでき上がっています[*2]

[*1] forループはカウンタ変数(i)が加算されて、継続条件(i < 3)を満たさなくなったとき処理を抜けます。そのため、iの値はnより1大きくなっています。

[*2] つぎのような等価比較を行うと、コード001ではtrue、コード002はfalseを返します。

_array[0].method == _array[1].method

03 入れ子関数でオブジェクトに関数を設定する
入れ子関数の少し変わった使い方を目にすることがあります。オブジェクトへの関数の設定を入れ子関数で行うのです。

(function(引数) {
  オブジェクト.関数名 = function() {
    // 処理内容
  }
})(引数)

このやり方で複数のObjectインスタンスに関数を設定したのが以下のコード003です。これは先に結果を見てしまいましょう。3つのオブジェクトの関数は、つぎのような配列を返します。前掲コード002とひとつ大きく異なるのは、this参照なしの変数(i)の値が、オブジェクトごとに違うことです。

0,0,2
1,1,2
2,2,2
コード003■入れ子関数の呼出しによりオブジェクトに関数を設定する
  1. var i = 100;
  2. var n = 200;
  3. var _array = [];
  4. function test() {
  5.   for (var i = 0; i < 3; i++) {
  6.     var n = i;
  7.     var myObject = {};
  8.     myObject.i = i;
  9.     (function(i) {
  10.       myObject.method = function() {
  11.         return [i, this.i, n];
  12.       }
  13.     })(i);
  14.     _array.push(myObject);
  15.   }
  16. }
  17. test();

このコード003のObjectインスタンスに関数を設定する処理(第9〜13行目)は、つぎのように書替えてもほぼ同じ内容になります[*3]。つまり、オブジェクトに関数を設定する入れ子の関数(setCallback)が定められ、直ちに呼出されているということです。

var setCallback = function(i) {
  myObject.method = function() {
    return [i, this.i, n];
  }
};
setCallback(i);

関数が呼出されると、その引数やローカル変数を納めるための暫定的なオブジェクトができます[*4]。関数が処理を終えると、そのオブジェクトは通常メモリから消されます(ガベージコレクションが働きます)。けれど、入れ子関数を他のオブジェクトに設定して保持するなど、参照を保ったままにすると、その暫定的なオブジェクトは消えません。つまり、設定した関数を呼出せば、引数やローカル変数にアクセスできるのです。前掲コード002で親関数のローカル変数が参照されたのもこの仕組みによります。

コード003では、さらに入れ子の関数(書替えたコードではsetCallback)をつくり、それをObjectインスタンスの数だけ繰返し呼出しました(第10〜13行目)。すると、呼出された関数には毎回異なる暫定のオブジェクトがつくられることになります。そして、関数を呼出すときカウンタ変数(i)を引数に渡しました(第13行目)。そのため、オブジェクトごとに違った引数値が残ったのです。

ただし、親関数(test())は1度しか呼出されていません(コード003第17行目)。したがって、3つのObjectインスタンスの関数が共通してひとつの親関数の(暫定オブジェクトがもつ)ローカル変数(n)を参照したのです。

もっとも、関数の設定される各オブジェクトに直接異なる変数を設定すれば、thisキーワードでそれぞれの値を得ることはコード001から003まで共通してできています。また、コード002でも注意したように、入れ子の関数はオブジェクトの数だけでき上がり、メモリを費やします。したがって、入れ子関数を使うことが必要な場合はかぎられるでしょう[*5]

[*3] 関数の参照を納めたローカル変数(setCallback)ができるという違いはあります。

[*4] JavaScriptではCallオブジェクトと呼ばれ、ECMAScriptにはActivationオブジェクトという名で定められています。ActionScript 3.0もECMAScriptにもとづきます。[ActionScript 3.0 コンポーネントリファレンスガイド]/[ステートメント、キーワード、ディレクティブ]の「withステートメント」では、Activationオブジェクトを「スクリプトが関数内で呼び出されたローカル変数を持つ関数を呼び出すときに自動的に作成されるテンポラリオブジェクト」と簡単に説明しています。

[*5] 親関数のローカル変数や引数まで含めた参照を保つ(閉じ込んだ)入れ子関数は「クロージャ」(closure)と呼ばれます。Mozilla Developer Network「関数」の「入れ子の関数とクロージャ」は、「クロージャは多くのメモリを消費する可能性があ」るので、「可能な限り関数を入れ子にするのは避け」るよう勧めています。

なお、ActionScript 3.0におけるクロージャについては、「リスナー関数が使う値を外から指定する」で解説しています。


04 クラスを入れ子関数として定義する
入れ子関数が親のローカル変数を参照する仕組みは、どのような場合に使えばよいのでしょう。それはクラス定義です。とくに多くのクラスをライブラリ化して公開するような場合には大きな意味があります。

ローカル変数は、関数の外からアクセスすることができません。コード001では、オブジェクトごとに異なる値は、オブジェクトの変数にしました。これで目的は果たせるものの、オブジェクトに設定した変数は外から書替えられます。小さなコンテンツでしたら、誤って上書きしないように気をつけさえすればよいことです。

しかし、たくさんのクラスを定める場合、変数が互いに重複しないようにするのは神経を使います。ましてや、公開されたライブラリは、中身を精査してから使うという人はあまりいません。重要な変数を知らずに上書きしてしまう恐れがあります。他の多くのプログラミング言語には、クラスの外からアクセスを許さないprivateという属性の指定があります。けれど、JavaScriptはそうした設定ができません。そこで、ローカル変数を使うのです。

JavaScriptでは、クラスは関数(function)として定めます。そのクラスの関数(コンストラクタ)を入れ子にして、親関数の呼出しによりクラスを定義するのです。すると、クラス定義に用いたvar宣言の変数には、親関数の外からアクセスできません。そこで、定義したクラスだけをグローバルなオブジェクト(windowオブジェクト)に設定します。

(function(window) {
  var コンストラクタ = function() {
    // 初期化
  }
  var プロトタイプ = コンストラクタ.prototype;
  // プロトタイプへのメソッド追加
  window.クラス = コンストラクタ;
})(window);

EaselJSでも、クラスはみなこのかたちで定義されています。たとえば、下図001はDisplayObjectクラスの一部です。クラス定義なら、ひとつのクラスにつき1度行うだけで済みますので、親関数を何度も呼出してメモリが無駄に費やされる恐れもありません。

図001■EaselJSのDisplayObjectクラス
図001


作成者: 野中文雄
作成日: 2012年3月23日


Copyright © 2001-2012 Fumio Nonaka.  All rights reserved.