JavaSciptの配列操作を非破壊的に行う

取り扱うメソッド

Array.prototype - JavaScript | MDNの変更メソッド

動作確認環境

  • Node.js v6.1.0

何をするか

破壊的なメソッドを非破壊的な関数に置き換えることで、参照透過性を維持する。

自作関数range()

まず説明のコード簡略化のため、配列を作成するための簡単な関数を作成する。

この関数は0から与えられた引数までの整数を配列の要素として格納し、その配列を返す。

function range(num){
  if(toString.call(num) !== '[object Number]') return null;
  if(num <= 0)  return null;
  var i = 0,
      arr = [];
  for(i = 0; i <= num; i++){
    arr.push(i);
  }
  return arr;
}

console.log(range(10));
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

slice

Array.prototype.slice() - JavaScript | MDN

Array.prototype.slice()は配列から配列の一部を取り出し、新しい配列を返すメソッドである。

このとき一部の配列操作メソッド(pop, shift等)とは異なり、元の配列には何ら影響しない。

つまり、slice()を使用して配列全てを取り出すと、元の配列のシャローコピーを作成することができる。

var arr = range(10);
console.log(arr.slice());
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

console.log(arr);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

これを利用して、破壊的メソッドを非破壊的関数に変換していく。

pop

Array.prototype.pop() - JavaScript | MDN

Array.prototype.pop()は配列の最後の要素を取り除き、その値を返すメソッドである。

var a, arr = range(10);
a = arr.pop();
console.log(a);
//10
console.log(arr);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

配列の最後の要素を取り除く動作と、配列の最後の要素を返す動作が同時に行われている。

pop()は元になる配列を変更してしまっているため、破壊的なメソッドである。これを非破壊的な関数に置き換える。

function pop(arr){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return null;
  var copy = arr.slice();
  copy.pop();
  return copy;
}

var a = range(10);
console.log(pop(a));
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
console.log(a);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

(関数名の是非はともかく)これで非破壊的な動作を行うようになる。配列の後ろの要素から何らかの処理を行いたい場合は、配列の要素を逆転させてmap()を使った方がわかりやすい。

配列の最後の要素を取得したいだけならばpop()を使う必要はないため、作成したpop関数の動作でも問題はないはず。

push

Array.prototype.push() - JavaScript | MDN

Array.prototype.push()は配列の末尾に要素を追加するメソッドである。また、戻り値として新たなる配列の要素数を返す。

var b, arr = range(10);
b = arr.push(100);
console.log(b);
//12
console.log(arr);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function push(arr, elem){
  if(toString.call(arr) !== '[object Array]') return null;
  var copy = arr.slice();
  copy.push(elem);
  return copy;
}

var b = range(10);
console.log(push(b, 100));
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100 ]
console.log(b);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

shift

Array.prototype.shift() - JavaScript | MDN

Array.prototype.shift()は配列の先頭の要素を取り除き、その値を返すメソッドである。

var c, arr = range(10);
c = arr.shift();
console.log(c);
//0
console.log(arr);
//[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function tail(arr){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return null;
  var copy = arr.slice();
  copy.shift();
  return copy;
}

var c = range(10);
console.log(tail(c));
//[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
console.log(c);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

ついでなので、配列の先頭の要素を取得する関数も作成してみる。

function head(arr){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return null;
  var copy = arr.slice();
  return copy.shift();
}

console.log(head(c));
//0
console.log(c);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

unshift

Array.prototype.unshift() - JavaScript | MDN

Array.prototype.unshift()は配列の先頭に要素を追加し、新たな配列の長さを返すメソッドである。

var d, arr = range(10);
d = arr.unshift(100);
console.log(d);
//12
console.log(arr);
//[ 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function unshift(arr, elem){
  if(toString.call(arr) !== '[object Array]') return null;
  var copy = arr.slice();
  copy.unshift(elem);
  return copy;
}

var d = range(10);
console.log(unshift(d, 100));
//[ 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
console.log(d);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

sort

Array.prototype.sort() - JavaScript | MDN

Array.prototype.sort()は配列の要素をソートするメソッドである。

var e, arr = [4, 1, 9, 6, 2, 8, 5, 9, 6, 1, 7];
e = arr.sort();
console.log(e);
//[ 1, 1, 2, 4, 5, 6, 6, 7, 8, 9, 9 ]
console.log(arr);
//[ 1, 1, 2, 4, 5, 6, 6, 7, 8, 9, 9 ]

function ascending(x, y){
  if(x < y){
    return -1;
  }else if(x > y){
    return 1;
  }else{
    return 0;
  }
}

function descending(x, y){
  if(x > y){
    return -1;
  }else if(x < y){
    return 1;
  }else{
    return 0;
  }
}

