C/C++ プログラマのための JavaScript 入門: 関数とスコープ

no extension

オブジェクトの話の3回目です。 今回から各データ型に対応するオブジェクトについて説明していきます。 とりあえず基本型の話はさらっと流して関数オブジェクトについての話から。 例によって「プロローグ」で挙げた文献を頻繁に参照しています。 これらの文献を参照しながらご覧になることをお薦めします。

「データ型」の回では, 6つのデータ型を基本型と参照型に分けて説明しました。 基本型は基本的に「by value」な振る舞いをするのですが, オブジェクトのように振舞うこともあります。 例えば

WScript.Echo("11 < 2 =",  (11 < 2).toString()); // false

といった場合です。 (11 < 2) の結果は論理値ですが, この場合テンポラリに論理オブジェクト(Boolean)を生成し, そのオブジェクトのメンバである toString() メソッドを呼び出しています。 つまり暗黙的に型変換を行っているわけです。

参照型はどうでしょう。 実は3つの参照型はいずれも自身がオブジェクトとして振る舞います。 関数も配列もオブジェクトです(この辺の話は次回します)。 オブジェクトとしての関数を特に「関数オブジェクト」と呼ぶ場合があります。 関数オブジェクトには様々な組み込みプロパティやメソッドがあります。 前回紹介した prototype も組み込みプロパティです。

関数を設計する際にはスコープに注意する必要があります。 ある関数について設定・参照されるスコープは2つだけです。 すなわち関数内部のスコープと外部のスコープです。 関数の内部から外部のスコープを設定・参照することはできますが, 関数の外部から内部のスコープを設定・参照することはできません。 関数定義は入れ子にすることができるので(関数がオブジェクトであることを思い出してください), スコープも入れ子になります。 ある関数内の変数を設定・参照する際は自身の内部スコープから外部に向かって順に探していくことになります。 これを「スコープ・チェイン」と呼びます。 スコープ・チェインの終端は(以前紹介した) Global オブジェクトです。 Global オブジェクトに対して関数内部のスコープを表すオブジェクトは Call オブジェクトと呼ばれます。 Call オブジェクトのインスタンスは関数ごとにひとつ作成されます。

関数内のローカル変数は Call インスタンスのプロパティとして格納されます。 ここで Call インスタンスが関数ごとにひとつしか作成されないことに注意してください。 例えば関数内部の処理をブロックで囲っても, ブロック内の変数のスコープは関数全体に及びます。

var scape = "global";
WScript.Echo("scape = " + scape); //global
function checkscope() {
  WScript.Echo("scape = " + scape); //ローカル・スコープだが値が設定されていないので undefined
  {
    var scape = "local";
    WScript.Echo("scape = " + scape); //local
  }
}
checkscope();

関数の外部から内部のスコープを参照できないことを利用して, 擬似的に private 変数を定義することができます。

function UniqueId() {
  var counter;
  this.resetCount = function() {
    counter = 0;
  }
  this.setCount = function() {
    return counter++;
  }
}
var o = new UniqueId();
o.resetCount();
WScript.Echo("o.counter = " + o.counter); // undefined
WScript.Echo("o->ID = " + o.setCount()); // 0
WScript.Echo("o->ID = " + o.setCount()); // 1
var o2 = new UniqueId();
WScript.Echo("o2.counter = " + o2.counter); // undefined
WScript.Echo("o->ID = " + o.setCount()); // 2
WScript.Echo("o2->ID = " + o2.setCount()); // NaN
o2.resetCount();
WScript.Echo("o2->ID = " + o2.setCount()); // 0
WScript.Echo("o2->ID = " + o2.setCount()); // 1
WScript.Echo("o->ID = " + o.setCount()); // 3

ただしこの方法ではメソッドを継承できません。 無理に prototype を使おうとすると以下のようになります。

function UniqueId() {
  var counter;
}
UniqueId.prototype.resetCount = function() {
  counter = 0;
}
UniqueId.prototype.setCount = function() {
  return counter++;
}
var o = new UniqueId();
o.resetCount();
WScript.Echo("o.counter = " + o.counter); // undefined
WScript.Echo("o->ID = " + o.setCount()); // 0
WScript.Echo("o->ID = " + o.setCount()); // 1
var o2 = new UniqueId();
WScript.Echo("o2.counter = " + o2.counter); // undefined
WScript.Echo("o->ID = " + o.setCount()); // 2
WScript.Echo("o2->ID = " + o2.setCount()); // 3
o2.resetCount();
WScript.Echo("o2->ID = " + o2.setCount()); // 0
WScript.Echo("o2->ID = " + o2.setCount()); // 1
WScript.Echo("o->ID = " + o.setCount()); // 2

前者のメソッドはインスタンスで, 後者のメソッドは関数オブジェクトで動作しているのが違いです。

関数 a() が関数 b() の中で定義されている場合, 関数 a() のスコープ・チェインは a() の Call インスタンス→ b() の Call インスタンス→ Global インスタンスの順で参照されます。 これは関数 a() が関数 b() の外部で呼び出された場合でも同じです。 分かりにくいですね。 以下に簡単な例を挙げます。

function times(u) {
  return function(n) {
    return n*u;
  };
}
function kuku(fn) {
  var arr = new Array();
  for(var i=1; i<10; i++) {
    arr[i] = fn(i);
  }
  return arr;
}
var kukuArray = new Array();
for(var j=1; j<10; j++) {
  kukuArray[j] = kuku(times(j));
}
WScript.Echo("1×2 = " + kukuArray[1][2]); // 2
WScript.Echo("3×5 = " + kukuArray[3][5]); // 15
WScript.Echo("9×9 = " + kukuArray[9][9]); // 81

これは九九表を作る処理です。 times() 関数内で掛け算を行うラムダ関数を返しています(まぁ普通はこんな面倒なことはしないと思いますが,ご容赦を)。 このラムダ関数が使われるのは kuku() 関数内なのですが, スコープ・チェインは明らかに times() 関数を参照しているのが分かると思います。 このような関数の構造を「クロージャ」と呼びます。 JavaScript では入れ子の関数は全てクロージャになります。

Call インスタンスには暗黙的に arguments プロパティが設定されます。 arguments プロパティは配列になっていて関数実行時の引数(実引数)のリストが格納されます。 JavaScript では関数定義時の引数(仮引数)は絶対ではありません。 仮引数より少ない数の実引数もありえますし, 仮引数より多い数の実引数もありえます。 実引数が仮引数より少ない場合は undefined がセットされます。 このことを利用して引数の省略や可変長引数をエミュレートできます。

function marge(str) {
  var s = "";
  if((typeof str)=="undefined" || arguments.length==0) {
    s = "default string!"
  }
  else {
    for(var i=0; i<arguments.length; i++) {
      s += arguments[i].toString();
    }
  }
  return s;
}
WScript.Echo(marge()); // default string!
var nothing; //undefined
WScript.Echo(marge(nothing)); // default string!
WScript.Echo(marge("sting")); // string
WScript.Echo(marge("sting", " more")); // string more

関数オブジェクトについてはこんなものでしょうか。 関数オブジェクトや Call オブジェクトについては他にも有用なプロパティやメソッドがありますが, ここでは割愛します。 次回は連想配列と配列について, です。