1. 程式人生 > 其它 >5.12 不可變物件

5.12 不可變物件

5.12.1 不可變物件的定義

所謂不可變物件,指的是在被生成之後狀態不能再被改變的物件。由於物件的狀態是由其各個屬性的值所決定的,因此從形式上來說也是指無法改變屬性的值的物件。也有觀點認為,在物件引用了另一個物件的情況下,只有當那個被引用的物件也是不可變的時候,引用了它的物件才能被稱為不可變物件。

從廣義上來說,不可變物件指的是不去改變狀態的物件。而從狹義上來說,只有既沒有改變,也無法改變狀態的物件,即為了禁止改變而專門設計的物件,才被稱為不可變物件。JavaScript 中的一種典型的不可變物件就是字串物件。

5.12.2 不可變物件的作用

靈活運用不可變物件有助於提高程式的健壯性。這是因為,程式中的很多錯誤都是由於非法改變了物件的狀態而造成的。例如,將物件傳遞給方法的引數時,存在方法會改寫物件內容的隱患。如果那是一個不可變物件,則不用擔心這一問題。不清楚物件的內部構造就改寫很容易引起錯誤,在排除了這種情況之後,就可以減少花在這個問題上的精力。

雖然不可變物件是一種便利的程式設計技巧,但其實在 JavaScript 開發中並沒有被大量使用。其中最主要的一個原因就是花銷的取捨。為了確保物件的不可變,不得不增加一些和主要功能無關的程式碼。對於一直使用小規模程式碼的 JavaScript 來說,需要權衡花銷。

5.12.3 實現不可變物件的方式

在 JavaScript 中可以通過以下方式實現物件的不可變。

  • 將屬性(狀態)隱藏,不提供變更操作。
  • 靈活運用 ECMAScript 第 5 版中提供的函式。
  • 靈活運用 writable 屬性、configurable 屬性以及 setter 和 getter。

JavaScript 中的物件沒有像 private 屬性這樣的顯式訪問控制功能。為了將屬性隱藏,可以使用一種被稱為閉包的方法。

在 ECMAScript 第 5 版中有一些用於支援物件的不可變化的函式(表 5.2)。seal 可以向下相容 preventExtensions,freeze 可以向下相容 seal。這裡的向下相容,指的是比後者有更為嚴格的限制。

表 5.2 ECMAScript 第 5 版中用於支援物件的不可變化的函式
方法名 屬性新增 屬性刪除 屬性變更 確認方法
preventExtension × Object.isExtensible
seal × × Object.isSealed
freeze × × × Object.isFrozen

程式碼清單 5.6 ~ 程式碼清單 5.8 是各個方法的具體示例。Object.keys方法用於對屬性列舉。

程式碼清單5.6 Object.preventExtensions的例子
var hzh = { x:2, y:3 };
console.log("呼叫preventExtensions方法:");
console.log(Object.preventExtensions(hzh));
console.log("");
// 無法新增屬性
hzh.z = 4;
console.log("看是否新增z屬性:");
console.log(Object.keys(hzh));
console.log("");
// 可以刪除屬性
delete hzh.y;
console.log("是否已經刪除y屬性:");
console.log(Object.keys(hzh));
console.log("");
// 可以更改屬性值
hzh.x = 20;
console.log("x屬性是否已經更改:");
console.log("hzh.x = " + hzh.x);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
呼叫preventExtensions方法:
{ x: 2, y: 3 }

看是否新增z屬性:
[ 'x', 'y' ]

是否已經刪除y屬性:
[ 'x' ]

x屬性是否已經更改:
hzh.x = 20

[Done] exited with code=0 in 0.281 seconds
程式碼清單5.7 Object.seal的例子
var hzh = { x:2, y:3 };
console.log("呼叫seal方法:");
console.log(Object.seal(hzh));
console.log("");
// 無法刪除屬性
delete hzh.y;
console.log("是否已經刪除y屬性:");
console.log(Object.keys(hzh));
console.log("");
// 可以更改屬性值
hzh.x = 20;
console.log("x屬性是否已經更改:");
console.log("hzh.x = " + hzh.x);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
呼叫seal方法:
{ x: 2, y: 3 }

是否已經刪除y屬性:
[ 'x', 'y' ]

x屬性是否已經更改:
hzh.x = 20

[Done] exited with code=0 in 0.203 seconds
程式碼清單5.8 Object.freeze的例子
var hzh = { x:2, y:3 };
console.log("呼叫freeze方法:");
console.log(Object.freeze(hzh));
console.log("");
// 無法更改屬性值
hzh.x = 20;
console.log("x屬性是否已經更改:");
console.log("hzh.x = " + hzh.x);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
呼叫freeze方法:
{ x: 2, y: 3 }

x屬性是否已經更改:
hzh.x = 2

[Done] exited with code=0 in 0.182 seconds

對於表 5.2 中的方法,有以下幾點需要注意。

  • 一旦更改就無法還原。
  • 如果想讓原型繼承中的被繼承方也不可變化,需要對其進行顯式的操作。

從內部實現來看,seal 的作用是將屬性的 configurable 屬性置為假,而 freeze 是將 writable 屬性置為假。如果在生成物件時,對這些屬性進行顯式地設定,也能夠取得相同的效果。靈活運用屬性的屬性,還能夠實現只有 getter 方法而沒有setter 方法的不可變物件。

儘可能不使用不可變物件。應該為程式的健壯性與其開銷選擇一個折中方案。為了安全性而增加開銷,產品可能就會無法按時完成。此外,客戶端 JavaScript 對程式碼的體積有著嚴格的要求,因此過分注重安全性的程式碼可能反而會降低使用者體驗。這不是一個簡單的是非問題,而是一個需要做出判斷的問題。儘管不可變物件是提升程式碼健壯性的一個有效方法,但如果過分拘泥於此而降低了使用者的使用體驗,反而本末
倒置了。實際的程式開發與理論研究有所不同,請時刻謹記考慮健壯性與開銷之間的平衡。