M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

名前付きfunction式の名前の所在/仮引数とargumentsの不思議な関係

さて、最近オライリーから出版された某書をさんざんにコメントしておいてアレだけれど、その問題の本のおかげで知ることができたこともあった。

名前付きfunction式の名前の所在

そのうちの1つが名前付きのfunction式( function functionName (arg0, arg1, ...) { ... }; )であり、その名前の所在に関する問題である。

var f1 = function f2 () {
    return 1;
};

f1(); // これは 1 を返す。
f2(); // そしてこれはエラー。


では、"f2"という名前が所属するスコープはどこかというと、f2関数のローカルスコープ内である。

したがってOCJ−P(旧・SJC-P)の問題開発チームがJavaScriptの資格試験をつくったとしたら、次のような問題が出題されることになるかもしれない。

var x = function(n){
    return n <= 0 
        ? 0 
        : n === 1 
            ? 1 
            : n * factorial(n - 1);
};

var y = function x(n){
    return x(n - 1);
};

y(123); // さてこの関数呼び出しは何を返す?


結果はたぶんスタックオーバーフローである。少なくともまともな終わり方はしない。

ちなみにこのとき1つめの関数定義がなければ呼出元のスコープにはxという名前は存在しない。つまり2つめの関数定義で1つめの定義が上書きされているのではない。呼出元スコープでは、xとyという変数にそれぞれ1つずつ関数オブジェクトへの参照がセットされるのである。

そしてyが指す関数のローカル・スコープでは、xはその関数自身を指す名前として登場する。このとき呼出元のスコープにあるxという名前はシャドウ化されて、yからは参照されない状態となる。

仮引数とargumentsの不思議な関係

私が某書から学んだもう1つの興味深い事項は、仮引数とargumentsの関係である。

結論から言おう。

  • argumentsはFunction()オブジェクト(Functionコンストラクタ関数が生成するオブジェクト)のプロパティである。つまり実用面ではそのように考えてよいとしても、本質的にはローカル変数の一種ではない。
  • プロパティargumentsには関数のローカル・スコープ外ではnullが設定されており、ローカル・スコープ内では(皆さんおなじみの)“配列のような”オブジェクトへの参照が設定されている。JavaScriptはシングル・スレッドで実行される言語なので、次のように言い換えられる(というよりもこちらがより真実なのだろう)──関数実行中は当該関数オブジェクトのargumentsプロパティに配列のようなオブジェクトへの参照が設定されており、実行が終了すると同プロパティにはnullが設定される。
  • ところでこのargumentsを通じて関数の引数として与えられた値を参照することができるけれど、この“配列のような”オブジェクトへの変更はそのまま仮引数にも反映される。つまり arguments[0] = "foo"; といった手続きを行うと、1つめの仮引数を通じて参照される対象も"foo"になる。
  • 逆もまたしかり。したがって、function(arg0, arg1){...}; というような関数があった場合、仮引数arg0 は arguments[0] の、また仮引数arg1 は arguments[1] の、それぞれ糖衣もしくはエイリアス以外の何モノでもないような動きを見せることになる。
var f3 = function(n){
    console.log(f3.arguments); // => "[object Arguments]"
    
    arguments[0] = 2;
    console.log(n);            // => "2" !?
    
    n = 3;
    console.log(arguments[0]); // => "3" !!
    
    var f4 = function(n){
        console.log(f3.arguments[0]);
    };
    
    f4(4);
    
    return arguments;
};

console.log(f3.arguments);    // => "null"

var args = f3(1);

console.log(f3.arguments);   // => "null"
console.log(args[0]);        // => "3"


ここで挙げたいずれの問題も、混乱の元になりこそすれ実用性はほとんどない。ただ名前付きfunction式は、arguments.calleeの使用をせずに再帰関数を定義することを可能にするという効用を持つ。