Shallow trong tiếng Anh nghĩa là nông, cạn (có dấu phẩy nhé mn :v), phân biệt với Deep nghĩa là sâu. Bài viết này sẽ giúp các bạn hiểu và phân biệt được sự khác nhau giữa deep và shallow copy trong JavaScript

JavaScript (JS) cũng giống như hầu hết các ngôn ngữ lập trình khác, đều cung cấp và cho phép hỗ trợ khái niệm deep copy và shallow copy, tuy nhiên lại khá dễ nhầm lẫn hơn so với các ngôn ngữ khác. Khái niệm copy được hiểu là tạo ra 1 bản sao trông giống với thứ được copy, sau khi tạo ra bản sao đó, chúng ta kỳ vọng “thứ copy ra” có giá trị giống bản cũ và “thứ được copy” sẽ không bị ảnh hưởng khi thay đổi giá trị bản copy.

Tuy nhiên trong ngôn ngữ lập trình thì mọi thứ không hẳn vậy, trong đó chúng ta lưu trữ giá trị trong các biến (variables). Copy nghĩa là bạn khởi tạo ra 1 biến mới với giá trị giống như cũ. Và để tối ưu cho việc quản lý bộ nhớ cũng như hiệu suất phần mềm, có 2 khái niệm copy được đưa ra là shallow copy và deep copy.

Shallow Copy

có nghĩa là sau khi copy (tạo ra biến mới) thì giá trị của nó (và cả những giá trị con (sub-values) bên trong đối với dạng array hay object) vẫn sẽ được kết nối đến biến gốc (original variables). Về cơ bản thì 2 biến (copy và original) đều vẫn đang dùng chung 1 địa chỉ bộ nhớ (memory address). Điều này đồng nghĩa với việc nếu giá trị bộ nhớ thay đổi thì sẽ ảnh hưởng đến giá trị của cả 2 biến trên. Bạn có thể thấy rõ hơn ở ví dụ dưới.

const employee = {
    name: 'Siddharth',
    age: 35
};

const copyOfEmployee = employee; // tạo 1 bản copy


/* Giá trị in ra của employee tất nhiên là không đổi */

console.log(employee, 'employee');

/*
{ name: 'Siddharth', age: 35 } employee
*/

console.log('----------Thay đổi giá trị bản copy-------');
copyOfEmployee.age = 29;

/*
Ở đây chúng ta kỳ vọng rằng biến employee (original) sẽ không thay đổi giá trị, tùy nhiên do biến copyOfEmployee 
và biến employee cùng dùng chung địa chỉ bộ nhớ (memory address) vì thế giá trị biến employee cũng thay đổi
*/

console.log(employee, 'employee');

/*
------------Thay đổi giá trị bản copy-----------
{ name: 'Siddharth', age: 29 } employee
*/

Deep Copy

không giống như shallow copy, deep copy sẽ tạo ra 1 bản copy tất cả các thuộc tính của đối tượng cũ và lưu trữ nó vào trong 1 địa chỉ bộ nhớ độc lập dành cho biến mới. Nó sẽ giúp tạo 1 copy object (cloned object) mà không phải lăn tăn gì vấn đề thay đổi ảnh hưởng giữa 2 biến copy và nguồn (copy và original). Có nhiều các để tạo 1 bản deep copy phụ thuộc vào cấu trúc của object mà bạn muốn copy, trong đó thì có 1 cách hoạt động với hầu như tất cả các loại đấy là sử dụng JSON.parse() và JSON.stringtify() như ví dụ dưới đây.

const employee = {
    name: "Siddharth",
    age: 30
}

console.log("=========Deep Copy========");

const copyOfEmployee = JSON.parse(JSON.stringify(employee));
console.log("Employee=> ", employee);
console.log("copyOfEmployee=> ", copyOfEmployee);

/*
=========Deep Copy========
Employee=>  { name: 'Siddharth', age: 30 }
copyOfEmployee=>  { name: 'Siddharth', age: 30 }
*/

console.log("---------Thay đổi biến copy---------");
copyOfEmployee.name = "Jack";
copyOfEmployee.age = 20;

