2010年 02月 21日



JavaScript、仮引数で宣言した変数へ代入したあとの arguments の挙動

さて問題です。以下のコードで alert されるのは何でしょう!!

(function (x) {
  x = 2;
  alert(arguments[0]);
})(1);


答えはやってみてください。ビビりました。どうやら arguments オブジェクトは、変数の参照 (値の参照ではなく) を持っています。

ECMAScript 3rd Edition 日本語訳から根拠を探すと、ちゃんと書いてありました。

  • 0 以上 length プロパティの値未満の整数 arg それぞれについて、属性 { DontEnum } のプロパティ ToString(arg) が作成される。このプロパティの初期値は対応するパラメータの呼出側に供給される実際の値である。最初の実際のパラメータ値が arg = 0、2 番目は arg = 1, 以下同様である。arg が Function オブジェクトの仮引数の数より小さい場合、このプロパティは Activation オブジェクトの対応するプロパティとその値を共有する。このことは、このプロパティの変更が Activation オブジェクトの対応するプロパティを変更すること、そしてその逆を意味する。
http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/10_Execution_Contexts.html#section-10.1.8

ちゃんと説明しとくと、この動作の奇妙さは

var obj = new Object();

obj[0] = 1;
obj.foo = obj[0];

obj[0] = 2;
alert(obj.foo); //=> 1

var baz = new Object();
baz.foo = obj[0];

obj[0] = 3;
alert(baz.foo); //=> 2

というあたり前 (に感じる) 挙動崩れるところです。普通、オブジェクトの別プロパティに代入したら、他のプロパティの値も同時に変わることなんてことはありえないし、別オブジェクトのプロパティが変わるなんてもってのほかですが、Activation オブジェクトの引数関係のプロパティだけは違う、ということです。

Activation オブジェクトはそもそも何かというと、変数を保持するオブジェクトです。この、隠れたオブジェクトを見えるようにしたコードを書くと、冒頭のコードは

(function (x) {
  // // Variable Instantiation
  // var a = new Activation(); 
  // a.arguments = { };
  // a.arguments[0] = 1;
  // a["x"] = a.arguments[0];
  // // End

  // a["x"] = 2;
  x = 2;

  // alert(a.arguments[0]);
  alert(arguments[0]);
})(1);

のように (実際は Activation オブジェクトにはアクセスできない) なり、そもそも仮引数 x が保持されているオブジェクトと、渡された引数第一番目の arguments[0] は別々のオブジェクトなのにも関わらず、値が共有されて相互作用をします。

これは「変数」を保持している「オブジェクト」が存在するという仕様上の説明からすると、一貫性がない不可解な動作です。

JSDeferred Deferred.chain

http://lab.hisasann.com/addCommand/index.html を見ていて、next().next() 以外に、こういう風に書けても別にいいよなぁと思ったので実装を書いてみた。next().next() はめんどい割とめんどいし

git master HEAD には今のところ Deferred.chain() として入ってる。これと Deferred.connect() の変数バインドを組み合せて、addCommand.js のデモ相当をしてみた。

それっぽいとこを抜きだすと

Deferred.define();

var img1 = $("#img1"),
	img2 = $("#img2"),
	img3 = $("#img3");
(function() {
	var callee  = arguments.callee;

	var animate = function (target, prop, speed, easing) {
		return Deferred.connect(target, "animate", { args: [ prop, speed, easing ] });
	};

	Deferred.chain(
		animate(img1, {
			opacity: 0, top: 90 + "px", left: 260 + "px"
			}, 1000, "easeInOutCirc"),
		animate(img2, {
			opacity: 0, top: 170 + "px", left: 450 + "px"
			}, 1000, "easeInOutCirc"),
		animate(img3, {
			opacity: 0, top: 260 + "px", left: 620 + "px"
			}, 1000, "easeInOutCirc"),
		[
			animate(img1, {
				opacity: 1, top: 0 + "px", left: 0 + "px"
				}, 1000, "easeInBack"),
			animate(img2, {
				opacity: 1, top: 0 + "px", left: 0 + "px"
				}, 1000, "easeInBack"),
			animate(img3, {
				opacity: 1, top: 0 + "px", left: 0 + "px"
				}, 1000, "easeInBack")
		],
		function () {
			return wait(0.5).next(callee);
		},
		function error (e) {
			alert(e);
		}
	);
})();

となっていて、デモのコードとほぼ同じになる。エラーをキャッチする関数には function error() としておくルールがある。

キモは animate() を実行した時点ではアニメーションは実行されず、アニメーションを実行する関数を返すだけのところです。(Deferred.connect は関数を返す関数です)

便利なので入れたい気もするけど、うまく書かないと混乱するのでむずかしい。