JavaScriptの浅い比較・浅い(深い)コピーの挙動まとめ

JavaScript

プログラミング言語において『浅い比較(shallow equal)』や『浅いコピー(shallow copy)』といった言葉をよく聞きます。
今回はJavaScriptにおける、比較やコピーで表現されることのある『浅い(深い)』について説明をします。

オブジェクトの『参照先』と『プロパティのスカラ値』について

データ型にはプリミティブ型とオブジェクト型があります。
プリミティブ型とは整数や理論値、文字列といったスカラ値を持つものです。オブジェクト型とはObjectやArrayといったものです。
『浅い』という概念はオブジェクトを操作する際に考える必要が出てきます。

まずは『浅い』という概念を理解する上で重要になるオブジェクトの『参照先』と『プロパティのスカラ値』について説明します。

オブジェクトの変更がお互いに影響を与えるとき、オブジェクトの参照先が一致しているといえます。

たとえば以下のa, bは参照先が一致しているオブジェクトです。
オブジェクトの参照先が一致しているか評価する場合は===(厳密な比較)を利用します。

const a = { value: "hoge" }
const b = a

a === b
// true

一方、参照先は異なるがプロパティのスカラ値が一致する状態とは、以下のa, bのようなオブジェクトです。
値は一致していますが参照先が違うため、===ではfalseになります。

const a = { value: "hoge" }
const b = { value: "hoge" }

a === b
// false

浅い比較(shallow equal)について

浅い比較におけるオブジェクトの評価方法は以下の通りです。

  • プロパティがプリミティブ型の場合、スカラ値が===において一致していること
  • プロパティがオブジェクト型の場合(多段ネストの場合)、オブジェクトが===において一致していること(参照先が一致していること)

以下では比較対象のオブジェクトが多段ネストの場合とそうでない場合に分けて、浅い比較の挙動について説明します。

多段ネストではないオブジェクトの浅い比較

プロパティがプリミティブ型の場合はスカラ値が一致しているかを評価します。
つまり、値が一致していれば浅い比較においてtrueです。

たとえば、先ほど===falseになったa, bの比較も浅い比較においてはtrueです。

今回は浅い比較をする際にshallowEqualを利用します。

import { shallowEqual } from 'react-redux'

const a = { value: "hoge" }
const b = { value: "hoge" }

// 浅い比較において、a, b はtrueになる
console.log(shallowEqual(a, b))
// true

// === の場合はfalseになる
console.log(a === b)
// false

多段ネストのオブジェクトの浅い比較(falseになる場合)

たとえば以下の場合、オブジェクトの見た目(スカラ値)は一致しているので浅い比較であればtrueになりそうですが結果はfalseです。

import { shallowEqual } from 'react-redux'

const a = {
  value1: "hoge",
  value2: {
    value2_1: "fuga",
    value2_2: "piyo"
  }
}

const b = {
  value1: "hoge",
  value2: {
    value2_1: "fuga",
    value2_2: "piyo"
  }
}

// 浅い比較において、多段ネストの場合は見かけの値が一致していてもfalseになる
console.log(shallowEqual(a, b))
// false

スカラ値のプロパティvalue1は、値が一致しているため浅い比較においてtrueです。

しかし、value2value2_1value2_2をプロパティに持つオブジェクトです。
プロパティがオブジェクトの場合、浅い比較ではオブジェクトの参照先が一致しているか評価します。
value2の参照先はabで異なっているため、浅い比較においてvalue2falseです。

以上の結果から、abは浅い比較においてfalseです。

多段ネストのオブジェクトの浅い比較(trueになる場合)

多段ネストのオブジェクトが浅い比較でtrueになるのは以下のような場合です。
value1のスカラ値、value2のオブジェクトの参照先が一致しているためtrueです。

import { shallowEqual } from 'react-redux'

const a = {
  value1: "hoge",
  value2: {
    value2_1: "fuga",
    value2_2: "piyo"
  }
}

const b = {
  value1: "hoge",
  value2: a.value2
}

console.log(shallowEqual(a, b))
// true

オブジェクトの浅いコピー(shallow copy)について

オブジェクトの浅いコピーの挙動は以下の通りです。

  • プロパティがプリミティブ型の場合、スカラ値を複製する
  • プロパティがオブジェクト型の場合(多段ネストの場合)、当該オブジェクトの参照先を複製する