arr = [4, 1, 9, 6, 2, 8, 5, 9, 6, 1, 7];
e = arr.sort(ascending);
console.log(e);
//[ 1, 1, 2, 4, 5, 6, 6, 7, 8, 9, 9 ]
console.log(arr);
//[ 1, 1, 2, 4, 5, 6, 6, 7, 8, 9, 9 ]

arr = [4, 1, 9, 6, 2, 8, 5, 9, 6, 1, 7];
e = arr.sort(descending);
console.log(e);
//[ 9, 9, 8, 7, 6, 6, 5, 4, 2, 1, 1 ]
console.log(arr);
//[ 9, 9, 8, 7, 6, 6, 5, 4, 2, 1, 1 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function sort(arr, compare){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return null;
  var copy = arr.slice();
  return copy.sort(compare);
}

var e = [4, 1, 9, 6, 2, 8, 5, 9, 6, 1, 7];
console.log(sort(e, ascending));
//[ 1, 1, 2, 4, 5, 6, 6, 7, 8, 9, 9 ]
console.log(e);
//[ 4, 1, 9, 6, 2, 8, 5, 9, 6, 1, 7 ]

reverse

Array.prototype.reverse() - JavaScript | MDN

Array.prototype.reverse()は配列の要素を反転させ、配列を書き換えるメソッドである。

var f, arr = range(10);
f = arr.reverse();
console.log(f);
//[ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 ]
console.log(arr);
//[ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function reverse(arr){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return arr;
  var copy = arr.slice();
  return copy.reverse();
}

var f, arr = range(10);
console.log(reverse(f));
//[ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 ]
console.log(f);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

splice

Array.prototype.splice() - JavaScript | MDN

Array.prototype.splice()は配列から要素を取り除き、新しい要素を追加するメソッドである。

var g, arr = range(10);
g = arr.splice(3, 4, 11, 12, 13); //index3から4要素を取り除き、12, 13を追加する
console.log(g);
//[ 3, 4, 5, 6 ]
console.log(arr);
//[ 0, 1, 2, 11, 12, 13, 7, 8, 9, 10 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function splice(arr, index, howMany /*, elements*/){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return null;
  var copy = arr.slice(),
    arg = Array.prototype.slice.call(arguments);
  arg.shift();
  Array.prototype.splice.apply(copy, arg);
  return copy;
}

var g, arr = range(10);
console.log(splice(g, 3, 4, 11, 12, 13));
//[ 0, 1, 2, 11, 12, 13, 7, 8, 9, 10 ]
console.log(g); 
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

fill

Array.prototype.fill() - JavaScript | MDN

Array.prototype.fill()は配列中の開始インデックスから終了インデックスまで固定値に変更するメソッドである。

var h, arr = range(10);
h = arr.fill(2, 6, arr.length);
console.log(h);
//[ 0, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2 ]
console.log(arr);
//[ 0, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function fill(arr, value /*, start, end*/){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return null;
  var copy = arr.slice(),
    arg = Array.prototype.slice.call(arguments);
  arg.shift();
  Array.prototype.fill.apply(copy, arg);
  return copy;
}

var h = range(10);
console.log(fill(h, 2, 6, h.length));
//[ 0, 1, 2, 3, 4, 5, 2, 2, 2, 2, 2 ]
console.log(h);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

copyWithin

Array.prototype.copyWithin() - JavaScript | MDN

Array.prototype.copyWithin()はstartからendまでの値をtargetの位置から始まるyousoにコピーするメソッドである。

var i, arr = range(10);
i = arr.copyWithin(4, 8); //arr[8]〜arr[10]をarr[4]〜arr[6]にコピーする
console.log(i);
//[ 0, 1, 2, 3, 8, 9, 10, 7, 8, 9, 10 ]
console.log(arr);
//[ 0, 1, 2, 3, 8, 9, 10, 7, 8, 9, 10 ]

破壊的なメソッドなので、これも非破壊的な関数へ置き換える。

function copyWithin(arr, target, start /*, end */){
  if(toString.call(arr) !== '[object Array]') return null;
  if(arr.length === 0) return null;
  var copy = arr.slice(),
    arg = Array.prototype.slice.call(arguments);
  arg.shift();
  Array.prototype.copyWithin.apply(copy, arg);
  return copy;
}

var i = range(10);
console.log(copyWithin(i, 4, 8));
//[ 0, 1, 2, 3, 8, 9, 10, 7, 8, 9, 10 ]
console.log(i);
//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

最後に

非破壊的な関数を作ったものの、何回も同じ関数を使い回すと処理時間が大幅に増えてしまうため、場合によっては破壊的なメソッドを使って高速に処理した方がいいかもしれない。

参照透過性を重視するなら、こういうライブラリを自作したりLodashやUnderscoreを使うといい。

また、配列のようなオブジェクトにはこのエントリで定義した関数は使えないため、Array.prototype.slice.call(arrayLike)などで配列に変換してから使う必要がある。