結構前に、JavaScriptで遊んでいたところ、以下のような場面に遭遇しました。
console.log({} === {})
出力結果は、 true
でしょうか。 false
でしょうか。
こんなの余裕で true
でしょ!そう思っていました。
false
でした。。。。
なんでだろう???
true
でしょ!!そう思った方は、是非最後までご覧ください。
1. JavaScriptの比較・データ型について
等価演算子(==)と厳密等価演算子(===)
等価演算子は、2 つのオペランドが同じ型でないならばオペランドを変換して、それから厳密な比較を行います。
厳密等価演算子は、型変換なしでオペランド同士が等しければ真を返します。
参考: 比較演算子 |MDN
重要な点は、等価演算子(==)は、暗黙的に型の変換を行った上で比較を行うという点です。
###データ型
プリミティブ型とオブジェクト型の2種類があり、
プリミティブ型には、以下7種類のデータ型があります。
Boolean
Null
Undefined
Number
BigInt
String
Symbol
プリミティブ型でないものは全てオブジェクト型になります。
配列もオブジェクト型です。
###プリミティブとは
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 参照が同じため
object
とobject2
は、プロパティとその値の組み合わせこそ同じなのですが、参照が異なるのでfalse
となります。
object
とobject3
は、参照が同じなのでtrue
となります。
以上のことから、
console.log({} === {}) // => false
となるわけです。なるほどっ!
でもやっぱり違和感を感じてしまいます。
直感的には、
{name: "aoki"}
と{name: "aoki"}
は同じなんだから、true
を返してほしい。
そう思い、npmパッケージを作ることにしました。
2. npmパッケージを作って公開する
今回、作成するnpmパッケージが満たすべき要件は、以下のように設定しました。
- 二つのオブジェクトを比較した際に、プロパティと値の組み合わせが同じなら、プロパティの順番が異なっていても、trueを返す
そこで、必要な機能をまとめました。
(1)それぞれのオブジェクトのプロパティをソートする
(2)それぞれソートされたオブジェクトをjson形式にして比較する
まず、(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;
}
今回のnpmパッケージ関しては、不安定なソートのまま実装しています。
次に、(2)です。
JSON.stringify()というメソッドが用意されているので、それを使います。
(1)と(2)を組み合わせて、関数を作ります。
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;
}
できました!!
いくつかテストをしてみます。
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についてはこちらから
以上になります!