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
  • Cũng giống như 12 Days of OpenAI - một chuỗi sự kiện diễn ra trong 12 ngày liên tiếp của OpenAI, mỗi ngày họ sẽ giới thiệu một công cụ "đột phá", và cứ như thế.

    DeepSeek đã bắt "trend" ngay sau đó với chuỗi 202502 Open-Source Week diễn ra ngay trong tuần sau. Mỗi ngày họ sẽ công bố một công cụ mã nguồn mở, trái được hoàn toàn với tính "Open" của AI. Chúng ta hãy chờ xem họ mang đến những dự án thú vị nào nhé 🤓. Chắc sẽ hấp dẫn lắm đây vì ai cũng biết từ lúc ra mô hình R1, Deepseek đã chiếm trọn tin tức nổi bật trên toàn thế giới.

    » Xem thêm
  • Grok 3 beta vừa ra mắt và cho mọi người dùng thử miễn phí có giới hạn số lần trong ngày (tài khoản trả phí hình như được dùng nhiều hơn). Trong đó có 2 tính năng nổi bật là Think và DeepSearch.

    Think thì chắc ai cũng biết hoặc dùng ở một số mô hình suy luận như ở ChatGPT rồi. Còn DeepSearch thì mới hơn, gõ điều bạn muốn vào thì nó sẽ tự lên mạng tìm kiếm thông tin rồi tổng hợp lại kết quả mà nó tìm thấy được. Khá hay nhưng chắc để tham khảo hoặc muốn tổng hợp thông tin nhanh chóng thôi chứ vẫn nên tự mình tìm kiếm thông tin 😅

    » Xem thêm
  • Có 2 phần mềm tiện ích cho Mac mà mỗi khi dùng máy Mac Mini hoặc Macbook có cắm thêm màn hình rời, thêm bàn phím với chuột nữa là BetterDisplayMac Mouse Fix. Đi qua từng cái nhé!

    BetterDisplay giúp tinh chỉnh kích thước của màn hình rời để đạt độ phân giải HiDPI. Như bạn biết Mac khá kén màn hình và tuỳ chọn độ phân giải trong cài đặt mặc định rất ít ỏi, nên BetterDisplay cung cấp nhiều lựa chọn phù hợp hơn.

    Cái thứ hai là Mac Mouse Fix, nếu dùng chuột ngoài bạn sẽ thấy nó cuộn không giống với Trackpad của Macbook cho lắm. Điều kì diệu xảy ra khi cài phần mềm này vào. Nó thêm hiệu ứng "smooth" và giúp cuộn chuột y như cuộn bằng Trackpad luôn. Thật thần kỳ.

    » 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

Bí mật ngăn xếp của Blog

Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, hãy bấm vào ngay!

Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, 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...