オブジェクトリテラル内のスプレッド構文は、ES2018で追加されたたいへん便利な構文です。特に、{ ...obj }
という形のコードでオブジェクトをコピーするのはJavaScriptプログラミングでは極めて頻出です。
スプレッド構文が無かった時代はObject.assign({}, obj)
として同様のことを達成していた方も多いと思われます。Object.assign
はES2015から使用可能でした。
では、この2種類の方法は同じでしょうか。タイトルにもある通り、もちろん違います。今回は、この違いに触れている日本語資料がMDN日本語版で一瞬触れているくらいしか無かったので記事にまとめました。
結論
最初に結論を述べると、Object.prototype
が汚染されていた場合にのみ違いが発生します。特に、Object.prototype
にsetterを持つプロパティ名が存在し、そのプロパティ名でコピーしようとした場合に違いが現れます。
例
Object.defineProperty(Object.prototype, "foo", {
enumerable: true,
get() { return 100; },
set(n) { console.log(`foo is set to ${n}`); }
});
console.log({}.foo); // 100
const sourceObj = { foo: 999 };
const obj1 = Object.assign({}, sourceObj); // foo is set to 999 と表示される
const obj2 = { ...sourceObj }; // 何も表示されない
console.log(obj1.foo, obj2.foo); // 100 999
obj1
とobj2
を2つの方法で作ったあと、それぞれのfoo
プロパティの値を調べると異なる値となっています。
解説
Object.assign
とスプレッド構文は非常に似た動作をしますが、その違いを一言でまとめるとこうです。すなわち、Object.assign
は代入によってプロパティをコピーする一方で、スプレッド構文はプロパティ自体の作成によってプロパティをコピーします。
すなわち、sourceObj
が{ foo: 999 }
であるという前提で、const obj1 = Object.assign({}, sourceObj)
の挙動は以下とおおよそ同様です。
const obj1 = {};
obj1.foo = sourceObj.foo;
一方、const obj2 = { ...sourceObj };
の挙動は以下とおおよそ同様です。
const obj2 = {};
Object.defineProperty(obj2, "foo", {
configurable: true,
enumerable: true,
writable: true,
value: sourceObj.foo
});
前者はobj1.foo
に対する代入ですから、obj1.foo
がセッタを持っていた場合はそれが呼び出されることになります。一方、後者はObject.defineProperty
によるプロパティの作成なので、セッタが呼び出されません。今回は{}
で新規オブジェクトを作ってそれを対象としているので、その違いを引き出すためにはObject.prototype
を汚染する必要がありました。
仕様書を実際に見てみると、Object.assign
と...obj
は非常に似た処理をしていることが分かります。
まずObject.assign
の定義を仕様書から画像で引用します。
次に、...obj
の処理の本体部分を使用書から画像で引用します。なお、この場合excludedItemsは空リストになります。
この2つの仕様を注意深く比べると、本質的な違いは下から2行目だけであることが見て取れます。Object.assign
はここでSetを使っているのに対し、スプレッド構文ではCreateDataPropertyOrThrowを使っています。深入りするのはやめておきますが、前者がプロパティへの代入に相当する操作である一方、後者はObject.defineProperty
に相当する操作です。
まとめ
Object.prototype
が汚染されていたときのことを考えながらコードを書きましょう!Object.prototype
を汚染するのはやめましょう。