Lập trình · 19/10/2026

Object.freeze trong JavaScript: vì sao object đã khóa vẫn có dữ liệu thay đổi?

Object.isFrozen(config) trả true nhưng một giá trị bên trong config vẫn đổi. Điều này không nhất thiết là lỗi runtime. Object.freeze khóa các thuộc tính của chính object được truyền vào; nó không tự đi qua toàn bộ đồ thị dữ liệu và khóa mọi trạng thái liên quan.

Object.freeze trong JavaScript: vì sao object đã khóa vẫn có dữ liệu thay đổi?

Object.isFrozen(config) trả true nhưng một giá trị bên trong config vẫn đổi. Điều này không nhất thiết là lỗi runtime. Object.freeze khóa các thuộc tính của chính object được truyền vào; nó không tự đi qua toàn bộ đồ thị dữ liệu và khóa mọi trạng thái liên quan.

Hiểu phạm vi của freeze giúp dùng nó để phát hiện sửa nhầm cấu hình và trạng thái. Trước khi thêm một hàm “deep freeze”, hãy xác định cấu trúc dữ liệu cần bảo vệ và ai đang giữ tham chiếu tới nó.

1. Freeze tác động lên chính object nguồn

'use strict';
const config = { retries: 3 };
const locked = Object.freeze(config);

console.log(locked === config);       // true
console.log(Object.isFrozen(config)); // true
try {
  config.retries = 5;
} catch (error) {
  console.log(error.name); // TypeError
}
console.log(config.retries); // 3

freeze không tạo bản sao. Mọi nơi đang giữ config đều nhìn thấy cùng object đã khóa. Với thuộc tính dữ liệu thông thường, không thể gán lại giá trị, thêm hoặc xóa thuộc tính sau khi khóa. Ví dụ dùng strict mode để thao tác gán sai phát sinh TypeError thay vì có thể thất bại âm thầm.

const chỉ ngăn gán lại binding, không tự khóa object. Việc khai báo const config và việc gọi Object.freeze(config) giải quyết hai phạm vi khác nhau.

2. Object con vẫn có vòng đời riêng

'use strict';
const config = Object.freeze({
  network: { timeoutMs: 1000 }
});
config.network.timeoutMs = 2000;

console.log(Object.isFrozen(config));         // true
console.log(Object.isFrozen(config.network)); // false
console.log(config.network.timeoutMs);       // 2000

Thuộc tính network không thể bị thay bằng object khác, nhưng object mà nó trỏ tới chưa bị khóa. Object.isFrozen chỉ kiểm tra đối tượng nhận vào, không chứng nhận toàn bộ dữ liệu bên dưới bất biến.

Với một cấu hình nhỏ có schema đã biết, có thể tạo mới và khóa từng tầng cần thiết. Cách này cũng tránh khóa nhầm object do bên gọi đang sử dụng.

3. Tạo cấu hình theo schema cụ thể

function createConfig(timeoutMs) {
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
    throw new RangeError('Expected a positive safe integer');
  }
  return Object.freeze({
    network: Object.freeze({ timeoutMs }),
    modes: Object.freeze(['read', 'write'])
  });
}

const config = createConfig(1500);
console.log(Object.isFrozen(config.network)); // true
console.log(Object.isFrozen(config.modes));   // true
console.log(config.network.timeoutMs);       // 1500

Ví dụ chỉ chứa các object đã biết, một mảng chuỗi và giá trị nguyên thủy. Nó không phải thuật toán khóa sâu cho dữ liệu tùy ý. Với cây lớn hoặc có vòng tham chiếu, cần thiết kế chính sách duyệt, xử lý kiểu đặc biệt và chi phí trước khi dùng đệ quy.

Freeze cũng không thay thế validation: hàm vẫn kiểm tra timeout trước khi tạo cấu hình. Giới hạn tối đa theo nghiệp vụ hoặc môi trường chạy cần bổ sung riêng nếu sản phẩm yêu cầu.

4. Khóa mảng không tự khóa từng phần tử

'use strict';
const rows = Object.freeze([{ name: 'Draft' }]);
rows[0].name = 'Ready';
try {
  rows.push({ name: 'Another' });
} catch (error) {
  console.log(error.name); // TypeError
}
console.log(rows.length);  // 1
console.log(rows[0].name); // Ready

Mảng giữ nguyên số phần tử trong ví dụ nhưng nội dung object phần tử vẫn đổi. Khi viết kiểm thử, hãy kiểm tra cả thao tác trên mảng và thao tác trên object lồng trong mảng; chỉ thử push là chưa đủ.

5. Map có trạng thái khác thuộc tính object thông thường

const counters = Object.freeze(new Map([['views', 1]]));
counters.set('views', 2);

console.log(Object.isFrozen(counters)); // true
console.log(counters.get('views'));     // 2

Khóa object Map không chặn cập nhật các entry bằng set. Đừng quảng bá một wrapper chứa Map đã freeze là dữ liệu chỉ đọc nếu vẫn đưa Map đó cho bên gọi. Có thể cung cấp thao tác đọc có kiểm soát hoặc một biểu diễn dữ liệu phù hợp với contract của ứng dụng.

Accessor cũng cần xem xét: freeze không loại bỏ setter hoặc bảo đảm getter luôn trả cùng giá trị. Với instance phức tạp, phải kiểm tra hành vi của kiểu đó thay vì suy ra từ Object.isFrozen.

6. Cập nhật tường minh và kiểm thử đúng phạm vi

  • Chọn object được phép khóa, tránh tác động ngoài ý muốn lên dữ liệu dùng chung.
  • Chạy kiểm thử gán sai trong strict mode để phát hiện lỗi sớm.
  • Kiểm tra riêng object con, phần tử mảng và các collection.
  • Khi cần thay đổi cấu hình, tạo cấu hình mới qua hàm tạo và validation thay vì mở khóa object cũ.
  • Đo chi phí nếu đóng băng nhiều tầng trong đường xử lý thường xuyên.
  • Giữ kiểm tra quyền và validation ở server; freeze trong client không tạo ranh giới bảo mật.

Các đoạn code đã chạy bằng Node.js local. Chưa kiểm thử tích hợp framework hay đo hiệu năng ứng dụng. Tài liệu đối chiếu: Object.freeze và Object.isFrozen.

Dùng freeze hiệu quả cần một phạm vi cụ thể: khóa thuộc tính nào, ở tầng nào và với kiểu dữ liệu nào. Khi phạm vi rõ ràng, nó trở thành công cụ hữu ích để phát hiện sửa nhầm dữ liệu.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.