+3

So sánh và copy Object trong javaScript

Object trong javaScript

Điều cơ bản khác nhau giữa object và các kiểu dữ liệu nguyên thủy khác là object lưu trữ dữ liệu theo kiểu tham chiếu. Nên việc so sánh 2 object trong JavaScript là theo địa chỉ


Kiểu dữ liệu tham chiếu

Khi gán một object cho một biến, thực chất là biến đó đang lưu địa chỉ trong bộ nhớ của object.

Khi ta truy cập vào một thuộc tính của object, trình thông dịch sẽ dựa theo địa chỉ đã lưu để truy xuất đúng giá trị cần lấy trong bộ nhớ. ví dụ:


// 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

Trong ví dụ trên, mình khởi tạo object p1. Tiếp theo, mình khởi tạo p2 và gán bằng p1. Sau đó, mình thay đổi giá trị thuộc tính x trong p2. Nhưng thấy kết quả là p1.xp2.x đều thay đổi.

Điều này cho thấy là p2p1 đang cùng trỏ vào một vùng nhớ giống nhau trong bộ nhớ.

1.So sánh 2 object

1.1. So sánh 2 object bằng tham chiếu

JavaScript cung cấp hai toán tử so sánh là == và ===, trong đó:

  • Toán tử === ("bằng nghiêm ngặt") trả về true khi và chỉ khi hai biến có cùng kiểu dữ liệu và cùng giá trị, ngược lại thì trả về false.
  • Toán tử == trả về true khi hai biến có cùng giá trị và có thể khác kiểu dữ liệu (JavaScript sẽ chuyển về cùng kiểu dữ liệu để so sánh), ngược lại thì trả về false.

Đối với so sánh object bằng tham chiếu: hai object được gọi là bằng nhau khi và chỉ khi chúng cùng tham chiếu đến cùng một địa chỉ bộ nhớ.

Ví dụ hai object bằng nhau theo tham chiếu:

// 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

Trong ví dụ trên, hai object x và y đang cùng tham chiếu tới một địa chỉ. Vì vậy, chúng hoàn toàn bằng nhau.

Ngược lại, hai object độc lập sẽ không bao giờ bằng nhau, mặc dù trông có vẻ giống nhau.

Ngoài hai toán tử trên, bạn có thể dùng hàm Object.is(value1, value2) để so sánh 2 object trong JavaScript bằng tham chiếu.

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. So sánh 2 object qua giá trị cách thủ công

so sánh thủ công từng giá trị ứng với từng thuộc tính của object. Và mình coi hai object bằng nhau khi chúng có cùng thuộc tính và cùng giá trị với từng thuộc tính.

Ví dụ xây dựng bài toán vẽ hình, khi đó mỗi điểm trên màn hình là một point với tọa độ (x,y). Khi đó, hai point bằng nhau khi chúng có cùng tọa độ (x,y), ví dụ:

let point1 = { x: 1, y: 2 };
let point2 = { x: 1, y: 2 };
let point3 = { x: 2, y: 3 };

Để so sánh thủ công giữa các object ta sẽ truy cập vào giá trị của 2 thuộc tính xy trong từng object rồi so sánh chúng với nhau.

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

Ở ví dụ trên tôi viết ra hàm isPointEqual để so sánh từng thuốc tính trong object.

Tuy nhiên, nếu như sô lượng thuộc tính lớn hơn vậy nên việc cần pahir tối ưu hàm viết trên. để giải quyết vấn đề trên thì tôi sẽ viết vòng lặp for...in để duyệt hết các thuộc tính.

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;
}

Ý tưởng của thuật toán trên là: duyệt tất cả các thuộc tính của một object và so sánh giá trị tương ứng trong hai object. Nếu hai giá trị khác nhau thì kết luận là hai object không bằng nhau.

Nếu hai trường hợp return false không xảy ra thì nghĩa là hai point bằng nhau, nên cuối cùng return true.

Tuy nhiên, việc so sánh này chỉ đúng khi các giá trị của thuộc tính là giá trị nguyên thủy. Nếu giá trị của thuộc tính là 1 object thì:

