[].slice.call(arguments)...?
現在Node.jsを勉強中で、Node.js デザインパターン を読んでます。
そんな中、読み進めていくとこんな記述が多くありました。
function hoge() {
...
const huga = [].slice.call(arguments, 1)
...
}
この部分。
結局ここは引数を配列にして返しているのですが、
slice
メソッドを空の配列に使っているという点と、
その後にcall(arguments, 1)
と繋がっているところで、やっていることがよくわからなかったので、自分なりに調べて考えてみました。
##argumentsについて
まず、arguments
について調べました。
mozillaの公式ドキュメントによると、
arguments は配列風Array-likeオブジェクトであり、関数に渡された引数の値を含んでおり、関数内からアクセスすることができます。
とのことで、関数内でアクセスできる"配列のような(array like)”オブジェクトとのことでした。
じゃあ前述したような処理を書かなくても、そのまま配列として利用できるのでは?と思い、下のコードを実行してみました。
function example () {
arguments.map(v => console.log(v))
}
example(1,2,3)
するとコンソールには以下のように出ます。
> "TypeError: arguments.map is not a function
at example (dolebug.js:4:13)
at dolebug.js:9:1
at https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:13924
at https://static.jsbin.com/js/prod/runner-4.1.8.min.js:1:10866"
つまり、arguments
ではmap
が利用できないということです。
そう。公式ドキュメントにあったとおり、arguments
はあくまでも**"配列のような"**オブジェクトなので、配列用の組み込みメソッドの全てに対応しているわけではないのです。
実際、公式ドキュメントでは以下の通りに書いてありました。
注: 「配列風」とは、 arguments が length プロパティと 0 から始まる添字のプロパティを持っているものの、 Array の組込みメソッド、例えば forEach() や map() を持っていないということです。
また、arguments
オブジェクトのプロパティは以下のものしかありません。
arguments.callee
個の引数が所属する、現在実行中の関数を参照します。厳格モードでは禁止されています。
arguments.length
関数に渡された引数の数を示します。
arguments[@@iterator]
新しい Array iterator オブジェクトで、 arguments のそれぞれの要素の値を含みます。
なので、arguments
オブジェクトを、配列と同様に扱うには適切な処理が必要です。
そこで、使われるのが今回の
[].slice.call(arguments)
になるということです。
##sliceメソッドについて
次に、slice
メソッドについて調べました。
一般的に、slice
メソッドは以下のように使われることが多いと思います。
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
console.log(animals.slice(2));
// expected output: Array ["camel", "duck", "elephant"]
ただ、それだけではなく、公式ドキュメントによるとslice
には以下のような使い方もあります。
slice メソッドを呼び出すことで、配列状オブジェクトやコレクションを新しい配列に変換することができます。メソッドをオブジェクトに bind するだけです。
つまり、slice
メソッドに今回のargumentsオブジェクトのような配列状オブジェクトをbindすれば、新しい配列に変換することができるということです。
今回の疑問は、まさにこの部分でした。しかし、ドキュメントでは「こうできるんですよ」としか書いておらず、疑問の解決にはなりませんでした。
なので、仕様書を読んでみることに。
仕様書で見るArray.prototype.slice
ECMAScriptの仕様書によると、sliceメソッドは、以下のような仕様になっています。
23.1.3.26 Array.prototype.slice ( start, end )
The slice method returns an array containing the elements of the array from element start up to, but not including, element end (or through the end of the array if end is undefined). If start is negative, it is treated as length + start where length is the length of the array. If end is negative, it is treated as length + end where length is the length of the array.
When the slice method is called, the following steps are taken:
- Let O be ? ToObject(this value).
- Let len be ? LengthOfArrayLike(O).
- Let relativeStart be ? ToIntegerOrInfinity(start).
- If relativeStart is -∞, let k be 0.
- Else if relativeStart < 0, let k be max(len + relativeStart, 0).
- Else, let k be min(relativeStart, len).
- If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
- If relativeEnd is -∞, let final be 0.
- Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
- Else, let final be min(relativeEnd, len).
- Let count be max(final - k, 0).
- Let A be ? ArraySpeciesCreate(O, count).
- Let n be 0.
- Repeat, while k < final,
a. Let Pk be ! ToString(𝔽(k)).
b. Let kPresent be ? HasProperty(O, Pk).
c. If kPresent is true, then
i. Let kValue be ? Get(O, Pk).
ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), kValue).
d. Set k to k + 1.
e. Set n to n + 1. - Perform ? Set(A, "length", 𝔽(n), true).
- Return A.
NOTE 1
The explicit setting of the "length" property of the result Array in step 15 was necessary in previous editions of ECMAScript to ensure that its length was correct in situations where the trailing elements of the result Array were not present. Setting "length" became unnecessary starting in ES2015 when the result Array was initialized to its proper length rather than an empty Array but is carried forward to preserve backward compatibility.
NOTE 2
The slice function is intentionally generic; it does not require that its this value be an Array. Therefore it can be transferred to other kinds of objects for use as a method.
英語なので読みづらいですが、簡単に訳すと、
「sliceメソッドは配列の start 要素から end 要素まで(end が未定義の場合は配列の終わりまで)の要素を含む配列を返します。start が負の場合は length + start として扱われ、 length は配列の長さとなります。end が負の場合は, length + end (length は配列の長さ)として扱われます.
sliceメソッドが呼ばれると,1~16の処理が行われます.」
みたいなことが冒頭に書かれています。
ただ、今回注目するべきはNOTE2の部分です。訳してみると、以下のように書かれています。
「slice関数は意図的に汎用的で、この値が配列であることを必要としません。よって、他の種類のオブジェクトに転送して、メソッドとして使用することができます。」
つまり、ECMAScriptの仕様決定の段階で、意図的に汎用的にしているということです。
実際、仕様のステップ2を見てみると、配列の長さであるlen
は、LengthOfArrayLike(obj)
を使用して取得しています。
LengthOfArrayLike(obj)
ECMAScriptの仕様書でLengthOfArrayLike(obj)を見てみると以下の通りになっています。
The abstract operation LengthOfArrayLike takes argument obj (an Object). It returns the value of the "length" property of an array-like object (as a non-negative integer). It performs the following steps when called:
- Return ℝ(? ToLength(? Get(obj, "length"))).
An array-like object is any object for which this operation returns an integer rather than an abrupt completion.
NOTE 1
Typically, an array-like object would also have some properties with integer index names. However, that is not a requirement of this definition.
NOTE 2
Arrays and String objects are examples of array-like objects.
簡単に訳すと
「LengthOfArrayLikeは、引数にオブジェクトを取ります。これは、配列状オブジェクトの "length"プロパティの値を(非負の整数として)返します。」
となります。上述した通り、argumentsオブジェクトにもlength
プロパティはあったので、これが適用されることがわかります。
また、以下のように配列状オブジェクトはインデックスを指定して要素を参照できるので、sliceメソッドが利用できます。
function huga () {
console.log(arguments[0])
console.log(arguments[1])
console.log(arguments[2])
}
huga(1,2,3)
// > 1
// > 2
// > 3
そして、NOTE2に注目すると、「Arrayオブジェクトや、Stringオブジェクトも配列状オブジェクトの例です。」と書かれています。
なのでslice
メソッドは、配列の長さの取得時にArrayオブジェクトを包含している配列状オブジェクトのlengthを取得することによって、汎用的に利用できるようにしているとわかりました。
この仕様書にある通り、Stringオブジェクトも配列状オブジェクトなので、
[].slice.call('hoge')
// => ['h', 'o', 'g', 'e']
というふうに、Array.prototype.slice
が適用できます。(String.prototype.slice
がありますが)
#まとめ
今回の疑問であった[].slice.call(arguments)
は結局、
「argumentsという配列状のオブジェクトを配列として扱いたいので、
配列状のオブジェクトに汎用的に利用できる[].slice === Array.prototype.slice
をbindして無理やり利用している」
ということがわかりました。
ただ、ES2015以降は、スプレッド構文やArray.from()
によって配列に変換できるとのことです。
var args = Array.from(arguments);
var args = [...arguments];
こちらの方が直感的ですね。