arguments オブジェクト詳説 [ES2019]

# イントロダクション

関数の中では引数の値がすべて入った `arguments` オブジェクトという特別な変数を使用でき、可変長引数を実現したいときなどに使われていた。しかし、今や rest parameter 構文があるので `arguments` を使う必要はほぼなくなったと言っていいだろう(IE など一部ブラウザは対応していないが、Babel などで transpile すればいいだけの話である)。もし自分のコード上で `arguments` を使わざるを得ない状況になったときこの記事が役立つだろう。

なおこの記事は[ECMAScript 2019 Language Specification](https://www.ecma-international.org/ecma-262/10.0/index.html) に基づいている。

# `arguments` オブジェクトとは？

一部の関数の中では `arguments` という変数(予約語やキーワードではない)が自動的に作られる。その値は引数が順番に入った Array のようなものである(Array ではない)。

```js
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` オブジェクトが生成されない)。

```js
!(function (x, y) {
  let arguments
  console.log(arguments)  // undefined
})(2, 3, 4)
```

一方、`var` で `arguments` を宣言した場合、その変数は本来の `arguments` オブジェクトそのものとなる。

```js
!(function () {
  console.log(arguments[0])  // 2
  var arguments              // <- あってもなくても同じ
  console.log(arguments[0])  // 2
})(2)
```

宣言時に代入すれば当然値は変わる。

```js
!(function () {
  console.log(arguments[0])  // 2
  var arguments = "args"
  console.log(arguments)     // args
})(2)
```

**NOTE:** ③だけ成り立っている場合では、実は引数のデフォルト値を指定する部分では `arguments` にアクセスできる。後述するが、

```js
!(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 構文で使用できる。

```js
!(function () {
  for (const arg of arguments) {
    console.log(arg)
  }
})(1, 2, 3)
```

## array-like である

`arguments` は Array と同じ形で要素をもっており、かつ`length` プロパティが引数の個数になっているため、Array のメソッドを適用すると予想通りの振る舞いをする。

```js
!(function () {
  const arr = Array.prototype.slice.call(arguments)
  console.log(arr)  // [1, 2, 3]
})(1, 2, 3)
```

array-like の定義については筆者の👇の記事で詳しく説明している。

[array-like object っていったい何？iterable との違いは？言語仕様に立ち返って説明する](https://qiita.com/righteous/items/56d6086ec83be08d3553)

## `toString` メソッドは `"[object Arguments]"` を返す

これは独自の `toString` メソッドを実装しているわけではなく、`Object.prototype.toString()` メソッドの仕様である。

```js
!(function () {
  console.log(arguments.toString())  // [object Arguments]
})(1, 2, 3)
```

## 共通のプロパティ

|  | プロパティの種類[^prop] | writable? | enumerable? | configurable? | 備考 |
|-------------------|------------------|-----------|-------------|---------------|----------------|
| 0, 1, ... | data | Yes | Yes | Yes | 各引数の値。 |
| length | data | Yes | No | Yes | 関数に**実際に渡された引数の数**となる。Array と違って要素を追加/削除しても変動することはない。 |
| [Symbol.iterator] | data | Yes | No | Yes | Array の `values()` メソッドと全く同じ。 |

[^prop]: プロパティには２種類あり、data property は普通のプロパティ、accessor property は getter と setter によって定義されるプロパティである。

extensible であるため、新しいプロパティを追加することもできる。

# unmapped `arguments` の性質

## `callee` プロパティ

`callee` プロパティを取得/代入するとエラーが発生する。

|  | プロパティの種類[^prop] | writable? | enumerable? | configurable? | 備考 |
|-------------------|------------------|-----------|-------------|---------------|----------------|
| callee | accessor | - | No | No | get/set すると TypeError が発生する。 |

# mapped `arguments` の性質

mapped `arguments` は exotic object と呼ばれる「通常のオブジェクトとは異なる振る舞いをするオブジェクト」の一種であり、実際、通常のオブジェクトでは実現できない振る舞いをする。

## `callee` プロパティ

`callee` プロパティには、その `arguments` オブジェクトに対応する関数がセットされている。

|  | プロパティの種類[^prop] | writable? | enumerable? | configurable? | 備考 |
|-------------------|------------------|-----------|-------------|---------------|----------------|
| callee | data | Yes | No | Yes | 呼び出されている関数 |

## `arguments` の要素と引数の値が同期している

NOTE: 「要素」=「プロパティ名が `0`、`1`、`2` ...であるプロパティ」という意味である。

引数の値と `arguments` の対応する要素の値は同期しており、どちらかの値を変えるともう一方も変わる。

```js
!(function (x) {
  console.log(x)             // 1
  arguments[0] = 2
  console.log(x)             // 2
  x = 3
  console.log(arguments[0])  // 3
})(1)
```

ただし、値が渡されていない引数については同期しない。

```js
!(function (x, y, z) {
  arguments[2] = 10
  console.log(z)             // undefined

  z = 20
  console.log(arguments[2])  // 10
})(1)
```

## 同期しなくなる条件

同期している引数も、以下の操作をすることで同期しなくなる。なお、これは不可逆であり、一度同期が切れると(その関数呼び出しに限り)戻ることはない。

**NOTE:** 一つの要素に対して以下のようなことをしても、すべての要素が同期しなくなるわけではない。あくまで要素ごとに同期する/しないがある。

### 要素を `delete` する

`arguments` オブジェクトの要素を `delete` すると、たとえ再び同じインデックスの要素を作っても同期されないままになる。

```js
!(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` にしても同期されるようにはならない。

```js
!(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 property[^prop] であるが、それを accessor property[^prop] に変えると同期しなくなる。

```js
!(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
```