JavaScript 核心觀念(55)- 物件屬性延伸章節:屬性的特徵 - 物件屬性不可寫入?物件擴充的修改與調整

前言

前一章節介紹了 Object.defineProperty,而這個語法主要是針對物件中的屬性做一些調整,因此這一篇將會介紹物件。

物件屬性不可寫入?物件擴充的修改與調整

如同前言,除了可以使用 Object.defineProperty 來針對物件中的屬性做操作之外,也可以針對物件本身做操作,而物件的操作主要是三個語法

  • Object.preventExtensions - 物件不可擴充
  • Object.seal - 物件封裝
  • Object.freeze - 物件凍結

接下來讓我們看一下所謂的 Object.preventExtensions 是什麼意思,首先這邊會有一個基本的程式碼:

1
2
3
4
var obj = {
myName: 'Ray',
blog: {},
};

Object.preventExtensions 的使用方式非常的簡單,假設說我希望 obj 這一整個物件不可以再次擴充的話,只需要這樣寫即可:

1
2
3
4
5
6
7
8
9
var obj = {
myName: 'Ray',
blog: {},
};

Object.preventExtensions(obj);

obj.url = 'https://hsiangfeng.github.io/';
console.log(obj); // 不會看到 url 屬性

當然這也是一個靜默錯誤:

1
2
3
4
5
6
7
8
9
10
'use strict'
var obj = {
myName: 'Ray',
blog: {},
};

Object.preventExtensions(obj);

obj.url = 'https://hsiangfeng.github.io/';
console.log(obj); // Uncaught TypeError: Cannot add property url, object is not extensible

否則正常來講你應該會看這樣子的回傳:

1
2
3
4
5
{
myName: "Ray",
blog: {},
url: "https://hsiangfeng.github.io/"
}

當然你可能會好奇「我怎麼知道這個物件能不能被擴充?」,其實可以使用 Object.isExtensible 來查看是否可以被擴充:

1
2
3
4
5
6
7
8
9
10
11
var obj = {
myName: 'Ray',
blog: {},
};

Object.preventExtensions(obj);
console.log('是否可以被擴充:', Object.isExtensible(obj)); // false

obj.url = 'https://hsiangfeng.github.io/';

console.log(obj); // 不會看到 url 屬性

但是這邊要注意一件事情,本身該物件若調整成不可被擴充之後,原有的屬性依然是可以調整的:

1
2
3
4
5
6
7
8
9
10
var obj = {
myName: 'Ray',
blog: {},
};

Object.preventExtensions(obj);

obj.blog = 'https://hsiangfeng.github.io/';

console.log(obj); // {myName: "Ray", blog: "https://hsiangfeng.github.io/"}

因此你可以理解成 Object.preventExtensions 是禁止物件去新增一個屬性,但可以調整現有的屬性或者是刪除屬性,因此 preventExtensions 並不會調整 defineProperty 底下的 writable 等,但接下來的另外兩個語法就稍微比較特別一點。

接下來的 Object.seal 就真的滿特別的,它包含物件無法被擴充之外也無法讓屬性被新增刪除,更不用說使用 defineProperty 重新調整屬性特徵,就如同它的翻譯一樣,整個物件都被封裝起來了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var obj = {
myName: 'Ray',
blog: {},
};

Object.seal(obj);

console.log('是否被封裝:', Object.isSealed(obj)); // true
console.log('查看 myName 的屬性特徵:', Object.getOwnPropertyDescriptor(obj, 'myName')); // {value: "Ray", writable: true, enumerable: true, configurable: false}

obj.myName = 'QQ123'; // 可以被修改
obj.url = 'https://hsiangfeng.github.io/'; // 可以被修改
delete obj.myName // false, 無法被刪除
obj.blog = false; // 無法被擴充

// 如果你嘗試調整屬性特徵則會直接噴錯:Uncaught TypeError: Cannot redefine property: myName
Object.defineProperty(obj, 'myName', {
configurable: true,
});

