uhyohyo.net

JavaScript初級者から中級者になろう

十六章第十七回 ES2015以降のObject

このページの最終更新日:

最近長い記事が続いたので、今回はさらっとした記事です。とはいえ、この記事でするのはもちろんES2015の新機能の話です。

今回はObjectに加えられた新機能を解説します。Objectの機能というのは、つまりObjectの静的メソッドのことです。ES5以前でも、Object.definePropertyなどがありましたね。

実はひとつはもう紹介してしまいました。Object.setPrototypeOfですね。オブジェクトのインスタンスに紐付けられているprototypeオブジェクトをあとから変更できるというとんでもないメソッドでした。

実はすでにもうひとつ紹介していますが、覚えているでしょうか。それは、Object.getOwnPropertySymbolsでした。これはあるオブジェクトが持つプロパティのうち、名前がシンボルであるものを列挙するメソッドでした。

ES2015でObjectにくらえられた静的メソッドはあと2つです。

Object.is

Object.isは、引数を2つ受け取り、その2つが同じならばtrueを返すメソッドです。Objectにあるメソッドとはいえ、プリミティブどうしの比較にも使うことができます。

聡明な読者ならば、2つのことが頭に浮かんだと思います。ひとつは、厳密等価演算子 ===です。この演算子も、2つの値が同じかどうか判定できるものでした。もうひとつは、前回のArray#includesに出てきた値の一致性判定です。