let point1 = { x: 1, y: 2, metadata: { type: "point" } };
let point2 = { x: 1, y: 2, metadata: { type: "point" } };

khi vòng lặp, lặp đến metadata do là kiểu object nên khi so sánh trả ra là khác nhau.

=> Như vậy, thuật toán so sánh nông chỉ đúng khi giá trị các thuộc tính trong object có kiểu dữ liệu nguyên thủy.

Để giải quyết vấn đề trên ta phải sử dụng thuật toán đệ quy để duyệt tất cả các lớp của object.

// 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

sau khi sử dụng thuật toán đệ quy để so sánh giá trị ứng với từng thuộc tính. Còn trong dự án thực tế, bạn có thể sử dụng hàm thư viện _.isEqual để giải quyết bài toán nhanh hơn, tránh mất công sinh ra lỗi sau này.

2. Copy Object trong Javascript

Tiếp theo, là copy Object trong javaScript cũng giống như so sánh object thực chất là copy địa chỉ của thuộc tính.

2.1. Copy object sử dụng vòng lặp for...in

Cách đơn giản nhất để copy object trong JavaScript là sử dụng vòng lặp for...in để duyệt tất cả các thuộc tính của object. Rồi lấy giá trị ứng với từng thuộc tính để gán cho object mới.

Ví dụ copy object bằng for...in:

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

Bạn thấy rằng, giá trị các thuộc tính x và y của p2 hoàn toàn giống p1. Nhưng khi thay đổi giá trị p2.x = 5 thì giá trị p1.x vẫn không thay đổi.

2.2. Copy nông

Ngoài cách sử dụng vòng lặp for...in như trên, bạn có thể dùng hàm tương tự là Object.assign() với cú pháp:

Object.assign(dest, [src1, src2, src3...]);

Phương thức trên sẽ copy toàn bộ các thuộc tính của các object nguồn src1, src2,...,srcN vào object đích dest. Và giá trị trả về chính là object đích dest.

Ví dụ sử dụng Object.assign:

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);

Tuy nhiên nó cũng chỉ copy trên một cấp độ. Nếu giá trị của thuộc tính trong object là 1 object thì object sẽ không độc lập với hoàn toàn với object nguồn.

ví dụ:

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); 

trong ví dụ trên, dù tôi có sử dụng hàm Object.assign() để copy point1 vào point2 nhưng khi thay đổi object ở point2 thì metadata cùng thay đổi theo.

Ngoài ra, ta cũng có thể sử dụng Cú pháp spread (...) được sử dụng để copy các thuốc tính của một object vào vào object mới.

let point3 = { ...point1 };

Tuy nhiên, có thể thấy khi trong object có 1 hay nhiều object khác thì việc copy này đang lưu dưới dạng địa chỉ của các object con.

2.3. Copy sâu

Để giải quyết vấn đề khi chỉ copy được các object con dưới địa chỉ thì ta cần sử dụng hàm JSON.stringify() để chuyển object về dạng JSON. Rồi sau đó, bạn dùng hàm JSON.parse() để tạo lại một object mới từ JSON.

// 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

Có thể thấy khi sử dụng JSON.stringify()JSON.parse thì giá trị của metadatapoint2 cos thay đổi thì ở point1, metadata vẫn được giữ nguyên. Đó chính là copy sâu.

trên thực tế, còn 1 trường hợp nữa, đó là thuộc tính trong object là 1 hàm . khi ta sử dụng JSON.stringify() thì hàm sẽ bị chuyenr về "undefined".

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

đúng vậy, vậy nên tôi đã đưa ra một cách khác để xử lý trường hợp này. Cũng giống như so sánh Object tôi cũng sử dụng đệ quy để xử lý nó.

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

Với hàm deepCopy khi trong object có chưa object con thì sẽ đê quy lại và trả về 1 object hoàn toàn độc lập. trên thực tế ta có thể sử dụng hàm _.cloneDeep(value) của lodash để xử lý nếu như gặp trường hợp này.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.