浅いコピーで複製されたオブジェクトの値を変更しても、参照先が異なるため複製元のオブジェクトに影響を与えません。

const a = { value: "hoge" }
const b = Object.assign({}, a) // 浅いコピー

b["value"] = "hogehoge"

console.log(a)
// { value: "hoge" }

console.log(b)
// { value: "hogehoge" }

一方、複製されたオブジェクトが同じ参照先の場合(浅いコピーではない場合)、オブジェクトの変更はお互いに影響を与えます。

const a = { value: "hoge" }
const b = a // 浅いコピーではない

b["value"] = "hogehoge"

console.log(a)
// { value: "hogehoge" }

console.log(b)
// { value: "hogehoge" }

『オブジェクトを複製し、複製したオブジェクトに対して変更を加える』といった処理をする場合は=で代入するのではなく、浅いコピーを利用します。

浅いコピーの方法はObjectであれば、Object.assign({}, object)やスプレッド演算子({ ...object })などがあります。
Arrayであれば、Object.assign([], array)やスプレッッド演算子([ ...array ])のほか、Array.from()Array.prototype.slice()Array.prototype.concat()などがあります。

浅いコピーによって複製されたオブジェクトは、浅い比較においてtrueになります。

import { shallowEqual } from 'react-redux'

const a = { value: "hoge" }
const b = Object.assign({}, a)  // 浅いコピー

console.log(shallowEqual(a, b))
// true

多段ネストのオブジェクトの複製は浅いコピーではなく深いコピー(deep copy)で行う

プロパティがオブジェクト型の場合、浅いコピーではオブジェクトの参照先が複製されます。
つまり、多段ネストのオブジェクトの場合は浅いコピーにもかかわらず参照先がコピーされてしまうため、オブジェクトの変更はお互いに影響を与えます。

const a = {
  value1: "hoge",
  value2: {
    value2_1: "fuga",
    value2_2: "piyo"
  }
}

const b = Object.assign({}, a) // 浅いコピー

b["value2"]["value2_1"] = "fugafuga"

console.log(a)
// const a = {
//   value1: "hoge",
//   value2: {
//     value2_1: "fugafuga", // bの変更がaに影響を与える
//     value2_2: "piyo"
//   }
// }

console.log(b)
// const a = {
//   value1: "hoge",
//   value2: {
//     value2_1: "fugafuga",
//     value2_2: "piyo"
//   }
// }

お互いに影響を与えない多段ネストのオブジェクトを複製する場合は深いコピー(deep copy)を利用します。
深いコピーにはlodashJSON.parse(JSON.stringify(object))などの方法があります。

const a = {
  value1: "hoge",
  value2: {
    value2_1: "fuga",
    value2_2: "piyo"
  }
}

const b = JSON.parse(JSON.stringify(a)) // 深いコピー

b["value2"]["value2_1"] = "fugafuga"

console.log(a)
// const a = {
//   value1: "hoge",
//   value2: {
//     value2_1: "fuga", // bの変更はaに影響を与えない
//     value2_2: "piyo"
//   }
// }

console.log(b)
// const a = {
//   value1: "hoge",
//   value2: {
//     value2_1: "fugafuga",
//     value2_2: "piyo"
//   }
// }

なお、深いコピーで多段ネストのオブジェクトを複製した場合、オブジェクト型のプロパティは参照先が異なるため浅い比較においてfalseです。

const a = {
  value1: "hoge",
  value2: {
    value2_1: "fuga",
    value2_2: "piyo"
  }
}
const b = JSON.parse(JSON.stringify(a)) // 深いコピー

console.log(a)
// const a = {
//   value1: "hoge",
//   value2: {
//     value2_1: "fuga",
//     value2_2: "piyo"
//   }
// }

console.log(b)
// const a = {
//   value1: "hoge",
//   value2: {
//     value2_1: "fuga",
//     value2_2: "piyo"
//   }
// }

console.log(shallowEqual(a, b))
// false

まとめ

  • 浅い比較はオブジェクトのプロパティのスカラ値が一致していることを検証する
  • 多段ネストのオブジェクトを浅い比較・浅いコピーする際は注意が必要
  • お互いに影響を与えないオブジェクトを複製する場合は浅いコピー(多段ネストの場合は深いコピー)を利用する

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!