※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。


【JavaScript】Object.assign()を使ったオブジェクトコピーに注意

web開発 javascript Object.assign() Shallow Copy Deep Copy コピー プログラミング

投稿日:2021年5月22日

このエントリーをはてなブックマークに追加
オブジェクトのコピーとしてObject.assign()はよく知られた方法であると思います。しかし、javascriptの細かい使用を理解していないと思わぬ落とし穴にはまってしまいます。この記事ではObject.assign()について動作を詳しく解説しています。

はじめに

この記事について

オブジェクトのコピーとしてObject.assign()はよく知られた方法であると思います。しかし、javascriptの細かい使用を理解していないと思わぬ落とし穴にはまってしまいます。この記事ではObject.assign()について動作を詳しく解説しています。


Object.assin()を使ったオブジェクトコピー

基本

Object.assign()は第一引数のオブジェクトにその後の引数(不特定多数)のオブジェクトのプロパティをコピーする関数でよく次のようにオブジェクトのコピーで使われます。

const sampleObject = { username: "marsquai", age: 10 };
const sampleObjectClone = Object.assign({}, sampleObject); //コピー

この関数の帰り値のオブジェクトは同じプロパティを持ち、コピー元と違う参照を持ちます。

まずはプロパティを確認してみましょう。

同じプロパティ値を持つ
// 値は同じ
console.log(sampleObject);
console.log(sampleObjectClone);
出力
{ username: 'marsquai', age: 10 }
{ username: 'marsquai', age: 10 }

次に参照を確認してみましょう。

違う参照を持つ
// 参照は違う
console.log(sampleObject == sampleObjectClone);

// 値を変えてもクローンしたオブジェクトの値は変わらない
sampleObject.age = 100;
console.log(sampleObjectClone);
出力
false
{ username: 'marsquai', age: 10 }

ここまでの例ではObject.assin()はオブジェクトのコピーに最適な関数であるように見えます。

しかし…

本当にそうでしょうか?他の例で試してみましょう!

プロパティにオブジェクトを持つ場合

クローン元のオブジェクトのプロパティにオブジェクトがある場合はどうでしょうか。

プロパティにオブジェクトがある場合
const sampleObject = {
  username: "marsquai",
  age: 10,
  job: { type: "engineer", years: 4 },
};
const sampleObjectClone = Object.assign({}, sampleObject);

// オリジナルだけ値を変えてみる
sampleObject.job.years = 5;
sampleObjectClone.age = 11;
sampleObject.username = "marsquaiSuper";

console.log(sampleObject);
console.log(sampleObjectClone);

コピー元のオブジェクトsampleObjectに新しくjobというオブジェクトのプロパティを追加し、Object.assign()でコピーした後、元のオブジェクトの値をいくつか変更するコードです。
どの様な結果になるのでしょうか?

出力
{ username: 'marsquaiSuper',
  age: 10,
  job: { type: 'engineer', years: 5 } }
{ username: 'marsquai',
  age: 11,
  job: { type: 'engineer', years: 5 } }

『!?』

驚いたことに一部のプロパティだけが一緒に変更されてしまっています。


これはバグでもなんでもありません。
実はObject.assign()でのコピーはShallow Copyとよばれ、プロパティは参照をコピーしているだけなのです。
しかし、先程の例の中で、元のオブジェクトのnameageの変更はコピー後のプロパティに影響を与えていませんでした。これには理由があり、string型number型プリミティブ型といい値そのものを保持するからなのです。

getter/setter

次にget宣言set宣言を使ったgettersetterを持ったオブジェクトを作成してみます。

const sampleObject = {
  _username: "【marsquai】",
  age: 10,
  set username(_username) {
    this._username = `【${_username}】`;
  },
  get username() {
    return this._username;
  },
  get nextAge() {
    return this.age + 1;
  },
};

ではこのオブジェクトをコピーするとどうなるでしょうか?

Object.assign()でコピー
const sampleObjectClone = Object.assign({}, sampleObject);
console.log(sampleObject);
console.log(sampleObjectClone);
出力
{ _username: '【marsquai】',
  age: 10,
  username: [Getter/Setter],
  nextAge: [Getter] }

