イントロダクション
関数の中では引数の値がすべて入った arguments
オブジェクトという特別な変数を使用でき、可変長引数を実現したいときなどに使われていた。しかし、今や rest parameter 構文があるので arguments
を使う必要はほぼなくなったと言っていいだろう(IE など一部ブラウザは対応していないが、Babel などで transpile すればいいだけの話である)。もし自分のコード上で arguments
を使わざるを得ない状況になったときこの記事が役立つだろう。
なおこの記事はECMAScript 2019 Language Specification に基づいている。
arguments
オブジェクトとは?
一部の関数の中では arguments
という変数(予約語やキーワードではない)が自動的に作られる。その値は引数が順番に入った Array のようなものである(Array ではない)。
function getArgs() {
return arguments
}
const args = getArgs("a", "b", "c")
console.log(args[0]) // a
console.log(args[1]) // b
console.log(args[2]) // c
arguments
オブジェクトが使えない条件
以下のいずれかが成り立てば、arguments
オブジェクトは使用できない。arguments
を使おうとしている場所を含んでいる最も内側の関数をFとする。
① FとFを包む関数がすべてアロー関数である
② Fの引数のいずれかの名前が arguments
③ Fの中で宣言された関数と let
か const
で宣言された変数のいずれかの名前が arguments
②と③については変数のシャドーイングのようなことが起こり、本来の arguments
オブジェクトにアクセスできなくなる(正確にはそもそも arguments
オブジェクトが生成されない)。
!(function (x, y) {
let arguments
console.log(arguments) // undefined
})(2, 3, 4)
一方、var
で arguments
を宣言した場合、その変数は本来の arguments
オブジェクトそのものとなる。
!(function () {
console.log(arguments[0]) // 2
var arguments // <- あってもなくても同じ
console.log(arguments[0]) // 2
})(2)
宣言時に代入すれば当然値は変わる。
!(function () {
console.log(arguments[0]) // 2
var arguments = "args"
console.log(arguments) // args
})(2)
NOTE: ③だけ成り立っている場合では、実は引数のデフォルト値を指定する部分では arguments
にアクセスできる。後述するが、
!(function (x, y = arguments[0]) {
let arguments = [100]
console.log(y) // 2
})(2)
arguments
オブジェクトは2種類ある
arguments
オブジェクトには unmapped なものと mapped なものがあり、前者は普通のオブジェクトだが、後者は引数の値と同期しているという性質がある。関数が呼び出された時、状況によってどちらか一方が作られる。
unmapped arguments
オブジェクトになる条件
以下のいずれかが成り立てば unmapped arguments
オブジェクトになる。逆にどれも成り立たなければ mapped arguments
となる。
- strict mode
- rest parameter がある (e.g.
function (a, b, ...args) { }
) - デフォルト値が指定されている (e.g.
function (a = 0) { }
) - destructuring している (e.g.
function ({ name, age }, x) { }
)
最近では strict mode でコーディングするのが普通なので、私達が遭遇するのはほとんど unmapped arguments
オブジェクトであると思われる。
共通の性質
unmapped、mapped 共通の性質を説明する。
iterable である
[Symbol.iterator]
プロパティを持っているため iterable であり、for-of でループしたり spread 構文で使用できる。
!(function () {
for (const arg of arguments) {
console.log(arg)
}
})(1, 2, 3)
array-like である
arguments
は Array と同じ形で要素をもっており、かつlength
プロパティが引数の個数になっているため、Array のメソッドを適用すると予想通りの振る舞いをする。
!(function () {
const arr = Array.prototype.slice.call(arguments)
console.log(arr) // [1, 2, 3]
})(1, 2, 3)
array-like の定義については筆者の👇の記事で詳しく説明している。
array-like object っていったい何?iterable との違いは?言語仕様に立ち返って説明する
toString
メソッドは "[object Arguments]"
を返す
これは独自の toString
メソッドを実装しているわけではなく、Object.prototype.toString()
メソッドの仕様である。
!(function () {
console.log(arguments.toString()) // [object Arguments]
})(1, 2, 3)
共通のプロパティ
プロパティの種類1 | writable? | enumerable? | configurable? | 備考 | |
---|---|---|---|---|---|
0, 1, ... | data | Yes | Yes | Yes | 各引数の値。 |
length | data | Yes | No | Yes | 関数に実際に渡された引数の数となる。Array と違って要素を追加/削除しても変動することはない。 |
[Symbol.iterator] | data | Yes | No | Yes | Array の values() メソッドと全く同じ。 |
extensible であるため、新しいプロパティを追加することもできる。
unmapped arguments
の性質
callee
プロパティ
callee
プロパティを取得/代入するとエラーが発生する。
プロパティの種類1 | writable? | enumerable? | configurable? | 備考 | |
---|---|---|---|---|---|
callee | accessor | - | No | No | get/set すると TypeError が発生する。 |
mapped arguments
の性質
mapped arguments
は exotic object と呼ばれる「通常のオブジェクトとは異なる振る舞いをするオブジェクト」の一種であり、実際、通常のオブジェクトでは実現できない振る舞いをする。
callee
プロパティ
callee
プロパティには、その arguments
オブジェクトに対応する関数がセットされている。
プロパティの種類1 | writable? | enumerable? | configurable? | 備考 | |
---|---|---|---|---|---|
callee | data | Yes | No | Yes | 呼び出されている関数 |
arguments
の要素と引数の値が同期している
NOTE: 「要素」=「プロパティ名が 0
、1
、2
...であるプロパティ」という意味である。
引数の値と arguments
の対応する要素の値は同期しており、どちらかの値を変えるともう一方も変わる。
!(function (x) {
console.log(x) // 1
arguments[0] = 2
console.log(x) // 2
x = 3
console.log(arguments[0]) // 3
})(1)
ただし、値が渡されていない引数については同期しない。
!(function (x, y, z) {
arguments[2] = 10
console.log(z) // undefined
z = 20
console.log(arguments[2]) // 10
})(1)
同期しなくなる条件
同期している引数も、以下の操作をすることで同期しなくなる。なお、これは不可逆であり、一度同期が切れると(その関数呼び出しに限り)戻ることはない。
NOTE: 一つの要素に対して以下のようなことをしても、すべての要素が同期しなくなるわけではない。あくまで要素ごとに同期する/しないがある。
要素を delete
する
arguments
オブジェクトの要素を delete
すると、たとえ再び同じインデックスの要素を作っても同期されないままになる。
!(function (x) {
console.log(`x=${x} arguments[0]=${arguments[0]}`)
delete arguments[0]
arguments[0] = 2
console.log(`x=${x} arguments[0]=${arguments[0]}`)
})(1)
x=1 arguments[0]=1
x=1 arguments[0]=2
writable 属性を false
にする
Object.defineProperty
で arguments
の要素の writable 属性 を false
にすることで同期されなくなる。ただし、同時に value
も指定すると、それは引数の変数に反映される。再び writable 属性を true
にしても同期されるようにはならない。
!(function (x) {
console.log(`x=${x} arguments[0]=${arguments[0]}`)
Object.defineProperty(arguments, "0", {
writable: false,
value: 2, // <- この値は x にも反映される
})
console.log(`x=${x} arguments[0]=${arguments[0]}`)
Object.defineProperty(arguments, "0", {
writable: true,
})
arguments[0] = 3
console.log(`x=${x} arguments[0]=${arguments[0]}`)
})(1)
x=1 arguments[0]=1
x=2 arguments[0]=2
x=2 arguments[0]=3
accessor property に変える
arguments
の要素はすべて data property1 であるが、それを accessor property1 に変えると同期しなくなる。
!(function (x) {
console.log(`x=${x} arguments[0]=${arguments[0]}`)
let inner = 2
Object.defineProperty(arguments, "0", {
get: () => inner,
set: v => inner = v,
})
console.log(`x=${x} arguments[0]=${arguments[0]}`)
arguments[0] = 3
console.log(`x=${x} arguments[0]=${arguments[0]}`)
})(1)
x=1 arguments[0]=1
x=1 arguments[0]=2
x=1 arguments[0]=3