Argumentsと関数内の変数環境の関係
おはようございます。
この記事はJavascript Advent Calender 2014の22日目の記事です。
JavaScript Advent Calendar 2014 - Qiita
domenicのツイートをストーキングしてたら、とある話を拾った。
I forgot about this until yesterday: `var f = function(a) { a = 'lol'; console.log(arguments) }; f(42);`
— Ryan Florence (@ryanflorence) 2014, 12月 20
議題のコードはこれ
var f = function(a) { a = 'lol'; console.log(arguments); }; f(42); // 実行結果 // ["lol"]
Argumentsオブジェクトを普通のオブジェクトと思っているとここで違和感を感じる
もう一個例を見てみる
var f = function (a) { a = 'lol'; console.log(b); } var b = {d: 10}; f(b.d); // 実行結果 // Object {d: 10}
違和感を感じた人は、多分この結果と同じように引数aはarguments[0]の値をコピー(今回はプリミティブ値なので)してるように考えるので、aを変更したところでarguments[0]自身には影響がないはずなのでは?って思ってしまうんだと思う。
さらに
var f = function(a) { arguments[0] = 'lol'; console.log(a) }; f(42); // 実行結果 // 'lol'
これもArgumentsにとても詳しいおじさん以外きっと違和感を感じると思う。
これらの挙動を見るにArgumentsは普通のオブジェクトではない。
結構いろいろ変なやつっぽい。
仕様を見てみるとその内部的な動きがわかった。
Argumentsは内部的にCreateMappedArgumentsObject (ES5まではCreateArgumentsObject)の呼出しによって生成される。
- ES6-draft
- ES5
ざっくりいうと、Argumentsオブジェクトに引数でセットされた値を引数インデックスをキーとしてセットしている。
そしてミソなのは、非strictモード化のときに、内部的にmapというオブジェクトを作成し、その中に引数にセットされた値を引数indexをキーとしてセットし、Argumentsから参照([[Get]]
)された時にmapの該当キーの値を返却する
(strictモード化では単純にArgumentsにセットされた値を返却するだけ)。
このmapに対する引数のセットの仕方が特殊で以下のように記述されてる
Let g be MakeArgGetter(name, env).
Let p be MakeArgSetter(name, env).
Call the
[[DefineOwnProperty]]
internal method of map passing ToString(index) and the PropertyDescriptor{[[Set]]
: p,[[Get]]
: g,[[Enumerable]]
: false,[[Configurable]]
: true} as arguments.
単純に[[Value]]
にセットするのではなくMakeArgGetter/Setterにenvと共に渡した結果を[[Getter]]
、[[Setter]]
にセットしてプロパティ定義をしている。
※ ここでいうnameは引数名 function fn(a, b){} だとしたら aやbという引数名にあたる
※ ここでいうenvはこのargumentsの存在する関数対する変数環境にあたる(所謂 変数オブジェクト)
このMakeArgGetter/Setterがこの一見変な挙動を生み出してる。
MakeArgGetterは以下のように記述されている
9.4.4.7.1 MakeArgGetter ( name, env) Abstract Operation
The abstract operation MakeArgGetter called with String name and environment record env creates a built-in function object that when executed returns the value bound for name in env.
適当な約: MakeArgGetterは変数環境envのnameの値を返却するような関数を作る
es5では簡単にこんな風にかかれてる
- Let body be the result of concatenating the Strings "return ", name, and ";".
- Return the result of creating a function object as described in 13.2 using no FormalParameterList, body for FunctionBody, env as Scope, and true for Strict.
scopeにenvをセットするってかいてあるので擬似的に表現するとこんな感じになると思う。
function A(a, b) { // arguments[0]の[[Get]]は function () { return a; } // arguments[1]の[[Get]]は function () { return b; } }
es6-draftの方ではMakeArgGetterの返す関数について
- Return the result of calling the GetBindingValue concrete method of env with arguments name and false.
となってるので挙動として同様の結果になるはず(GetBindingValueは変数環境から与えられたnameの値を返却する内部処理)
こう考えれば 最初の例(↓)
var f = function(a) { a = 'lol'; console.log(arguments); }; f(42); // 実行結果 // ["lol"]
のように変数aが'lol'に書き変わればarguments[0]の値もgetterを通して変数aが返却されてるだけなので同様の結果を返すということがわかる。
またMakeArgSetterは以下のように記述されてる
The abstract operation MakeArgSetter called with String name and environment record env creates a built-in function object that when executed sets the value bound for name in env.
適当な約: MakeArgSetterは変数環境envのnameにvalueをセットする関数を作る
es5では簡単にこんな風にかかれてる
Let param be the String name concatenated with the String "_arg".
Let body be the String
"<name> = <param>;"
withreplaced by the value of name and <param>
replaced by the value of param.Return the result of creating a function object as described in 13.2 using a List containing the single String param as FormalParameterList, body for FunctionBody, env as Scope, and true for Strict.
scopeにenvをセットするってかいてあるので擬似的に表現するとこんな感じになると思う。
function A(a, b) { // arguments[0]の[[Set]]は function (_arga) { return a = _arga; } // arguments[1]の[[Set]]は function (_argb) { return b = _argb; } }
es6-draftの方では、MakeArgSetterの返す関数について
- Return the result of calling the SetMutableBinding concrete method of env with arguments name, value, and false.
となってるのでこれもまた挙動的には同じだと思う(SetMutableBindingは変数環境に存在するnameの値内部にvalueをセットする内部処理)
こう考えれば 結構前の例(↓)
var f = function(a) { arguments[0] = 'lol'; console.log(a) }; f(42); // 実行結果 // 'lol'
のようにarguments[0]が'lol'に書き変わればsetterを通して変数環境aに'lol'がセットされるので変数aも同様に値がかわってしまうことがわかる
とまぁ挙動に関してはここまででわかったけど辛いの'use strict'つけて回避したいですね。(callee, callerもアクセス拒否されるようになるし)
あとes6では...args
(argumentsの単純な配列版)があるのでそれを使えばこの現象に遭遇することもないんでしょうね
おわりです。