Objects in JavaScript
The main difference between object and other primitive data types is that object stores data by reference. So comparing two objects in JavaScript is by address
Reference data type
When assigning an object to a variable, the variable is actually storing the address in the object’s memory.
When we access a property of an object, the interpreter will use the stored address to retrieve the correct value from memory. For example:
1 2 3 4 5 6 7 8 9 | // vd về kiểu tham chiếu let p1 = { x: 1, y: 2 }; let p2 = p1; p2.x = 2; console.log(p2.x); // 2 console.log(p1.x); // 2 |
In the above example, I initialize the object p1 . Next, I initialize p2 and assign it with p1 . Then I change the x attribute value in p2 . But the result is that p1.x and p2.x both change.
This shows that p2 and p1 are pointing at the same location in memory.
1. Compare 2 objects
1.1. Compare 2 objects by reference
JavaScript provides two comparison operators == and ===, where:
- The === (“strict equality”) operator returns true if and only if the two variables have the same data type and the same value, false otherwise.
- The == operator returns true when two variables have the same value and may have different data types (JavaScript will return the same data type for comparison), otherwise it returns false.
For object-by-reference comparison : two objects are said to be equal if and only if they both refer to the same memory address.
Example two objects are equal by reference:
1 2 3 4 5 6 7 8 9 10 11 12 | // vd so sánh 2 object bằng kiểu tham chiếu let x = { name: "long" }; let y = x; console.log(y == x); // true console.log(y === x); // true let long1 = {name: "LongNT"}; let long2 = {name: "LongNT"}; // khởi tạo object độc lập console.log(long1 == long2); // false console.log(long1 === long2); // false |
In the above example, two objects x and y are referencing the same address. So they are completely equal.
Conversely, two independent objects will never be equal, even though they look similar.
In addition to the above two operators, you can use the Object.is (value1, value2) function to compare two JavaScript objects by reference.
1 2 3 4 5 6 7 | let long1 = {name: "LongNT"}; let longcopy = long1; let long2 = {name: "LongNT"}; console.log(Object.is(long1, longcopy)); // true console.log(Object.is(long2, long1)); // false |
1.2. Compare 2 objects by value manually
manually compare each value for each property of the object. And I consider two objects equal when they have the same properties and the same value for each property.
For example, build a drawing problem, then each point on the screen is a point with coordinates (x,y) . Then, two points are equal when they have the same coordinates (x,y) , for example:
1 2 3 4 | let point1 = { x: 1, y: 2 }; let point2 = { x: 1, y: 2 }; let point3 = { x: 2, y: 3 }; |
To manually compare objects, we will access the values of the two properties x and y in each object and then compare them with each other.
1 2 3 4 5 6 7 8 9 10 | function isPointEqual(p1, p2) { return p1.x === p2.x && p1.y === p2.y; } let point1 = { x: 1, y: 2 }; let point2 = { x: 1, y: 2 }; let point3 = { x: 2, y: 3 }; console.log(isPointEqual(point1, point2)); // true console.log(isPointEqual(point1, point3)); // false |
In the above example, I wrote the function isPointEqual to compare each drug in the object.
However, if the number of attributes is larger, it is necessary to optimize the function written above. To solve the above problem, I will write a for…in loop to go through all the properties.
1 2 3 4 5 6 7 8 9 10 11 12 | function isPointEqual(obj1, obj2) { for (let prop in obj1) { if (obj1[prop] !== obj2[prop]) return false; } for (let prop in obj2) { if (obj2[prop] !== obj1[prop]) return false; } return true; } |
The idea of the above algorithm is: iterate over all the properties of an object and compare the corresponding value in the two objects. If the two values are different then the conclusion is that the two objects are not equal.
If two cases return false does not occur, then the two points are equal, so finally return true .
However, this comparison is true only when the values of the property are primitive values. If the value of the property is an object then:
1 2 3 | let point1 = { x: 1, y: 2, metadata: { type: "point" } }; let point2 = { x: 1, y: 2, metadata: { type: "point" } }; |
when looping, iterates to metadata because it is an object type, so when comparing, the output is different.
=> Thus, the shallow comparison algorithm is only correct when the values of the properties in the object have primitive data types.
To solve the above problem we have to use a recursive algorithm to traverse all the classes of the object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // Hàm kiểm tra một giá trị là object function isObject(obj) { return obj != null && typeof obj === "object"; } // Hàm so sánh sâu function isDeepEqual(obj1, obj2) { const keys1 = Object.keys(obj1); // trả về mảng các thuộc tính của obj1 const keys2 = Object.keys(obj2); // trả về mảng các thuộc tính của obj2 // nếu số lượng keys khác nhau thì chắc chắn khác nhau if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { const val1 = obj1[key]; const val2 = obj2[key]; // kiểm tra xem hai giá trị có cùng là object hay không const areObjects = isObject(val1) && isObject(val2); // nếu cùng là object thì phải gọi đệ quy để so sánh 2 object if (areObjects && !isDeepEqual(val1, val2)) { return false; } // nếu không cùng là object thì so sánh giá trị if (!areObjects && val1 !== val2) { return false; } } return true; } let point1 = { x: 1, y: 2, metadata: { type: "point" } }; let point2 = { x: 1, y: 2, metadata: { type: "point" } }; console.log(isDeepEqual(point1, point2)); // true |
after using recursive algorithm to compare the value for each attribute.
2. Copy Object in JavaScript
Next, copying Object in JavaScript is like comparing object, actually copying the address of the property.
2.1. Copy object using for…in . loop
The simplest way to copy an object in JavaScript is to use a for…in loop to iterate over all the properties of the object. Then get the value for each property to assign to the new object.
Example copy object with for…in :
1 2 3 4 5 6 7 8 9 10 11 12 | let p1 = { x: 1, y: 2 }; let p2 = {}; for (let key in p1) { p2[key] = p1[key]; } console.log(p2.x); // 1 console.log(p2.y); // 2 p2.x = 5; console.log(p2.x); // 5 console.log(p1.x); // 1 |
You see, the values of the x and y attributes of p2 are exactly the same as p1. But when changing the value of p2.x = 5, the value of p1.x remains unchanged.
2.2. Shallow copy
In addition to using the for…in loop as above, you can use the same function as Object.assign() with the syntax:
1 2 | Object.assign(dest, [src1, src2, src3...]); |
The above method will copy all the properties of the source objects src1, src2,…,srcN to the destination object dest. And the return value is the destination object dest.
Example using Object.assign:
1 2 3 4 5 6 7 8 | let user = { name: "longnt1" }; let FE = { isFE: true }; let BE = { isBE: false }; // copy toàn bộ thuộc tính từ FE và BE vào user Object.assign(user, FE, BE); console.log(user); |
However, it also only copies on one level. If the value of the property in the object is an object, then the object will not be completely independent of the source object.
For example:
1 2 3 4 5 6 7 8 9 10 11 | let point1 = { x: 1, y: 2, metadata: { type: "point" } }; let point2 = {}; Object.assign(point2, point1); point2.metadata.type = "CHANGED"; point2.y = 5; console.log(point1.metadata.type); console.log(point2.metadata.type); |
In the above example, even though I use the Object.assign() function to copy point1 to point2 , when I change the object at point2 , the metadata changes along with it.
In addition, we can also use the spread (…) syntax which is used to copy the properties of an object into the new object.
1 2 | let point3 = { ...point1 }; |
However, it can be seen that when there is one or more other objects in the object, this copy is saving as the address of the child objects.
2.3. Deep copy
To solve the problem when we can only copy the child objects below the address, we need to use the JSON.stringify() function to convert the object to JSON format. Then, you use the JSON.parse() function to recreate a new object from the JSON.
1 2 3 4 5 6 7 8 9 10 11 12 | // chuyển object về dạng JSON let jsonPoint1 = JSON.stringify(point1); console.log(jsonPoint1); // {"x":1,"y":2,"metadata":{"type":"point"}} // parse JSON lại thành object mới let point2 = JSON.parse(jsonPoint1); console.log(point2.metadata.type); // point point2.metadata.type = "CHANGED"; console.log(point2.metadata.type); // CHANGED console.log(point1.metadata.type); // point |
It can be seen that when using JSON.stringify() and JSON.parse , the value of metadata at point2 cos changes, but at point1 , the metadata remains the same. That is deep copy.
actually, there is 1 more case, that is a property in object is a function. when we use JSON.stringify() , the function will be converted to “undefined”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | let point1 = { x: 1, y: 2, getDisplayName: function () { return "Longnt1"; }, }; // chuyển object về dạng JSON let jsonPoint1 = JSON.stringify(point1); console.log(jsonPoint1); // {"x":1,"y":2} // parse JSON lại thành object mới let point2 = JSON.parse(jsonPoint1); console.log(point2.getDisplayName); // undefined |
yes, so I came up with another way to handle this case. Just like Object comparison I also use recursion to handle it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | let point1 = { x: 1, y: 2, getDisplayName: function () { return "Longnt1"; }, }; function deepCopy(obj) { // kiểm tra xem có phải là object ko if (typeof obj !== "object" || obj === null) { return obj; } const copy = {}; // lặp qua từng thuộc tính và copy từng thuộc tính for (const key in obj) { const value = obj[key]; copy[key] = deepCopy(value); } return copy; } const clonedPoint1 = deepCopy(point1); console.log("clonedPoint1 :", clonedPoint1.getDisplayName()); //clonedPoint1 : Longnt1 |
With the deepCopy function, when there are child objects in the object, it will recursively and return a completely independent object. In fact, we can use the _.cloneDeep(value) function of lodash to handle this if this is the case.