Bàn về tính bất biến trong JavaScript

Bàn về tính bất biến trong JavaScript

Vấn đề

Biến là một thành phần không thể thiếu trong hầu hết ngôn ngữ lập trình. Khi nhắc đến biến, chúng ta thường liên tưởng ngay đến một cú pháp bao gồm từ khóa (keyword), tên biến, kiểu dữ liệu bao gồm cả giá trị ban đầu của nó. Ví dụ một biến trong JavaScript được khai báo như sau:

let name = "2coffee.dev";

Biến - đúng như cái tên của nó, giá trị của biến có thể thay đổi thông qua một phép gán. Việc thay đổi giá trị của biến giúp cho người lập trình tái sử dụng lại được tên biến, tiết kiệm bộ nhớ và tăng tính linh hoạt trong lập trình. Tưởng tượng biến name ở trong ví dụ trên, khi đến dòng x, giá trị cũ không còn tác dụng nữa, mà cần phải được thay thế thành "https://2coffee.dev" trước khi trả dữ liệu về cho người dùng chẳng hạn. Nói tóm lại, thay đổi giá trị của một biến là một điều hết sức bình thường.

Nhưng mọi chuyện sẽ dần trở nên rắc rối khi trong mã sử dụng nhiều biến hơn. Việc tạo ra nhiều biến thường sẽ tỉ lệ thuận với độ phức tạp của logic xử lý vấn đề. Tôi từng viết ra một hàm mà trong đó sử dụng đến hàng chục biến khác nhau. Hãy thử tưởng tượng nếu tất cả các biến đó được thay đổi liên tục thì dòng chảy dữ liệu của bạn lúc này sẽ như thế nào?

Đơn giản! chỉ cần tuân theo quy tắc khai báo dữ liệu không thay đổi bằng từ khóa const, khi đó biến sẽ không bao giờ bị gán lại giá trị nữa, const hay còn được biến đến là khai báo hằng số, mà đã là hằng số thì không bị thay đổi. Thông thường, gần như khi tạo ra một biến, theo thói quen tôi sẽ gõ const vì lợi ích mà nó mang lại là: hạn chế việc thay đổi dữ liệu của biến.

Nhưng trong thế giới của JavaScript, không gì là không thể xảy ra. Khi sử dụng const cho các biến chứa giá trị là đối tượng như Object, Arrayconst ngăn được việc gán giá trị, nhưng lại không thể ngăn được dữ liệu bên trong đối tượng thay đổi.

const website = {
  name: "2coffee.dev",  
};

website.name = "https://2coffee.dev";
console.log(website.name); // https://2coffee.dev

Khi đó, dữ liệu lại tha hồ "nhảy múa" trong luồng xử lý logic của bạn. Nhiều người nghĩ: "có gì nghiêm trọng ở đây đâu, dữ liệu thay đổi cũng là chuyện bình thường mà…" thì tôi chắc chắn rằng họ chưa bao giờ debug và maintain một hàm có hàng chục loại biến. Dữ liệu thay đổi, đồng nghĩa với việc bạn phải quan sát chúng mọi lúc mọi nơi, khi nhìn vào mã mà không thể đoán được tại dòng x, vị trí y, biến data này đang nắm giữ dữ liệu gì, thì khả năng cao sau này bảo trì sẽ vô cùng khó khăn. Nếu bạn là tác giả của đoạn mã đó thì còn có thể an tâm mà sửa, còn nếu như là người khác, họ có thể vô tình tạo ra một số lỗi khác khi đang cố gắng thay đổi "dòng chảy dữ liệu" mà bạn tạo ra trước đó.

Vậy bài học rút ra là gì?

Tính bất biến

Tính bất biến trong lập trình JavaScript đề cập đến khả năng của biến và cấu trúc dữ liệu không thay đổi sau khi chúng đã được tạo. Điều này có nghĩa là giá trị của chúng không thể thay đổi sau khi đã được xác định. Tính bất biến mang lại ổn định và dễ dàng theo dõi trong mã nguồn, giúp ngăn chặn các thay đổi không mong muốn và tạo ra một cơ sở cho kiểm thử và bảo trì dễ dàng.

Trong JavaScript, chúng ta thường sử dụng từ khóa const để tạo tính bất biến trên các kiểu dữ liệu nguyên thủy - Primitive, Object.freeze cho dữ liệu dạng đối tượng.

const website = {
  name: "2coffee.dev",  
};

Object.freeze(website);

website.name = "https://2coffee.dev";
console.log(website.name); // 2coffee.dev