{ _username: '【marsquai】',
  age: 10,
  username: '【marsquai】',
  nextAge: 11 }

なにやら全く違う形になってしまいました。

このクローンされたオブジェクトはどのような動作をするのでしょうか?

まずはnextAgeから確認します。

// それぞれに同じageを与える
sampleObject.age = 20;
sampleObjectClone.age = 20;
console.log(sampleObject.nextAge);
console.log(sampleObjectClone.nextAge);
出力
21
11

次にusernameを確認してみます。

// それぞれに同じusernameを与える
sampleObject.username = "ogihara";
sampleObjectClone.username = "ogihara";

console.log(sampleObject);
console.log(sampleObjectClone);
出力
{ _username: '【ogihara】',
  age: 10,
  username: [Getter/Setter],
  nextAge: [Getter] }

{ _username: '【marsquai】',
  age: 10,
  username: 'ogihara',
  nextAge: 11 }

もうめちゃくちゃですね。

Object.assign()のコピーにはgettersetterも使われるのですが、ただコピーに使われるだけで、getter関数はその時点での値をコピーしただけのプロパティ、setter関数に至ってはそのまま消えてしまいます。

関数を持つオブジェクト

次に以下のようにプロパティとして関数を持つオブジェクトを考えてみましょう。

const sampleObject = {
  username: "marsquai",
  age: 10,
  greeting() {
    console.log("Hello");
  },
};

このオブジェクトはgreeting()というコンソールに挨拶を出力する関数を持っています。

コピーして中身を確認してみましょう。

const sampleObjectClone = Object.assign({}, sampleObject);

// 中身を確認
console.log(sampleObject);
console.log(sampleObjectClone);
出力
{ username: 'marsquai', age: 10, greeting: [Function: greeting] }
{ username: 'marsquai', age: 10, greeting: [Function: greeting] }

同じ関数を持ったオブジェクトを作成できました。

上で説明したように、プリミティブ型ではない関数は参照がコピーされています。

// どちらも同じ関数をさしている
console.log(sampleObjectClone.greeting === sampleObject.greeting);
出力
true

ここまでに解説したプリミティブ型への理解、参照のコピーについてちゃんと理解できていれば問題ないないようですね。

しかし次が個人的にかなりややこしいです。

クラスインスタンスのコピー

クラスインスタンスのコピーをしてみます。

まずは以下のようなクラスを作成してみます。

class User {
  constructor(username, age) {
    this.username = username;
    this.age = age;
  }
  selfIntroduce() {
    console.log(`Hi!! I'm ${this.username}, ${this.age} years old.`);
  }
}

このクラスは以下のようにパブリックのメソッドを持ちます。

const sampleUser = new User("marsquai", 10);
sampleUser.selfIntroduce();

しかしこれをObject.assign()でコピーすると…

const sampleUserClone = Object.assign({}, sampleUser);
console.log(sampleUser);
console.log(sampleUserClone);
出力
User { username: 'marsquai', age: 10 }
{ username: 'marsquai', age: 10 }

微妙に出力が変わっているようですが…、これがどういう出力かわかるでしょうか?

この出力は元のオブジェクトはUserのインスタンスで、コピー後のオブジェクトはただのオブジェクトになってしまったことをあらわしています。

ただのオブジェクトになってしまったということは…!?

元もとのクラスにあったメソッドを呼び出してみます。

sampleUserClone.selfIntroduce();
出力
sampleUserClone.selfIntroduce();
                ^

TypeError: sampleUserClone.selfIntroduce is not a function
    at Object.<anonymous> (/home/ogihara/Projects/getts/cpObject/index.js:94:17)
    at Module._compile (internal/modules/cjs/loader.js:778:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)

そう、メソッドが消えてしまったのです!!

Object.assign()の第一引数の空のオブジェクトにプロパティをコピーしているだけなので当たり前といえば当たり前なのかも知れませんが…

前の節の関数を持ったオブジェクトの例があることを考えると、かなりややこしく思えます…。

このエントリーをはてなブックマークに追加


関連記事

記事へのコメント