因此 Object.seal 的重點在於「它包含物件無法被擴充之外也無法讓屬性被新增刪除,更不用說使用 defineProperty 重新調整屬性特徵」,但是它可以修改屬性,因此 Object.seal 與前面 preventExtensions 是有關連的,物件會先被限制擴充再加上 seal 避免物件被刪除。

最後一個是 Object.freezefreeze 會針對當前物件給予 seal 再去限制不可調整值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {
myName: 'Ray',
blog: {},
};

Object.freeze(obj);

console.log('是否被封裝:', Object.isSealed(obj)); // true
console.log('是否可以被擴充:', Object.isExtensible(obj)); // false
console.log('是否被凍結:', Object.isFrozen(obj)); // true
console.log('查看 myName 的屬性特徵:', Object.getOwnPropertyDescriptor(obj, 'myName')); // {value: "Ray", writable: false, enumerable: true, configurable: false}

obj.myName = 'QQ123'; // 無法被修改
delete obj.myName; // 無法刪除
obj.url = 'https://hsiangfeng.github.io/'; // 無法新增

console.log(obj); // {myName: "Ray", blog: {…}}

就算你使用 Object.defineProperty 調整屬性特徵也一樣是會出現錯誤的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {
myName: 'Ray',
blog: {},
};

Object.freeze(obj);

console.log('是否被封裝:', Object.isSealed(obj)); // true
console.log('是否可以被擴充:', Object.isExtensible(obj)); // false
console.log('是否被凍結:', Object.isFrozen(obj)); // true
console.log('查看 myName 的屬性特徵:', Object.getOwnPropertyDescriptor(obj, 'myName')); // {value: "Ray", writable: false, enumerable: true, configurable: false}

obj.myName = 'QQ123'; // 無法被修改
delete obj.myName; // 無法刪除
obj.url = 'https://hsiangfeng.github.io/'; // 無法新增

最後的最後,在上面其實沒有特別講到如果是物件下的物件,例如 obj.blog 的話那麼上面三個方法是否可以調整?其實都是可以調整的,因為物件本身是採用參考位置,因此不論任何一個方法都是針對當前物件設置封裝凍結等,而不會針對其他物件調整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
myName: 'Ray',
blog: {},
};

Object.freeze(obj);

console.log('是否被封裝:', Object.isSealed(obj)); // true
console.log('是否可以被擴充:', Object.isExtensible(obj)); // false
console.log('是否被凍結:', Object.isFrozen(obj)); // true
console.log('查看 myName 的屬性特徵:', Object.getOwnPropertyDescriptor(obj, 'myName')); // {value: "Ray", writable: false, enumerable: true, configurable: false}

obj.myName = 'QQ123'; // 無法被修改
delete obj.myName; // 無法刪除
obj.url = 'https://hsiangfeng.github.io/'; // 無法新增

obj.blog.url = 'https://hsiangfeng.github.io/'; // 可以被新增
console.log(obj); // { blog: {url: "https://hsiangfeng.github.io/"},myName: "Ray" }

當然如果你希望深層物件也跟著凍結的話,其實 MDN 也有提供範例程式碼,以下擷取 MDN:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function deepFreeze(obj) {

// 取得物件的所有屬性名稱
var propNames = Object.getOwnPropertyNames(obj);

// 在凍結物件本身之前先凍結物件的所有物件屬性
propNames.forEach(function(name) {
var prop = obj[name];

// 凍結 prop 如果它是一個物件
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});

// 凍結本身 (no-op 如果已經被凍結了)
return Object.freeze(obj);
}

obj2 = {
internal: {}
};

deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
obj2.internal.a; // undefined

基本上你設置了上述任一個方法後就無法再次取消,最主要是如果開發中你可以任意取消的話,那麼就失去了這些語法的意義,畢竟這些語法最主要是避免不可被修改的物件被修改

參考文獻