Bảo vệ các thuộc tính của một đối tượng trong Javascript
Trong bài viết này chúng ta sẽ tìm hiểu về việc tạo ra một đối tượng, đối tượng mà các thuộc tính của nó sẽ không thể thay đổi.
Tại sao chúng ta lại quan tâm tới việc “khóa cứng” các thuộc tính của một đối tượng? Chúng ta đang sống trong thế giới của OOP, chúng ta biết tầm quan trọng của các lớp, các đối tượng trong lập trình và triển khai các chương trình, nhưng không giống như những ngôn ngữ khác, Javascript không định nghĩa các lớp theo kiểu truyền thống như đã thấy ở những ngôn ngữ lập trình khác.
Việc thay đổi giá trị hoặc cách thực hiện phương thức của một đối tượng có thể dẫn tới các hành vi không kiểm soát được trong ứng dụng ở một nơi khác.
Object.freeze()
Phương thức này sẽ ngăn chặn hoàn toàn việc sửa đổi một object. Khi một object được áp dụng phương thức freeze này, chúng ta không thể:
- Xóa thuộc tính của object
- Thêm mới thuộc tính cho object
- Không thể thay đổi các tính chất enumerability, configurability, hay writability của thuộc tính
- Thay đổi giá trị của thuộc tính đã tồn tại
var cat = { name: 'Tom', age: 18, mood: 'nonplussed', }; var frozenCat = Object.freeze(cat); console.log(cat === frozenCat); // true
Như chúng ta thấy phương thức Object.freeze không tạo mới một đối tượng mà chỉ áp dụng thay đổi lên đối tượng được truyền vào.
Khi chúng ta cố gắng xóa một thuộc tính của object bằng cách sử dụng cú pháp delete, chúng ta sẽ nhận được kết quả trả về là false, và thuộc tính của đối tượng không bị xóa.
var result = delete frozenCat.name; console.log(result); // false console.log(frozenCat); // { name: 'Tom', age: 18, mood: 'nonplussed' }
Tương tự khi chúng ta sử dụng đối tượng Reflect để xóa một thuộc tính của object, kết quả cũng như trên
console.log(Reflect.deleteProperty(frozenCat, 'name')); // false
Khi chúng ta cố gắng thêm mới một thuộc tính cho object, thuộc tính mới sẽ không được thêm vào object đó:
frozenCat.address = 'Hanoi, Vietnam'; console.log(frozenCat); // { name: 'Tom', age: 18, mood: 'nonplussed' }
Khi chúng ta sử dụng phương thức Object.defineProperty, chúng ta sẽ nhận lại lỗi “TypeError: Cannot define property address, object is not extensible”:
Object.defineProperty(frozenCat, 'address', { value: 'Hanoi, Vietnam' });
Object.defineProperty(frozenCat, 'address', { value: 'Hanoi, Vietnam' }); ^ TypeError: Cannot define property address, object is not extensible
Chúng ta cũng không thể thay đổi giá trị của những thuộc tính đã tồn tại:
frozenCat.name = 'Jerry'; console.log(frozenCat); // { name: 'Tom', age: 18, mood: 'nonplussed' }
Tương tự, chúng ta cũng không thể thay đổi các “thuộc tính” enumerability, configurability, writability của một thuộc tính.
Enumerable – Giá trị này của một thuộc tính trong đối tượng có được “hiển thị” khi chúng ta sử dụng vòng lặp for...in
, hay thuộc tính đó có được hiển thị trong list trả về của phương thức Object.keys
.
var obj = {}; Object.defineProperty(obj, 'newProp', { enumerable: true }); console.log(Object.getOwnPropertyDescriptors(obj)['newProp']); // { // value: undefined, // writable: false, // enumerable: true, // configurable: false // } console.log(Object.keys(obj)); // ['newProp'] var obj2 = {}; Object.defineProperty(obj2, 'newProp', { enumerable: false }); console.log(Object.getOwnPropertyDescriptors(obj2)['newProp']); // { // value: undefined, // writable: false, // enumerable: false, // configurable: false // } console.log(Object.keys(obj2)); // []
Khi chúng ta cố gắng thay đổi giá trị này bằng phương thức Object.defineProperty
đối với một object đã “đóng băng”, thì chúng ta sẽ nhận được lỗi “TypeError: Cannot redefine property: name” :
Object.defineProperty(frozenCat, 'name', { enumerable: false });
Writable – Giá trị này cho phép chúng ta có thể thay đổi giá trị của thuộc tính, sau khi đối tượng được khởi tạo. Khi áp dụng phương thức Object.freeze
cho một object thì giá trị writable của tất cả các thuộc tính sẽ là false, khi chúng ta cố gắng thay đổi giá trị writable bằng phương thức Object.defineProperty
chúng ta sẽ nhận được lỗi “TypeError: Cannot redefine property: name”:
Object.defineProperty(frozenCat, 'name', { writable: true });
Configurable – Giá trị này cho phép khả năng chúng ta cập nhật các giá trị enumerable, configurable, writable bằng cách sử dụng phương thức Object.defineProperty
. Khi một object này bị “đóng băng”, giá trị này ở tất cả các thuộc tính sẽ là false, khi chúng ta cố gặp thay đổi giá trị này, chúng ta sẽ nhận lại lỗi “TypeError: Cannot redefine property: name”:
Object.defineProperty(frozenCat, 'name', { configurable: true });
Nếu chúng ta sử dụng strict mode cho đoạn code, các hành động như cố xóa, thay đổi giá trị, thêm mới thuộc tính cho một object đã bị “đóng băng” sẽ xảy ra lỗi “TypeError”.
Chúng ta cũng có thể áp dụng phương thức Object.freeze lên một array:
var frozenArray = Object.freeze([1, 2, 3, 4, 5]); frozenArray[0] = 10; console.log(frozenArray); // [ 1, 2, 3, 4, 5 ]
Khi chúng ta cố thêm mới hoặc xóa một giá trị của mảng, chương trình sẽ trả lại lỗi “TypeError” (kể cả việc bạn có sử dụng strict mode hay không):
frozenArray.shift(); // TypeError: Cannot assign to read only property '0' of object '[object Array]' frozenArray.push(6); // TypeError: Cannot add property 5, object is not extensible
Nếu bạn dùng Typescript thì kiểu của frozenArray
trong trường hợp này sẽ là readonly number[]
.
Deep Freeze
Lưu ý: Phương thức Object.freeze
chỉ thực hiện đóng băng ở mức shallow (nông) trên một object, điều này có nghĩa: Nếu object có thuộc tính cũng là các object thì các object này sẽ không bị “đóng băng”.
var catLover = { name: "hoangdv", address: { city: 'Hanoi' } }; var frozenCatLover = Object.freeze(catLover);
Đoạn code trên ngăn chặn việc thêm mới, xóa và chỉnh sửa thuộc tính của đối tượng catLover. Nhưng bản thân thuộc tính address cũng là một object, phương thức freeze sẽ không “đóng băng” giá trị của thuộc tính address. Chúng ta vẫn có thể thay đổi address object:
frozenCatLover.address.city = 'Viet Nam'; console.log(frozenCatLover.address); // { city: 'Viet Nam' } frozenCatLover.address.street = "Pham Hung"; console.log(frozenCatLover.address); // { city: 'Viet Nam', street: 'Pham Hung' }
Để giải quyết vấn đề này, chúng ta có một đoạn code từ trang MDN:
function deepFreeze(object) { // Retrieve the property names defined on object var propNames = Object.getOwnPropertyNames(object); // Freeze properties before freezing self for (let name of propNames) { let value = object[name]; object[name] = value && typeof value === "object" ? deepFreeze(value) : value; } return Object.freeze(object); }
Để kiểm tra một object đã bị “đóng băng” hay chưa, chúng ta có phương thức Object.isFrozen
. Phương thức này sẽ chỉ trả lại kết quả là true nếu tất cả các thuộc tính của object là {Writable: false, Configurable: false} và object đó không thể Extensible (thêm mới, xóa bỏ thuộc tính).
Trường hợp 1: Dùng phương thức Object.freeze
để đóng băng object
var catLover = { name: "hoangdv", }; Object.freeze(catLover); console.log(Object.isFrozen(catLover)); // true
Trường hợp 2: Sử dụng các phương thức khác để đóng băng đối tượng
var catLover = { name: "hoangdv", }; console.log(Object.isFrozen(catLover)); // false Object.defineProperty(catLover, 'name', { writable: false, configurable: false }); console.log(Object.isFrozen(catLover)); // false vì object vẫn có thể thêm (xóa) được thuộc tính của object Object.preventExtensions(catLover); console.log(Object.isFrozen(catLover)); //true
Object.seal()
Nếu bạn chỉ muốn ngăn chặn việc thêm mới hay xóa bỏ các thuộc tính của một object, nhưng vẫn muốn thay đổi giá trị các thuộc tính đã có khi object được khởi tạo, thì phương thức Object.seal
sẽ giúp bạn làm việc đó.
Phương thức Object.seal
sẽ làm cho tất cả các thuộc tính của một đối tượng trở thành: Không thể thay đổi cài đặt (non-configurable – Giống với Object.freeze
) và không thể xóa (non-deletable – Giống với Object.freeze
), nhưng vẫn có khả năng thay đổi giá trị (writable – Khác với Object.freeze
).
var cat = { name: 'Tom', age: 18, mood: 'nonplussed', }; var sealedCat = Object.seal(cat); console.log(sealedCat === cat); // true
Chúng ta có thể thay đổi giá trị của những thuộc tính đã tồn tại của object cat
(hay sealedCat
):
sealedCat.name = 'Jerry'; console.log(sealedCat); // { name: 'Jerry', age: 18, mood: 'nonplussed' }
Nhưng chúng ta không thể thêm mới thuộc tính cho object:
sealedCat.address = 'Hanoi'; console.log(sealedCat); // { name: 'Jerry', age: 18, mood: 'nonplussed' }
Bạn cũng không thể xóa một thuộc tính của một object đã bị “đóng dấu”:
delete sealedCat.name; console.log(sealedCat); // { name: 'Jerry', age: 18, mood: 'nonplussed' }
Để kiểm tra một object đã bị “đóng dấu” hay chưa, chúng ta có thể dùng phương thức Object.isSealed
. Một object được coi là đã bị đóng dấu khi:
- Không thể mở rộng (không thể thêm mới thuộc tính cho object)
- Không thể thay đổi cấu hình cảu tất cả các thuộc tính của đối tượng (non-configurable), và xóa bỏ thuộc tính (not removable)
Điều này có nghĩa một object bị “đóng băng” bằng phương thức Object.freeze
, thì object đó sẽ trở thành object bị đóng dấu, nhưng một object được “đóng dấu” bằng phương thức Object.seal
thì không phải là một object bị “đóng băng”:
var cat = { name: 'Tom', age: 18, mood: 'nonplussed', }; console.log(Object.isFrozen(cat)); // false console.log(Object.isSealed(cat)); // false Object.freeze(cat); console.log(Object.isFrozen(cat)); // true console.log(Object.isSealed(cat)); // true
var cat = { name: 'Tom', age: 18, mood: 'nonplussed', }; console.log(Object.isFrozen(cat)); // false console.log(Object.isSealed(cat)); // false Object.seal(cat); console.log(Object.isFrozen(cat)); // false console.log(Object.isSealed(cat)); // true
Deep Seal
Tương tự như Object.freeze
, phương thức Object.seal
cũng chỉ thực hiện việc “đóng dấu” lên object ở mức “nông”. Và cũng tương tự, nếu bạn muốn xây dựng một hàm để thực hiện việc “deepSeal” thì bạn chỉ cần thay thế “freeze” bằng “seal” trong hàm deepFreeze
đã viết ở trên:
function deepSeal(object) { // Retrieve the property names defined on object var propNames = Object.getOwnPropertyNames(object); // Seal properties before freezing self for (let name of propNames) { let value = object[name]; object[name] = value && typeof value === "object" ? deepSeal(value) : value; } return Object.seal(object); }
Kết luận
Cảm ơn các bạn đã đọc bài! Tôi hy vọng các bạn thích bài viết này và học được một vài thứ mới liên quản tới Javascript.
Tham khảo: MDN Web Docs