/*
Ở đây biến employee sẽ không bị thay đổi
*/

console.log("Employee=> ", employee);
console.log("copyOfEmployee=> ", copyOfEmployee);

/*
---------Thay đổi biến copy---------
Employee=>  { name: 'Siddharth', age: 30 }
copyOfEmployee=>  { name: 'Jack', age: 20 }
*/

Đến đây thì các bạn có thể hiểu và phân biệt được shallow và deep copy rồi nhé. Tuy nhiên là như đã nói từ đầu bài, JS vốn không đơn giản. Để thực sự hiểu sâu hơn về copy trong JS thì chúng ta cần phải biết cách mà JS lưu trữ giá trị như thế nào. Trước tiên cần phân biệt 2 loại dữ liệu trong JS: loại dữ liệu nguyên thủy (Primitive data types) và loại dữ liệu tổng hợp (Composite data types). Cụ thể như sau:

Primitive data types

bao gồm

  • Number (const a = 1)
  • String (const a = ‘string’)
  • Boolean (const a = true)
  • undefined (const a = undefined)
  • null (const a = null)

Khi bạn tạo 1 bản copy của loại dữ liệu nguyên thủy, bạn không cần bận tâm đến shallow copy hay deep copy. Nó sẽ luôn luôn và 1 phiên bản copy thực sự (real copy), không có bất kỳ sự liên quan gì giữa biến copy và biến nguồn (original) ở đây cả.

const a = 5;
let b = a;
console.log('a =>', a);
console.log('b =>', b);

/*
a => 5
b => 5
*/

console.log("---------Sau khi thay đổi---------");

b = 10;
console.log('a =>', a);
console.log('b =>', b);
/*
---------Sau khi thay đổi---------
a => 5
b => 10
*/

Composite Data Types

bao gồm

  • Arrays ( về mặt kỹ thuật thì chúng cũng là những objects, vì thế nên việc copy xảy ra tương tự).
  • Objects

Khi bạn tạo 1 biến với loại dữ liệu tổng hợp (composite data types) thì giá trị của chúng sẽ thực sự được lưu trữ “1 lần” lúc khởi tạo, sau đó gán vào cho biến bằng cách tạo 1 reference (tham chiếu) đến giá trị đó. Như ví dụ ở đầu bài, chúng ta đã tạo ra 1 bản shallow copy của object bằng cách

const copyOfEmployee = employee;

Có 1 vài cách copy khác trong JS, chúng ta sẽ đi tìm hiểu xem cụ thể chúng hoạt động thế nào và kết quả đạt được sau khi copy nhé.

Đối với Array

  • Spread Operator (sử dụng ký hiệu ba chấm … )

Về mặt kỹ thuật thì sử dụng spread operator không tạo ra 1 bản deep copy hoàn chỉnh. Nó chỉ cung cấp deep copy nếu như array đó không phải là nested arrays (mảng lồng nhau) hoặc 2D, 3D arrays (mảng 2, 3 chiều). Nếu mảng là dạng nested arrays thì nó sẽ chỉ deep copy cho các giá trị của phần tử con trực tiếp, còn lại với các mảng con (lồng) bên trong thì sẽ là shallow copy. Bạn có thể tham khảo ví dụ dưới đây để dễ hiểu hơn.

console.log("********Nested ARRAYS********");
const c = [1, 2, [3, 4]];
const d = [...c];
console.log('c => ', c);
console.log('d => ', d);
/*
c =>  [ 1, 2, [ 3, 4 ] ]
d =>  [ 1, 2, [ 3, 4 ] ]
*/
d[0] = 0;
d[2][1] = null;
console.log("---------After modification---------");
/*
Here the nested values of c will change but the first initial value won't. 
Because using spread operator it doesn't provide deep copy with nested arrays.
*/
console.log('c => ', c);
console.log('d => ', d);
/*
---------After modification---------
c =>  [ 1, 2, [ 3, null ] ]
d =>  [ 0, 2, [ 3, null ] ]
*/
  • Array methods – Map, ForEach, Slice

