Reactを使っていると、オブジェクトを変えずに済むときは、そのまま使いまわしたくなります。ということで、「できるだけ使い回す配列操作(map
、filter
、sort
)」をやってみました。
Reactと不変性(immutability)
Reactでは、props
やstate
に入れるオブジェクトの破壊的変更は禁じ手となっていて、各種のメモ化でもインスタンスの一致で比較するなど、不変性を前提とした操作が各所に現れます。Facebook自身が、Immutable.jsという不変データ構造を提供しています。
ただ、「ライブラリを入れるには大げさすぎる」けど「配列にmap
、filter
、sort
といった操作をかける際に、何も変化しないのにインスタンスが別物になるのは鬱陶しい」みたいな場面があるかと思います。今回は、「元の配列から変化しないなら同じ配列インスタンスをそのまま返す」関数を作ってみます。
お断り
なお、一般的な配列、そして不変性を前提としますので、以下のような状況は想定しません。
-
Array
以外のオブジェクトについて -
Array.prototype
がプロトタイプ汚染されている場合 -
Object.defineProperty
で通常と違う(writable
、configurable
、enumerable
のどれかをfalse
にした、あるいはゲッターやセッターのある)プロパティを生やした場合 - コールバック関数や、そこから要素に対して行う操作が配列自身や要素を変更してしまう場合
filter
いちばん簡単なのはfilter
で、普通にfilter
をかけて、length
を比較して同じかどうかで判別すれば終わりです。なお、もとが疎な配列だった場合、抜けている要素はfilter
が勝手に飛ばしてしまいますので、length
の比較で一致せず、違う配列が必ず出来上がります。
function filterOrOriginal(arr, func, thisArg) {
const filtered = arr.filter(func, thisArg);
return filtered.length === arr.length ? arr : filtered;
}
map
map
の場合も、実装を簡単にすることを考えると、「普通にmap
を行って、要素が等しいか比較する」という方法があります。なお、要素の比較には===
ではなく、Object.is
が適切です(===
は数値型に一部イレギュラーがあります)。
function mapOrOriginal(arr, func, thisArg) {
const mapped = arr.map(func, thisArg);
const changed = arr.every((val, index) => Object.is(val, mapped[index]));
return changed ? mapped : arr;
}
sort
いちばんややこしいのがsort
です。sort
の場合、
-
sort
メソッド自体が破壊的なのでコピーが必要 - 安定ソートが保証されていないので、「結果を比較する」では同じ配列でいい場合を見逃すことがある
というような事情があるので、ソートが必要かを自力で判定する必要があります。なお、.sort
で比較する条件もけっこう複雑です。
// sort()に関数を指定しなかった場合の比較関数
const defaultCompare = ( x, y ) => {
if( x === undefined && y === undefined )
return 0;
if( x === undefined )
return 1;
if( y === undefined )
return -1;
const xString = ''.concat(x);
const yString = ''.concat(y);
if( xString < yString )
return -1;
if( xString > yString )
return 1;
return 0;
};
const hasOwn = Object.prototype.hasOwnProperty;
function sortOrOriginal(arr, compare = defaultCompare) {
// 1個以下の場合は、ソートしても変化しようがない
if(arr.length <= 1) return arr;
// ソートしなくて大丈夫かをチェック
let needSort = false;
for(let i = arr.length - 1; i > 0 && !needSort; --i) {
const aUndef = arr[i-1] === undefined, bUndef = arr[i] === undefined;
// 値抜けやundefinedは、比較関数にかかわらず場所が固定
if(aUndef || bUndef) {
const aEmpty = aUndef && !hasOwn.call(arr, i - 1);
const bEmpty = bUndef && !hasOwn.call(arr, i);
// まずは抜けているものを判定
if(aEmpty) {
needSort = !bEmpty;
continue;
}
if(bEmpty) continue;
// 値があるけどundefined
if(aUndef) {
needSort = !bUndef;
}
continue;
}
if(compare(arr[i-1], arr[i]) > 0) needSort = true;
}
if(!needSort) return arr;
return arr.concat().sort(compare);
}