14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScript 2Advent Calendar 2019

Day 3

【JavaScript】 {} === {} はfalseになるので、簡単なnpmパッケージを作りました。

Last updated at Posted at 2019-12-02

結構前に、JavaScriptで遊んでいたところ、以下のような場面に遭遇しました。

console.log({} === {})

出力結果は、 true でしょうか。 false でしょうか。

こんなの余裕で true でしょ!そう思っていました。

結果
false

でした。。。。

なんでだろう???

trueでしょ!!そう思った方は、是非最後までご覧ください。

1. JavaScriptの比較・データ型について

等価演算子(==)と厳密等価演算子(===)

等価演算子は、2 つのオペランドが同じ型でないならばオペランドを変換して、それから厳密な比較を行います。
厳密等価演算子は、型変換なしでオペランド同士が等しければ真を返します。
参考: 比較演算子 |MDN

重要な点は、等価演算子(==)は、暗黙的に型の変換を行った上で比較を行うという点です。

###データ型

プリミティブ型とオブジェクト型の2種類があり、
プリミティブ型には、以下7種類のデータ型があります。
Boolean
Null
Undefined
Number
BigInt
String
Symbol

プリミティブ型でないものは全てオブジェクト型になります。
配列もオブジェクト型です。

参考: JavaScript のデータ型とデータ構造

###プリミティブとは
JavaScript において、プリミティブ (primitive、プリミティブ値、プリミティブデータ型) はオブジェクトでなく、メソッドを持たないデータのことです。 すべてのプリミティブ値は、イミュータブル、つまり変更できません。変数には新しい値を再割り当てすることができますが、既存の値については、オブジェクト、配列、関数が変更できるのに対して、プリミティブ値は変更することができません。

参考: プリミティブ |MDN

つまり、プリミティブな型は、値を貸すことはできるが、定義された値に対して新しい値を代入することができません。

具体的に例で示します。

プリミティブな型の場合。


const number = 10
numberChange(number)
console.log(number) // => 10

function numberChange(number){
	number += 10000
	console.log(number) // => 10010
}

値を渡すことはできますが、定義したconst number = 10の値自体が変更されることはありません。

オブジェクト型(プリミティブ型でない)場合。


const array = [1,2,3]
arrayChange(array)
console.log(array) // => [1,2,3,10000]

function arrayChange(array){
	array.push(10000)
	console.log(array) // => [1,2,3,10000]
}

値ではなく、アドレスを渡している(参照渡し)ため、const array = [1,2,3]の値が変更されます。

1. 上記疑問を解決する

ドキュメントに答えが書かれていました。

等価演算子は、2 つのオペランドが同じ型でないならばオペランドを変換して、それから厳密な比較を行います。両方のオペランドがオブジェクトならば、 JavaScript は内部参照を比較するので、オペランドがメモリ内の同じオブジェクトを参照するときに等しくなります。

具体的に例で示します。

object  = {name: "aoki"}
object2 = {name: "aoki"}
object3 = object

console.log(object === object2) // => false 参照が異なるため  
console.log(object === object3) // => true 参照が同じため

objectobject2は、プロパティとその値の組み合わせこそ同じなのですが、参照が異なるのでfalseとなります。
objectobject3は、参照が同じなのでtrueとなります。

以上のことから、

console.log({} === {}) // => false

となるわけです。なるほどっ!

でもやっぱり違和感を感じてしまいます。
直感的には、
{name: "aoki"}{name: "aoki"}は同じなんだから、trueを返してほしい。

そう思い、npmパッケージを作ることにしました。

2. npmパッケージを作って公開する

今回、作成するnpmパッケージが満たすべき要件は、以下のように設定しました。

  • 二つのオブジェクトを比較した際に、プロパティと値の組み合わせが同じなら、プロパティの順番が異なっていても、trueを返す

そこで、必要な機能をまとめました。

(1)それぞれのオブジェクトのプロパティをソートする
(2)それぞれソートされたオブジェクトをjson形式にして比較する

まず、(1)です。以下のように実装しました。

(1)の実装

function objectSort(object) {
  var newObject = {};
  var keyArray = [];
  for (key in object) {
    keyArray.push(key);
  }
  keyArray.sort()
  for (var i = 0; i < keyArray.length; i++) {
    newObject[keyArray[i]] = object[keyArray[i]];
  }
  return newObject;
}

