JavaScript の Object.assign について理解する

12885 ワード

はじめに

Object.assignはオブジェクトをコピーするための関数です。今回は ECMAScript の仕様を通じてObject.assignの理解を深めてみましたので、備忘録として残しておきます。

使い方

最初にObject.assignの使い方に触れます。

Object.assignの構文はObject.assign(target, ...sources)という形式になっています。引数はtargetというコピー先のオブジェクトとsourcesという複数のコピー元のオブジェクトの2種類があり、戻り値としてはコピー先のオブジェクトが返却されます。

例えば、以下のようなnamelocationを持ったオブジェクトに対して、Object.assignを使うと、コピー先のオブジェクトであるtarget{ name: 'tommykw', location: 'kobe' }に更新されていることがわかります。

const target = {
  name: 'tommykw',
  location: 'tokyo',
};

const source = {
  name: 'tommykw',
  location: 'kobe',
};

Object.assign(target, source);

console.log(target); // { name: 'tommykw', location: 'kobe' }
console.log(source); // { name: 'tommykw', location: 'kobe' }

仕様を確認する

簡単に利用方法について確認したので、ここからは仕様を通じてさらに理解を深めます。

ECMAScriptの仕様は以下の通りで、Object.assignに関しては大きく4つのステップに分解できます。

1. Let to be ? ToObject(target).
2. If only one argument was passed, return to.
3. For each element nextSource of sources, do
  1. If nextSource is neither undefined nor null, then
    1. Let from be ! ToObject(nextSource).
    2. Let keys be ? from.[[OwnPropertyKeys]]().
    3. For each element nextKey of keys, do
      1. Let desc be ? from.[[GetOwnProperty]](nextKey).
      2. If desc is not undefined and desc.[[Enumerable]] is true, then
        1. Let propValue be ? Get(from, nextKey).
        2. Perform ? Set(to, nextKey, propValue, true).
4. Return to.

順番に見ていきます。

1. Let to be ? ToObject(target).

Object.assign(target, ..sources)targetをオブジェクトに変換します。ToObject()の仕様は以下の通りで、targetがオブジェクトの場合はそのまま返却され、プリミティブであればボックス化されて返却されます。


ECMAScript® 2023 Language Specification. Table 16: ToObject Conversions.

さらに ? ToObject(target) に注目してみると、ToObject(target) の前に?がありますが、これは何を意味するのでしょうか。? は例外が発生する可能性を示しています。この場合targetNullUndefinedの場合は、TypeError の例外が発生します。

2. If only one argument was passed, return to.

Object.assign(target)のように引数が1つの場合は変数toを返却します。

実際に引数を1つだけ指定して戻り値を確認すると、コピー先のオブジェクトがそのまま返却されていることがわかります。

const target = {
  name: 'tommykw',
};

const to = Object.assign(target);

console.log(to); // { name: 'tommykw' }

3. For each element nextSource of sources, do

ステップ3は、ぞれぞれのコピー元のオブジェクトをコピーしていきます。ここでは、オブジェクト元である各要素をnextSourceとして展開していきます。

3-1. If nextSource is neither undefined nor null, then

nextSourceUndefinedNullではない場合に 3-1-1 へ進みます。

3-1-1. Let from be ! ToObject(nextSource).

nextSourceをオブジェクトに変換して、fromに内容を格納します。

先ほどがありましたが、! は例外が発生しないことを意味します。3-1 でnextSourceUndefinedNullではないことをチェックしているため、ToObject(nextSource)で例外が発生しないことがわかります。

3-1-2. Let keys be ? from.[[OwnPropertyKeys]]().

[[OwnPropertyKeys]]とは、オブジェクトからプロパティキーをリストとして返します。つまりfromから独自のプロパティキーをkeysに格納します。


ECMAScript® 2023 Language Specification. Table 4: Essential Internal Methods

3-1-3. For each element nextKey of keys, do

プロパティキーのリストの各要素であるnextKeyとして展開していきます。

3-1-3-1. Let desc be ? from.[[GetOwnProperty]](nextKey).

fromから独自のプロパティを取得してnextKeyに対応した値を取得してdescに格納します。[[GetOwnProperty]]とはオブジェクトの独自プロパティを取得します。


ECMAScript® 2023 Language Specification. Table 4: Essential Internal Methods

3-1-3-2. If desc is not undefined and desc.[[Enumerable]] is true, then

[[Enumerable]]とは、プロパティが列挙可能かどうかを示します。ここでは、desc[[Enumerable]]trueかつ、descUndefinedでない場合に 3-1-3-2-1 へ進みます。


ECMAScript® 2023 Language Specification. Table 3: Attributes of an Object property

プロパティの列挙可能とは、enumerableで表現できます。例えば、以下のようにObject.defineProperty を使って明示的にenumerableを定義すると、enumerable: falseの場合は空のオブジェクトが表示されますが、enumerable: trueの場合は、{ name: "tommykw" }が表示されることがわかります。

const target = {
    name: ''
};

Object.defineProperty(target, 'name', {
    enumerable: false,
    value: 'tommykw',
});

console.log(target); // { }

Object.defineProperty(target, 'name', {
    enumerable: true,
    value: 'tommykw',
});

console.log(target); // { name: "tommykw" }

3-1-3-2-1. Let propValue be ? Get(from, nextKey).

Get()とは、オブジェクトの特定のプロパティの値を取得できます。つまり、fromオブジェクトのnextKeyの値を取得してpropValueに格納します。

3-1-3-2-2. Perform ? Set(to, nextKey, propValue, true).

Set()とは、オブジェクトの特定のキーに値を設定できます。つまり、toオブジェクトの nextKeypropValueの値を設定します。ちなみにSet() の4つ目の引数は例外発生の要否を判定しており、trueの場合は TypeError 例外が発生します。

4. Return to.

ステップ3にて各要素であるnextSourceのコピーを繰り返した上、最後にtoを返却します。

終わりに

ECMAScript の仕様を通じてObject.assignについて理解を深めました。仕様を確認してみると思った以上に内部的にはいろんな処理があるんだなと思いました。引き続き ECMAScript の仕様について触れていきたいと思います。

参考資料