投稿日:2021年5月22日
オブジェクトのコピーとしてObject.assign()はよく知られた方法であると思います。しかし、javascriptの細かい使用を理解していないと思わぬ落とし穴にはまってしまいます。この記事ではObject.assign()について動作を詳しく解説しています。
オブジェクトのコピーとしてObject.assign()はよく知られた方法であると思います。しかし、javascriptの細かい使用を理解していないと思わぬ落とし穴にはまってしまいます。この記事ではObject.assign()について動作を詳しく解説しています。
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とよばれ、プロパティは参照をコピーしているだけなのです。
しかし、先程の例の中で、元のオブジェクトのnameやageの変更はコピー後のプロパティに影響を与えていませんでした。これには理由があり、string型やnumber型はプリミティブ型といい値そのものを保持するからなのです。
const sampleObject = {
_username: "【marsquai】",
age: 10,
set username(_username) {
this._username = `【${_username}】`;
},
get username() {
return this._username;
},
get nextAge() {
return this.age + 1;
},
};
ではこのオブジェクトをコピーするとどうなるでしょうか?
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()のコピーにはgetter、setterも使われるのですが、ただコピーに使われるだけで、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()の第一引数の空のオブジェクトにプロパティをコピーしているだけなので当たり前といえば当たり前なのかも知れませんが…
前の節の関数を持ったオブジェクトの例があることを考えると、かなりややこしく思えます…。