JavaScript
メモ

【小ネタ】argumentsなどのArray-likeオブジェクトにforEachなどを適用する【JavaScript】


はじめに

JavaScriptの関数には、暗黙的に渡される変数「arguments」が存在します。これは当該関数に渡された引数を保持しています。

「arguments」オブジェクトは「length」プロパティを持つので、数字の添え字を指定して任意の値を取得することができ、配列のように扱うことができます。しかしながら、Arrayオブジェクトが通常持っているプロパティを(length以外は)持っておらず、mapやforEachなどの便利なメソッドを利用することができません。

この記事では、callメソッドを活用し、「arguments」オブジェクトに対してforEachなどを適用する方法を示します。

便宜上、ここではforEachメソッドを用いていますが、push()やmap()などArrayオブジェクトのメソッドを何でも適用することができるようになります。


TL;DR


  • argumentsはArrayではない。飽くまでArray-likeなオブジェクト。

  • Functionオブジェクトのcallメソッドは、当該関数実行時の「this」オブジェクトが指すものを変えることが出来る。

  • Array.prototype.forEach.callといったように、Arrayオブジェクトのメソッドを、Functionオブジェクトのcallメソッドと併せて呼び出すことができる。

  • forEachメソッド実行時の「this」を「arguments」オブジェクトにすることで、「arguments」オブジェクトに対してforEachやpush、mapなどを実行することが出来る。

  • Arrayオブジェクト以外のメソッドも適用しようとすればできるが、too much informationになるので割愛。


試してみる

サンプルコードは下記の通りです。

引数として渡された値を全て合計して返すsumAllValues関数がそれになります。


動かない例


function sumAllValues() {
var result = 0;

// argumentsがArrayオブジェクトであればこう書きたいところですが、実際には下記のようにエラーとなります。
// Uncaught TypeError: arguments.forEach is not a function
arguments.forEach((n) => result += N);

return result;
};

// sumAllValues関数実行中にエラーとなる
console.log(sumAllValues(1,2,3,4,5));



動く例


function sumAllValues() {
var result = 0;
Array.prototype.forEach.call(arguments, (n) => result += N);

return result;
};

// console.logの出力結果は 15
console.log(sumAllValues(1,2,3,4,5));



解説


argumentsとは

argumentsの説明は、MDNの下記ページにあります。

ポイントは下記引用の通りです。

The arguments object - JavaScript | MDN

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments


arguments is an Array-like object accessible inside functions that contains the values of the arguments passed to that function.

Note: “Array-like” means that arguments has a length property and properties indexed from zero, but it doesn't have Array's built-in methods like forEach and map. See §Description for details.


余談ですが、DOMを操作するAPIにはgetElementsByClassNameやquerySelectorAllなど、複数の値を返すものがありますが、これらもArray-likeオブジェクトを返却します。


callとapply

callとapplyはFunctionオブジェクトが持つメソッドであり、実行対象の関数の「this」の値を指定して実行します。

callとapplyの違いは、実際に実行される関数に対してどのように値を渡すかの違いであり、本稿の目的にはあまり関係が無いので割愛し、ここではcallメソッドのみにフォーカスします。

callメソッドの詳しい説明は、MDNの下記ページにあります。

Function.prototype.call() - JavaScript | MDN

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call

ポイントとなるのは下記の1行です。これを詳しく見てみます。

    Array.prototype.forEach.call(arguments, (n) => result += N);

forEachメソッドは、配列のような一連の値に対して1つずつ処理を行いますが、これは引数としてコールバック関数を取ります。

Array.prototype.forEach() - JavaScript | MDN

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

Array.prototype.forEachメソッドは、引数として指定されるコールバック関数の実行前に、処理対象の配列の値が決まります。下記のように、単にArray.prototype.forEachメソッドを実行しただけでは、処理対象の値が存在せず、実質何も行いません。

    Array.prototype.forEach((n) => result += N);

argumentsオブジェクトに対してArray.prototype.forEachメソッドを実行するためには、このメソッド実行時の「this」オブジェクトが、「arguments」オブジェクトになるようにする必要があります。

ここでcallメソッドが出てきます。これは対象の関数を実行する際に、当該関数から参照できる「this」オブジェクトの値に、任意のものを指定することが可能です。