Các phương thức trên cũng tương tự như Spread Operator

Map

console.log("********Nested ARRAYS********");
const c = [1, 2, [3, 4]];
const d = c.map(el => el);;
console.log('c => ', c);
console.log('d => ', d);
/*
********Nested ARRAYS********
c =>  [ 1, 2, [ 3, 4 ] ]
d =>  [ 1, 2, [ 3, 4 ] ]
*/
d[0] = 0; // => Change the values of outer array
d[2][1] = null; // Change the values of nested array
console.log("---------After modification---------");

console.log('c => ', c);
console.log('d => ', d);
/*
---------After modification---------
c =>  [ 1, 2, [ 3, null ] ]
d =>  [ 0, 2, [ 3, null ] ]
*/

Slice

console.log("********Nested ARRAYS********");
const c = [1, 2, [3, 4]];
const d = c.slice(0);

console.log('c => ', c);
console.log('d => ', d);
/*
********Nested ARRAYS********
c =>  [ 1, 2, [ 3, 4 ] ]
d =>  [ 1, 2, [ 3, 4 ] ]
*/

d[0] = 0; // => Change the values of outer array
d[2][1] = null; // Change the values of nested array
console.log("---------After modification---------");

console.log('c => ', c);
console.log('d => ', d);
/*
---------After modification---------
c =>  [ 1, 2, [ 3, null ] ]
d =>  [ 0, 2, [ 3, null ] ]
*/

ForEach

console.log("********Nested ARRAYS********");

const c = [1, 2, [3, 4]];
const d = [];
c.forEach(el => d.push(el));;

console.log('c => ', c);
console.log('d => ', d);
/*
********Nested ARRAYS********
c =>  [ 1, 2, [ 3, 4 ] ]
d =>  [ 1, 2, [ 3, 4 ] ]
*/

d[0] = 0; // => Change the values of outer array
d[2][1] = null; // Change the values of nested array
console.log("---------After modification---------");

console.log('c => ', c);
console.log('d => ', d);
/*
---------After modification---------
c =>  [ 1, 2, [ 3, null ] ]
d =>  [ 0, 2, [ 3, null ] ]
*/
  • JSON Parse và Stringtify

Phương thức này như đã nhắc ở phần trước thì 100% là deep copy nhé các bạn. Ngoài ra để sử dụng deep copy, các bạn có thể dùng thư viện khác như lodash (tham khảo link: https://lodash.com/docs/#cloneDeep)

console.log("********Nested ARRAYS********");

const c = [1, 2, [3, 4]];
const d = JSON.parse(JSON.stringify(c)); // => JSON Methods

console.log('c => ', c);
console.log('d => ', d);

/*
********Nested ARRAYS********
c =>  [ 1, 2, [ 3, 4 ] ]
d =>  [ 1, 2, [ 3, 4 ] ]
*/

d[0] = 0;
d[2][1] = null;
console.log("---------After modification---------");

console.log('c => ', c);
console.log('d => ', d);
/*
---------After modification---------
c =>  [ 1, 2, [ 3, 4 ] ]
d =>  [ 0, 2, [ 3, null ] ]
*/

Đối với Objects

  • Object.assign()

khi sử dụng phương thức assign thì bạn phải set object nguồn ít nhất ở tham số thứ 2, thông thường sẽ để object rỗng (empty) là tham số đầu tiên. Phương thức này cũng không cung cấp deep copy hoàn toàn, nó vẫn giống với spread operator.

const employee = {
    name: 'Siddharth',
    age: 35,
    salary: {
        annual: '100K',
        hourly: '$50'
    }
};

const copyOfEmployee = Object.assign({}, employee);
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
employee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
*/
console.log('------------After Modification-----------');
copyOfEmployee.name = 'Elon Musk';
copyOfEmployee.salary.annual = '120K';
/*
Here you would expect employee object wouldn't change, but copyOfEmployee 
and employee object both share same memory address
*/
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
------------After Modification-----------
employee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '120K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Elon Musk',
  age: 35,
  salary: { annual: '120K', hourly: '$50' }
}
*/
  • Object.create()