前者(===)による値の判定は厳密ですが、NaNどうしを比較するとfalseになるという特徴を持つのでした。後者(Array#includesのときの値の判定)はその点を克服し、NaNどうしを比較するとtrueになります。

Object.isによる比較は最も厳密です。よって、後者と同様にNaNどうしを比較するとtrueになります。


console.log(Object.is(Number.NaN, Number.NaN)); // true

また、Object.isによる比較は+0と-0を区別します。つまり、+0と-0を比較すると異なる値となり、falseとなります。

しかし、+0と-0とは何でしょうか。数学においては、ゼロはゼロであり、プラスもマイナスもないはずです。ところが、JavaScriptには実は0に2種類あります。それが+0と-0です。

通常我々が0と呼んでいるものは+0にあたります。もうひとつのゼロ、すなわち-0は、滅多なことでは出現しませんが、浮動小数点数まわりの演算で出現することがあります。例えば、負の数を正のInfinityで割ったり、正の数を負のInfinityで割ったりした結果は-0となります。


console.log(-3 / Infinity); // -0

-0は普通に-0と書くことでも(すなわち、+0にマイナス演算子を付けることでも)得ることができます。-0にはいろいろ面白い性質があります。たとえば+0と-0を加算すると+0ですが、-0と-0を加算した場合は-0となります。掛け算に関しては、もちろん+0と-0をかけると-0になります。

このように、じつは2種類あったゼロですが、やはりどちらもゼロだということで、通常はこれらは等しいものとして扱われます。しかし、Object.isは非常に厳密なので、+0と-0を区別します。よって、次のように+0と-0を引数にわたすとfalseとなります。


console.log(Object.is(0, -0)); // false

この+0と-0を区別するという性質は、Array#includesによる比較とは異なります。Array#includesの場合(SameValueZero)は+0と-0は等しいとして扱われます。


var arr = [2, 3, 5, 0, 1];

console.log(arr.includes(-0)); // true

何にせよ、NaNどうしが等しいような比較は今までJavaScriptで使うことはできませんでした(自分で頑張って実装すれば不可能ではありませんが)。それがこのように簡単に利用できるのはありがたいですね。

Object.assign

ES2015で追加されたもうひとつのメソッドはObject.assignです。これは、あるオブジェクトに他のオブジェクトのプロパティを全てコピーして書き込むという、なかなかに便利なメソッドです。

言葉で説明しても分かりにくいと思うので、まず例を見てください。


var obj1 = {
  name: 'John Smith',
  age: 42,
};

var obj2 = {
  age: 28,
  job: 'Time-traveller',
};

Object.assign(obj1, obj2);

console.log(obj1);
/*
  {
    name: "John Smith",
    age: 28,
    job: "Time-traveller",
  }
*/

この例では、2つのオブジェクトobj1とobj2を作ったあと、Object.assignを用いてobj2の内容をobj1に書き込んでいます。よって、obj2のプロパティであるageとjobがobj1に書き込まれています。

この操作は結構需要がある操作なので、ES2015ではそのためのメソッドが追加されたのです。

Object.assignは見ての通り、破壊的な操作を行います。すなわち、新しいオブジェクトを作って返すのではなく、最初の引数に渡されたオブジェクトに書き込みます。ちなみに、Object.assignの返り値は最初の引数に渡されたオブジェクトそのものです。

しかし、Object.assignの典型的な利用法のひとつは、あるオブジェクトをコピーした新しいオブジェクトを作るというものです。これは次のように行うことができます。


var obj1 = {
  name: 'John Smith',
  age: 42,
};

var obj2 = Object.assign({}, obj1);

console.log(obj1 === obj2); // false
console.log(obj1, obj2);

Object.assignの第1引数に{}を渡しています。ということは、その場で作った新しいオブジェクトにobj1の内容を書き込むということです。上の説明にある通り、Object.assignの返り値は新しく作られたオブジェクトです。このようにして、obj1と同じ中身を持つ新しいオブジェクトを作ることができました。当然ながら、上のコードで確かめているように、obj1とobj2は別のオブジェクトとなります。

また、実はObject.assignにはさらに多くの引数を渡すことができます。その場合、2つ目以降の引数に渡されたオブジェクトの中身が順に最初のオブジェクトに書き込まれます。よって、例えばobj1にobj2の内容を書き込んだ新しいオブジェクトを作りたい場合は次のようにすることができます。


var obj1 = {
  name: 'John Smith',
  age: 42,
};

var obj2 = {
  age: 28,
  job: 'Time-traveller',
};

var obj3 = Object.assign({}, obj1, obj2);

console.log(obj3);
/*
  {
    name: "John Smith",
    age: 28,
    job: "Time-traveller",
  }
*/

この例では、obj3は、{}(空のオブジェクト)にまずobj1の内容を書き込み、次にobj2の内容を書き込んだ結果のオブジェクトとなります。

いくつか細かい注意をすると、まずObject.assignによって書き込まれるのは実はenumerable属性がtrueのプロパティだけです。Object.assignに対してそのようなプロパティを持つオブジェクトを渡す機会はそうそう無いと思いますが、罠になるかもしれませんので気をつけてください。

また、Object.assignはオブジェクトをコピーすることができるため便利ですが、ここで行われるのは浅いコピー (shallow copy)です。


var obj1 = {
  prop: {
    foo: 'bar',
  },
};

var obj2 = Object.assign({}, obj1);

console.log(obj1.prop === obj2.prop); // true

この例では、obj1のpropプロパティがobj2にコピーされていますが、その中身は同じオブジェクトになっています。同じということは、obj1.propをいじった影響は当然obj2.propにも現れるということです。もしかしたらこれは不思議に思えるかもしれませんが、そんなことはありません。上のコードは、次のようにしてobj2を作った場合とおおよそ同じです。


var obj1 = {
  prop: {
    foo: 'bar',
  },
};

var obj2 = {};         // まず空のオブジェクトを作る
obj2.prop = obj1.prop; // プロパティをコピー

console.log(obj1.prop === obj2.prop); // true

こうすると、obj1.propとobj2.propが同じオブジェクトなのは極めて当然ですね。

このように、Object.assignはオブジェクトの各プロパティに対してobj2.prop = obj1.prop;のような代入に相当する操作を行います。このとき代入されるプロパティがそれ自身オブジェクトだったとしてもお構いなしです。

Object.assignは「オブジェクトのコピー」に利用されることが多いと思いますが、その実態はあくまでプロパティをひとつずつ新しいオブジェクトに代入しているだけです。このようにプロパティの中身はただ代入されるだけであるようなコピーのことを浅いコピーと呼びます。Object.assignをオブジェクトのコピーに利用するときは、それが浅いコピーであることを理解していないといけません。

Object.entries, Object.values

以上でES2015で追加された4つのメソッドは終わりなのですが、続いてES2017で追加されたObjectの静的メソッドを紹介します。それはObject.entriesObject.valuesです。

この名前を見ると、どこかで見たような気がしますね。そう、この名前のメソッドは配列Mapなどにも存在していました。

一方、entries, valuesとくればもうひとつ仲間がいるはずです。そう、keysですね。ご存知の通り、Object.keysはES5の時点から存在していました。これはオブジェクトの持つ(enumerableな)プロパティ名の配列を返すメソッドです。ES2017ではこれの仲間としてvalues、entriesが追加されたわけです。

となると、entriesやvaluesがどういう動作をするのか想像がつきますね。サンプルで確かめてみましょう。


var obj1 = {
  name: 'John Smith',
  age: 28,
  job: 'Time-traveller',
};

console.log(Object.values(obj1)); // ["John Smith", 28, "Time-traveller"]

console.log(Object.entries(obj1)); // [["name", "John Smith"], ["age", 28], "job", "Time-traveller"]]

このように、Object.valuesはプロパティ名ではなく各プロパティに入っている値を列挙します。Object.entriesは[プロパティ名, 値]という2要素の配列をプロパティの分だけ列挙します。これはMapなどのメソッドと同じなので分かりやすいですね。

ただし、注意すべき点があります。ES2017で追加というだけあってとても今どきなこれらのメソッドですが、Object.valuesやObject.entriesではシンボルをキーとするプロパティは列挙できません。これは、列挙されるプロパティをObject.keysに合わせるためです。

また、Object.valuesやObject.entriesの返り値は、Object.keysと同様に配列です。配列やMapの場合とは異なりイテレータではありません。これもObject.keysの動作に合わせるためです。

要するに、Object.keysだけ仲間がいなくてかわいそうなのでentriesとvaluesを仲間に加えてあげたということです。特にentriesは便利に使える場面が結構あるのではないかと思います。

Object.getOwnPropertyDescriptors

このObject.getOwnPropertyDescriptorsメソッドは同じくES2017で追加されたもので、指定したオブジェクトの全てのプロパティのプロパティデスクリプタをまとめて返してくれるメソッドです。

実際に使ってみるとだいたい理解できると思います。


var obj1 = {
  name: 'John Smith',
  age: 28,
  job: 'Time-traveller',
};

console.log(Object.getOwnPropertyDescriptors(obj1));
/*
{
  "name": {
    "value": "John Smith",
    "writable": true,
    "enumerable": true,
    "configurable": true
  },
  "age": {
    "value": 28,
    "writable": true,
    "enumerable": true,
    "configurable": true
  },
  "job": {
    "value": "Time-traveller",
    "writable": true,
    "enumerable": true,
    "configurable": true
  }
}
*/

このように返り値はオブジェクトで、各プロパティの中身がプロパティデスクリプタになっています。これはObject.definePropertiesでそのまま使える形ですね。このメソッドはさっきとは違い、名前がシンボルのプロパティやenumerable属性がfalseのプロパティも扱ってくれます。オブジェクトのコピーに使えるかもしれません。

今回はこれで終わりです。これで皆さんは正真正銘、Objectが持つ静的メソッドを全て知ったことになります。おめでとうございます。(ES2018以降でまた増えたら話は違いますが)。