Ở trong ví dụ trên, sau khi sử dụng Object.freeze(website), ngay lập tức đối tượng website bị "đóng băng" và dữ liệu trong các thuộc tính của nó sẽ không thể nào thay đổi được nữa, dù cho chúng ta có cố gắng gán lại bao nhiêu lần. Tuyệt vời, nếu vậy thì chẳng phải const cho nguyên thủy, Object.freeze cho đối tượng là mọi vấn đề về tính bất biến đã được giải quyết rồi sao?

Nhưng rất tiếc, Object.freeze chỉ "đóng băng" nông (shallow) được thôi, nếu trong website có thuộc tính có kiểu dữ liệu là object thì nó hoàn toàn có thể thay đổi.

const website = {
  name: "2coffee.dev",  
  props: {
    color: "black",  
  }
};

Object.freeze(website);

website.props.color = "white";
console.log(website.props.color); // white

Để giải quyết vấn đề này, chúng ta phải tự triển khai một hàm có khả năng "đóng băng" sâu (deep) một đối tượng, tức là phải đóng băng được thuộc tính của thuộc tính của thuộc tính… của đối tượng.

function deepFreeze(object) {
  // Retrieve the property names defined on object
  const propNames = Reflect.ownKeys(object);

  // Freeze properties before freezing self
  for (const name of propNames) {
    const value = object[name];

    if ((value && typeof value === "object") || typeof value === "function") {
      deepFreeze(value);
    }
  }

  return Object.freeze(object);
}

const website = {
  name: "2coffee.dev",  
  props: {
    color: "black",  
  }
};

deepFreeze(website);

website.props.color = "white";
console.log(website.props.color); // black

Ưu và nhược điểm của tính bất biến

Khi dữ liệu không bị thay đổi, chúng ta có thể quan sát được dữ liệu ở mọi lúc mọi nơi, sử dụng lại chúng mà không lo lắng về việc bị thay đổi ở một nơi nào đó. Trước kia, tôi thường cố gắng thay đổi dữ liệu bên trong một mảng hoặc một đối tượng, sau đó đối tượng lại được dùng ở một nơi khác và gây ra vấn đề sai lệch dữ liệu, dẫn đến logic xử lý bị hỏng hoàn toàn.

Giảm thiểu lỗi trong quá trình phát triển, một trong số đó có thể kể đến như Đôi điều về Object Reference trong JavaScript. Nhiều lúc quên thật phiền toái!. Khi vô tình thay đổi dữ liệu tham chiếu, kéo theo là bị thay đổi toàn bộ ở những nơi sử dụng dữ liệu đó.

Dễ dàng trong quá trình kiểm thử, tăng cường bảo mật do dữ liệu không được thay đổi từ một nơi nào đó trong mã.

Tuy vậy, tính bất biến cũng mang lại nhiều vấn đề, nổi bật nhất chính là chi phí cho bộ nhớ, vì cứ mỗi lần thay đổi dữ liệu thì cần tạo ra một đối tượng "ôm" hết dữ liệu mới. Bộ nhớ tăng, kéo theo là cả hiệu năng bị ảnh hưởng, tăng sự phức tạp, nhiều mã cần phải viết ra hơn cho một chức năng đơn giản.

Lời khuyên

JavaScript là một ngôn ngữ linh hoạt, không bị ràng buộc bởi các kiểu dữ liệu và rất "phóng khoáng" trong cách viết. Sẽ có những dự án, bạn thấy họ áp dụng tính bất biến một cách nghiêm ngặt và ngược lại, tính bất biến lại chỉ nên xuất hiện ở nơi cần thiết.

Theo kinh nghiệm cá nhân, tôi luôn sử dụng const trong khai báo tất cả các biến, trong trường hợp biến đó cần được gán lại thì xem xét sang một cách viết khác để hạn chế việc thay đổi dữ liệu của biến.

Khi xử lý dữ liệu là Array hoặc Object, hạn chế việc thay đổi trực tiếp dữ liệu bên trong nó, nếu dữ liệu cần thay đổi quá nhiều so với hiện tại, thì tốt nhất là tạo ra một biến với bộ dữ liệu mới. Hãy cố gắng sao chép sâu (deep copy) để tránh các vấn đề "object reference" sau này, clone | npm có thể là một thư viện hữu ích cho bạn.

Nếu cần nghiêm ngặt hơn, hãy xem xét đến việc sử dụng thêm Object.freeze hoặc các thư viện hỗ trợ tạo các đối tượng bất biến như là Immutable.js. Khi đó mọi người sẽ làm việc trên cùng một bộ quy tắc chung, tránh "đột biến" dữ liệu ngoài tầm kiểm soát.

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (0)