以下のように指定することで、Array.prototype.forEachメソッド実行時の「this」オブジェクトが、「arguments」オブジェクトとなります。

    Array.prototype.forEach.call(arguments, (n) => result += N);

callメソッドはFunctionオブジェクトのプロパティで、Function.prototype.callとして定義されているため、全てのFunctionオブジェクトから参照可能です。

Array.prototype.forEachメソッドはFunctionオブジェクトですので、Function.prototype.callメソッドも利用できることになります。


その他の解法


その1. スプレッド構文で新たなArrayオブジェクトを生成(shallow copy)する

ピリオド3つの「...」演算子(スプレッド構文)が使えるのであれば、下記のように書いても良いですね。というか昨今、Babel等のトランスパイラの発展により、ES2015(ES6)が概ね標準的に利用できるようになってきているので、これがシンプルで分かりやすいベストな解法かもしれません。

function sumAllValues() {

var result = 0;

// Arrayオブジェクトを作成して、argumentsの各要素をいったんその中に展開する。
// ArrayオブジェクトのforEachメソッドを利用して処理する。
[...arguments].forEach((n) => result += n);

return result;
};

// console.logの出力結果は 15
console.log(sumAllValues(1,2,3,4,5));


その2. Arrayオブジェクトのsliceメソッドで新たなArrayオブジェクトを生成(shallow copy)する

よく見るイディオムです。

挙動としては上記の方法とほぼ同じかと思います。

function sumAllValues() {

var result = 0;

// sliceメソッドでargumentsオブジェクトから値を切り出す。
// sliceメソッドに対する引数は指定していないので、argumentsオブジェクトの全ての要素が返却される。
// sliceメソッドはArrayオブジェクトを返却するため、forEachなどのメソッドが利用できる。
Array.prototype.slice.call(arguments).forEach((n) => result += n);

return result;
};

// console.logの出力結果は 15
console.log(sumAllValues(1,2,3,4,5));


その3. argumentsオブジェクトに手を加える

他にこんな手段も。

出来ないことも無いけど、argumentsオブジェクトに手を加えるのはどうなんだろうか…

function sumAllValues() {

var result = 0;

// argumentsオブジェクトにforEachメソッドを追加したのち、forEachメソッドを実行する。
arguments.forEach = Array.prototype.forEach;
arguments.forEach((n) => result += n);

return result;
};

// console.logの出力結果は 15
console.log(sumAllValues(1,2,3,4,5));


その他勘違いしていたこと

forEachメソッドには第2引数として「thisArg」という値を指定できるため、これが利用できるんじゃないかと思っていましたが、今回の目的にはかないませんでした。

一瞬、下記のようなコードでも動くんじゃないかと思いましたが、これは意図した結果になりません。

function sumAllValues() {

var result = 0;

// 実質的に何も行われない。
Array.prototype.forEach((n) => result += N, arguments);

return result;
};

// console.logの出力結果は 0
console.log(sumAllValues(1,2,3,4,5));

forEachの構文は下記の通りです。

「thisArg」引数は、第1引数で指定されたコールバック関数が実行される際の「this」を指定します。今回、「this」の参照先を変更したかったのはforEachメソッドそのものですので、目的の達成にかないません。

Array.prototype.forEach() - JavaScript | MDN

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach


SyntaxSection

arr.forEach(callback[, thisArg]);

ParametersSection

callback

Function to execute for each element, taking three arguments:

currentValue

The value of the current element being processed in the array.
indexOptional
The index of the current element being processed in the array.
arrayOptional
The array that forEach() is being applied to.

thisArg Optional

Value to use as this (i.e the reference Object) when executing callback.



おわりに

昨今JavaScript界隈では、ES2015(ES6)でかなり構文が整理され、使いやすくなりました。また、これを支える開発環境・トランスパイラも格段に進化し、便利になり、今ではWebpack+Babelを利用するのがほぼ当たり前の状況となりました。

新たにコーディングする際には、これまでのようなある種の黒魔術的なコーディングをする必要はなくなりましたが、依然として古いコードはたくさん残っています。

一見すると理解できないコードがたくさん存在するため、これらを読むためにはやはりJavaScriptの詳しい仕様・挙動を理解しておく必要があると思います。