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

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

Tin ngắn hàng ngày dành cho bạn
  • Từ lâu rồi suy nghĩ làm thế nào để tăng sự hiện diện thương hiệu, cũng như người dùng cho blog. Nghĩ đi nghĩ lại thì chỉ có cách chia sẻ lên mạng xã hội hoặc trông chờ họ tìm kiếm, cho đến khi...

    In cái áo này được cái tắc đường khỏi phải lăn tăn, càng đông càng vui vì hàng trăm con mắt nhìn thấy cơ mà 🤓

    (Có tác dụng thật nha 🤭)

    » Xem thêm
  • Một vòng của sự phát triển nhiều dự án khá là thú vị. Tóm tắt lại trong 3 bước: Thấy một cái gì đó phức tạp -> Làm cho nó đơn giản đi -> Thêm thắt tính năng cho đến khi nó phức tạp... -> Quay trở lại vòng lặp mới.

    Tại sao lại như vậy? Để mình lấy 2 ví dụ cho các bạn thấy.

    Markdown ra đời với mục tiêu tạo ra một định dạng văn bản thô "dễ viết, dễ đọc, dễ dàng chuyển thành một dạng gì đó như HTML". Vì thời đó chẳng ai đủ kiên nhẫn mà vừa ngồi viết vừa thêm định dạng cho văn bản hiển thị ở trên web như thế nào. Ấy vậy mà giờ đây người ta đang "nhồi nhét" hoặc tạo ra các biến thể dựa trên markdown để bổ sung thêm nhiều định dạng mới đến mức... chẳng nhớ nổi hết cú pháp.

    React cũng là một ví dụ. Từ thời PHP, việc khát khao tạo ra một cái gì đó tách biệt hẳn giao diện người dùng và phần xử lý logic chính của ứng dụng thành 2 phần riêng biệt cho dễ đọc, dễ viết. Kết quả là các thư viện UI/UX phát triển rất mạnh mẽ, mang lại khả năng tương tác với người dùng rất tốt, còn phần logic ứng dụng thì nằm ở một máy chủ riêng biệt. Bộ đôi Front-end, Back-end cũng từ đấy mà thịnh hành, không thể thiếu anh bồi bàn REST API. Ấy vậy mà giờ đây React trông cũng không khác biệt gì so với PHP là mấy, kéo theo là cả Vue, Svelte... lại cùng quy tất cả về một mối.

    Cơ mà không phải vòng lặp là xấu, ngược lại vòng lặp này mang tính tiến hoá nhiều hơn là "cải lùi". Nhiều khi lại tạo ra được cái hay hơi cái cũ thế là người ta lại dựa trên cái hay đó để tiếp tục lặp. Nói cách khác là chắc lọc tinh hoa từng tí một tí một á 😁

    » Xem thêm
  • Song song với các dự án chính thức thì thi thoảng mình vẫn thấy các dự án "bên lề" nhằm tối ưu hoặc cải tiến ngôn ngữ theo khía cạnh nào đó. Ví dụ nature-lang/nature là một dự án hướng tới cải tiến Go, mang lại một số thay đổi nhằm giúp cho việc sử dụng Go trở nên thân thiện hơn.

    Nhìn lại mới thấy hao hao JavaScript 😆

    » Xem thêm

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, Array... const 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.

Cao cấp
Hello

5 bài học sâu sắc

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

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.

Bình luận (0)

Nội dung bình luận...