phương thức này tạo 1 object mới sử dụng object nguồn làm prototype (lưu ý là prototype chứ không phải value nhé) cho nó. Tuy nhiên nó vẫn chỉ cung cấp kiểu copy như Spread Operator thôi nhé. Xem ví dụ dưới đây.

const employee = {
    name: 'Siddharth',
    age: 30,
    salary: {
        annual: '100K',
        hourly: '$50'
    }
};

const copyOfEmployee = Object.create( employee);
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
employee =>  {
  name: 'Siddharth',
  age: 30,
  salary: { annual: '100K', hourly: '$50' }
}
copyOfEmployee =>  {}
*/
console.log('------------After Modification-----------');
copyOfEmployee.name = 'Elon Musk';
copyOfEmployee.salary.annual = '120K';
/*
Here you would expect employee object wouldn't change, but copyOfEmployee 
and employee object both share same memory address
*/
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
------------After Modification-----------
employee =>  {
  name: 'Siddharth',
  age: 30,
  salary: { annual: '120K', hourly: '$50' }
}
copyOfEmployee =>  { name: 'Elon Musk' }
*/
  • Spread Operator
const employee = {
    name: 'Siddharth',
    age: 30,
    salary: {
        annual: '100K',
        hourly: '$50'
    }
};

const copyOfEmployee = {...employee};
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
employee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
*/
console.log('------------After Modification-----------');
copyOfEmployee.name = 'Elon Musk';
copyOfEmployee.salary.annual = '120K';
/*
Here you would expect employee object wouldn't change, but copyOfEmployee 
and employee object both share same memory address
*/
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
------------After Modification-----------
employee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '120K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Elon Musk',
  age: 35,
  salary: { annual: '120K', hourly: '$50' }
}
*/

Nếu bạn biết được độ sâu của object lồng (depth of the nested objects) thì bạn có thể sử dụng spread operator cho việc deep copy. Ngược lại thì thôi đừng dùng nhé. Ví dụ dưới đây là deep copy sử dụng spread operator:

const employee = {
    name: 'Siddharth',
    age: 30,
    salary: {
        annual: '100K',
        hourly: '$50'
    }
};

const copyOfEmployee = {...employee, salary: { ...employee.salary }}; // => DEEP COPY
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
employee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
*/
console.log('------------After Modification-----------');
copyOfEmployee.name = 'Elon Musk';
copyOfEmployee.salary.annual = '120K';

console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
------------After Modification-----------
employee =>  {
  name: 'Siddharth',
  age: 30,
  salary: { annual: '100K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Elon Musk',
  age: 30,
  salary: { annual: '120K', hourly: '$50' }
}
*/
  • JSON Parse và Stringtify

Cuối cùng thì đây vẫn là cách tốt nhất để deep copy. Cũng tương tự Array thì lodash cũng cung cấp method cho việc deep copy object, bạn có thể tìm hiểu thêm. Còn dưới đây là ví dụ sử dụng JSON

const employee = {
    name: 'Siddharth',
    age: 30,
    salary: {
        annual: '100K',
        hourly: '$50'
    }
};

const copyOfEmployee = JSON.parse(JSON.stringify(employee)); // => DEEP COPY
console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
employee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Siddharth',
  age: 35,
  salary: { annual: '100K', hourly: '$50' }
}
*/
console.log('------------After Modification-----------');
copyOfEmployee.name = 'Elon Musk';
copyOfEmployee.salary.annual = '120K';

console.log('employee => ', employee);
console.log('copyOfEmployee => ', copyOfEmployee);
/*
------------After Modification-----------
employee =>  {
  name: 'Siddharth',
  age: 30,
  salary: { annual: '100K', hourly: '$50' }
}
copyOfEmployee =>  {
  name: 'Elon Musk',
  age: 30,
  salary: { annual: '120K', hourly: '$50' }
}
*/

Cảm ơn mọi người đã đọc.

Link bài viết gốc đây nhé

https://javascript.plainenglish.io/copies-of-javascript-shallow-and-deep-copy-ac7f8dcd1dd0

From Anyway with Love!