新しいオブジェクト・配列を用意し、引数として与えられたオブジェクトのプロパティを配列に格納します。
格納された配列をsortメソッドを使ってソートし、ソートされた配列に対して、対応する値とともに新しいオブジェクトに格納します。
格納されたオブジェクトを返します。

⚠︎追記 
JavaScriptのsortメソッドは、不安定なソートであり、必ずしも同じ序列を持つ値の順番が保証されません。
参考: Array.prototype.sort() |MDN

chromeだと安定ぽい...!

安定なソートとして実現させる場合、以下のような処理を行います。

安定
function objectSort(object) {
  var newObject = {};
  var keyArray = [];
  for (key in object) {
    keyArray.push(key);
  }
  stableSort(keyArray)
  for (var i = 0; i < keyArray.length; i++) {
    newObject[keyArray[i]] = object[keyArray[i]];
 
  console.log(newObject)
  return newObject;
}

function compare (a, b) {
  if (a === b) {
    return 0;
  } else if (a > b) {
    return 1;
  } else {
    return -1;
  }
}

function stableSort (array, fn) {
  if (fn == null) {
    fn = compare;
  }

  var i, len = array.length;

  if (len === 0) return array;

  // 値とインデックスのペアにする
  for (i = 0; i < len; i++) {
    array[i] = [array[i], i];
  }

  array.sort(function (p1, p2) {
    // ペアの0番目同士が等しくない場合はその比較結果を返す
    // 等しい場合は、インデックスを比較した結果を返す
    return fn(p1[0], p2[0]) || (p1[1] - p2[1]);
  });

  // ペアの0番目を取り出す
  for (i = 0; i < len; i++) {
    array[i] = array[i][0];
  }

  return array;
}

参考: JavaScript で安定ソート

今回のnpmパッケージ関しては、不安定なソートのまま実装しています。

次に、(2)です。
JSON.stringify()というメソッドが用意されているので、それを使います。

(1)と(2)を組み合わせて、関数を作ります。

index.js
function objectMatch(obj1,obj2){
  const obj1Sorted = objectSort(obj1)
  const obj2Sorted = objectSort(obj2)
  obj1Json = JSON.stringify(obj1Sorted)
  obj2Lson = JSON.stringify(obj2Sorted)
  if(obj1Json === obj2Lson){
    return true
  }else{
    return false
  }
}

function objectSort(object) {
  var newObject = {};
  var keyArray = [];
  for (key in object) {
    keyArray.push(key);
  }
  keyArray.sort()
  for (var i = 0; i < keyArray.length; i++) {
    newObject[keyArray[i]] = object[keyArray[i]];
  }
  return newObject;
}

できました!!
いくつかテストをしてみます。

index_test.js
const objectMatch = require('./index');

const object1 = {
  name: "aoki",gender: 1,height: 168.5
}

const object2 = {
  gender: 1,name: "aoki",height: 168.5
}

const object3 = {
  gender: 1,name: "aoki",height: 168.6
}

const object4 = {
  gender: 1,name: "aoki",profile: {hobby: {sports: ["soccer","baseball","basketball"]}}
}

const object5 = {
  gender: 1,profile: {hobby: {sports: ["soccer","baseball","basketball"]}},name: "aoki"
}

//プロパティとその値の組み合わせは同じだが、順番が異なる時、trueを返す。
test('object1 and object2 is equal', () => {
  expect(objectMatch(object1,object2)).toBe(true);
});

//プロパティに対して、その値が異なる時、falseを返す。
test('object1 and object3 is not equal', () => {
  expect(objectMatch(object1,object3)).toBe(false);
});

//プロパティに対する値が複雑でも、同じならtrueを返す。
test('object4 and object5 is equal', () => {
  expect(objectMatch(object4,object5)).toBe(true);
});
 PASS  ./index.test.js
  ✓ object1 and object2 is equal (2ms)
  ✓ object1 and object3 is not equal (1ms)
  ✓ object4 and object5 is equal (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        2.969s

通りました。

では、この関数をnpmパッケージにして公開します。
こちらの記事がとても分かりやすく、とても簡単に公開することができました!

公開したnpmパッケージについてはこちらから
githubについてはこちらから

スクリーンショット 2019-12-02 19.25.56.png

以上になります!

14
5
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?