瘋狂的技術宅 前端先鋒 html
每日前端夜話0xDB
每日前端夜話,陪你聊前端。
天天晚上18:00準時推送。
正文共:6189 字
預計閱讀時間:15 分鐘
做者:Axel Rauschmayer 博士
翻譯:瘋狂的技術宅
來源:2ality前端
本文回答瞭如下問題:程序員
共享可變狀態的解釋以下:es6
1function logElements(arr) { 2 while (arr.length > 0) { 3 console.log(arr.shift()); 4 } 5} 6 7function main() { 8 const arr = ['banana', 'orange', 'apple']; 9 10 console.log('Before sorting:'); 11 logElements(arr); 12 13 arr.sort(); // changes arr 14 15 console.log('After sorting:'); 16 logElements(arr); // (A) 17} 18main(); 19 20// Output: 21// 'Before sorting:' 22// 'banana' 23// 'orange' 24// 'apple' 25// 'After sorting:'
這裏有兩個獨立的部分:函數logElements()和函數main()。後者想要在對數組進行排序的先後都打印其內容。可是它到用了 logElements() ,會致使數組被清空。因此 main() 會在A行輸出一個空數組。正則表達式
在本文的剩餘部分,咱們將介紹三種避免共享可變狀態問題的方法:編程
在開始研究如何避免共享以前,咱們須要看一下如何在 JavaScript 中複製數據。json
對於數據,有兩個可複製的「深度」:數組
讓咱們看一下淺拷貝的幾種方法。安全
經過傳播複製普通對象和數組數據結構
咱們能夠擴展爲對象字面量和擴展爲數組字面量進行復制:
1const copyOfObject = {...originalObject}; 2const copyOfArray = [...originalArray];
可是傳播有幾個限制:
1class MyClass {} 2 3const original = new MyClass(); 4assert.equal(MyClass.prototype.isPrototypeOf(original), true); 5 6const copy = {...original}; 7assert.equal(MyClass.prototype.isPrototypeOf(copy), false);
正則表達式和日期之類的特殊對象有未複製的特殊「內部插槽」。
1const proto = { inheritedProp: 'a' }; 2const original = {__proto__: proto, ownProp: 'b' }; 3assert.equal(original.inheritedProp, 'a'); 4assert.equal(original.ownProp, 'b'); 5 6const copy = {...original}; 7assert.equal(copy.inheritedProp, undefined); 8assert.equal(copy.ownProp, 'b');
1const arr = ['a', 'b']; 2assert.equal(arr.length, 2); 3assert.equal({}.hasOwnProperty.call(arr, 'length'), true); 4 5const copy = {...arr}; 6assert.equal({}.hasOwnProperty.call(copy, 'length'), false);
1const original = Object.defineProperties({}, { 2prop: { 3 value: 1, 4 writable: false, 5 configurable: false, 6 enumerable: true, 7}, 8}); 9assert.deepEqual(original, {prop: 1}); 10 11const copy = {...original}; 12// Attributes 「writable」 and 「configurable」 of copy are different: 13assert.deepEqual(Object.getOwnPropertyDescriptors(copy), { 14prop: { 15 value: 1, 16 writable: true, 17 configurable: true, 18 enumerable: true, 19}, 20});
這意味着,getter 和 setter 都不會被如實地被複制:value 屬性(用於數據屬性),get 屬性(用於 getter)和set 屬性(用於 setter)是互斥的。
js const original = { get myGetter() { return 123 }, set mySetter(x) {}, }; assert.deepEqual({...original}, { myGetter: 123, // not a getter anymore! mySetter: undefined, });
1const original = {name: 'Jane', work: {employer: 'Acme'}}; 2const copy = {...original}; 3 4// Property .name is a copy 5copy.name = 'John'; 6assert.deepEqual(original, 7{name: 'Jane', work: {employer: 'Acme'}}); 8assert.deepEqual(copy, 9{name: 'John', work: {employer: 'Acme'}}); 10 11// The value of .work is shared 12copy.work.employer = 'Spectre'; 13assert.deepEqual( 14original, {name: 'Jane', work: {employer: 'Spectre'}}); 15assert.deepEqual( 16copy, {name: 'John', work: {employer: 'Spectre'}});
這些限制有的能夠消除,而其餘則不能:
1class MyClass {} 2 3const original = new MyClass(); 4 5const copy = { 6__proto__: Object.getPrototypeOf(original), 7...original, 8}; 9assert.equal(MyClass.prototype.isPrototypeOf(copy), true);
另外,咱們能夠在副本建立後經過 Object.setPrototypeOf() 設置原型。
沒有簡單的方法能夠通用地複製特殊對象。
如前所述,僅複製本身的屬性是功能而非限制。
咱們能夠用 Object.getOwnPropertyDescriptors() 和 Object.defineProperties() 複製對象(操做方法稍後說明):
他們考慮了全部屬性(而不只僅是 value),所以正確地複製了getters,setters,只讀屬性等。
用 Object.getOwnPropertyDescriptors() 檢索可枚舉和不可枚舉的屬性。
Object.assign()
進行淺拷貝(高級)Object.assign()的工做原理就像傳播到對象中同樣。也就是說如下兩種複製方式大體相同:
1const copy1 = {...original}; 2const copy2 = Object.assign({}, original);
使用方法而不是語法的好處是能夠經過庫在舊的 JavaScript 引擎上對其進行填充。
不過 Object.assign() 並不徹底像傳播。它在一個相對微妙的方面有所不一樣:它以不一樣的方式建立屬性。
1const original = {['__proto__']: null}; 2const copy1 = {...original}; 3// copy1 has the own property '__proto__' 4assert.deepEqual( 5 Object.keys(copy1), ['__proto__']); 6 7const copy2 = Object.assign({}, original); 8// copy2 has the prototype null 9assert.equal(Object.getPrototypeOf(copy2), null);
Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
進行淺拷貝(高級)JavaScript 使咱們能夠經過屬性描述符建立屬性,這些對象指定屬性屬性。例如,經過 Object.defineProperties() ,咱們已經看到了它。若是將該方法與 Object.getOwnPropertyDescriptors()結合使用,則能夠更加忠實地進行復制:
1function copyAllOwnProperties(original) { 2 return Object.defineProperties( 3 {}, Object.getOwnPropertyDescriptors(original)); 4}
這消除了經過傳播複製對象的兩個限制。
首先,可以正確複製本身 property 的全部 attribute。咱們如今能夠複製本身的 getter 和 setter:
1const original = { 2 get myGetter() { return 123 }, 3 set mySetter(x) {}, 4}; 5assert.deepEqual(copyAllOwnProperties(original), original);
其次,因爲使用了 Object.getOwnPropertyDescriptors(),非枚舉屬性也被複制了:
1const arr = ['a', 'b']; 2assert.equal(arr.length, 2); 3assert.equal({}.hasOwnProperty.call(arr, 'length'), true); 4 5const copy = copyAllOwnProperties(arr); 6assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
如今該解決深拷貝了。首先咱們將手動進行深拷貝,而後再研究通用方法。
若是嵌套傳播,則會獲得深層副本:
1const original = {name: 'Jane', work: {employer: 'Acme'}}; 2const copy = {name: original.name, work: {...original.work}}; 3 4// We copied successfully: 5assert.deepEqual(original, copy); 6// The copy is deep: 7assert.ok(original.work !== copy.work);
儘管這是一個 hack,可是在緊要關頭,它提供了一個快速的解決方案:爲了對 `original 對象進行深拷貝」,咱們首先將其轉換爲 JSON 字符串,而後再解析該它:
1function jsonDeepCopy(original) { 2 return JSON.parse(JSON.stringify(original)); 3} 4const original = {name: 'Jane', work: {employer: 'Acme'}}; 5const copy = jsonDeepCopy(original); 6assert.deepEqual(original, copy);
這種方法的主要缺點是,咱們只能複製具備 JSON 支持的鍵和值的屬性。
一些不受支持的鍵和值將被忽略:
1assert.deepEqual( 2 jsonDeepCopy({ 3 [Symbol('a')]: 'abc', 4 b: function () {}, 5 c: undefined, 6 }), 7 {} // empty object 8);
其餘致使的例外:
1assert.throws( 2 () => jsonDeepCopy({a: 123n}), 3 /^TypeError: Do not know how to serialize a BigInt$/);
能夠用如下函數進行通用深拷貝:
1function deepCopy(original) { 2 if (Array.isArray(original)) { 3 const copy = []; 4 for (const [index, value] of original.entries()) { 5 copy[index] = deepCopy(value); 6 } 7 return copy; 8 } else if (typeof original === 'object' && original !== null) { 9 const copy = {}; 10 for (const [key, value] of Object.entries(original)) { 11 copy[key] = deepCopy(value); 12 } 13 return copy; 14 } else { 15 // Primitive value: atomic, no need to copy 16 return original; 17 } 18}
該函數處理三種狀況:
1const original = {a: 1, b: {c: 2, d: {e: 3}}}; 2const copy = deepCopy(original); 3 4// Are copy and original deeply equal? 5assert.deepEqual(copy, original); 6 7// Did we really copy all levels 8// (equal content, but different objects)? 9assert.ok(copy !== original); 10assert.ok(copy.b !== original.b); 11assert.ok(copy.b.d !== original.b.d);
注意,deepCopy() 僅解決了一個擴展問題:淺拷貝。而其餘全部內容:不復制原型,僅部分複製特殊對象,忽略不可枚舉的屬性,忽略大多數屬性。
一般徹底徹底實現複製是不可能的:並不是全部數據的都是一棵樹,有時你並不須要全部屬性,等等。
deepCopy()
版本若是咱們使用 .map() 和 Object.fromEntries(),可使之前的 deepCopy() 實現更加簡潔:
1function deepCopy(original) { 2 if (Array.isArray(original)) { 3 return original.map(elem => deepCopy(elem)); 4 } else if (typeof original === 'object' && original !== null) { 5 return Object.fromEntries( 6 Object.entries(original) 7 .map(([k, v]) => [k, deepCopy(v)])); 8 } else { 9 // Primitive value: atomic, no need to copy 10 return original; 11 } 12}
一般使用兩種技術能夠實現類實例的深拷貝:
.clone()
方法該技術爲每一個類引入了一個方法 .clone(),其實例將被深拷貝。它返回 this 的深層副本。如下例子顯示了能夠克隆的三個類。
1class Point { 2 constructor(x, y) { 3 this.x = x; 4 this.y = y; 5 } 6 clone() { 7 return new Point(this.x, this.y); 8 } 9} 10class Color { 11 constructor(name) { 12 this.name = name; 13 } 14 clone() { 15 return new Color(this.name); 16 } 17} 18class ColorPoint extends Point { 19 constructor(x, y, color) { 20 super(x, y); 21 this.color = color; 22 } 23 clone() { 24 return new ColorPoint( 25 this.x, this.y, this.color.clone()); // (A) 26 } 27}
A 行展現了此技術的一個重要方面:複合實例屬性值也必須遞歸克隆。
靜態工廠方法
拷貝構造函數是用當前類的另外一個實例來設置當前實例的構造函數。拷貝構造函數在靜態語言(例如 C++ 和 Java)中很流行,你能夠在其中經過 static 重載(static 表示它在編譯時發生)提供構造函數的多個版本。
在 JavaScript 中,你能夠執行如下操做(但不是很優雅):
1class Point { 2 constructor(...args) { 3 if (args[0] instanceof Point) { 4 // Copy constructor 5 const [other] = args; 6 this.x = other.x; 7 this.y = other.y; 8 } else { 9 const [x, y] = args; 10 this.x = x; 11 this.y = y; 12 } 13 } 14}
這是使用方法:
1const original = new Point(-1, 4); 2const copy = new Point(original); 3assert.deepEqual(copy, original);
相反,靜態工廠方法在 JavaScript 中效果更好(static 意味着它們是類方法)。
在如下示例中,三個類 Point,Color 和 ColorPoint 分別具備靜態工廠方法 .from():
1class Point { 2 constructor(x, y) { 3 this.x = x; 4 this.y = y; 5 } 6 static from(other) { 7 return new Point(other.x, other.y); 8 } 9} 10class Color { 11 constructor(name) { 12 this.name = name; 13 } 14 static from(other) { 15 return new Color(other.name); 16 } 17} 18class ColorPoint extends Point { 19 constructor(x, y, color) { 20 super(x, y); 21 this.color = color; 22 } 23 static from(other) { 24 return new ColorPoint( 25 other.x, other.y, Color.from(other.color)); // (A) 26 } 27}
在 A 行中,咱們再次使用遞歸複製。
這是 ColorPoint.from() 的工做方式:
1const original = new ColorPoint(-1, 4, new Color('red')); 2const copy = ColorPoint.from(original); 3assert.deepEqual(copy, original);
只要咱們僅從共享狀態讀取,就不會有任何問題。在修改它以前,咱們須要經過複製(必要的深度)來「取消共享」。
防護性複製是一種在問題可能出現時始終進行復制的技術。其目的是確保當前實體(函數、類等)的安全:
下一節說明兩種防護性複製。
請記住,在本文開頭的例子中,咱們遇到了麻煩,由於 logElements() 修改了其參數 arr:
1function logElements(arr) { 2 while (arr.length > 0) { 3 console.log(arr.shift()); 4 } 5}
讓咱們在此函數中添加防護性複製:
1function logElements(arr) { 2 arr = [...arr]; // defensive copy 3 while (arr.length > 0) { 4 console.log(arr.shift()); 5 } 6}
如今,若是在 main() 內部調用 logElements() 不會再引起問題:
1function main() { 2 const arr = ['banana', 'orange', 'apple']; 3 4 console.log('Before sorting:'); 5 logElements(arr); 6 7 arr.sort(); // changes arr 8 9 console.log('After sorting:'); 10 logElements(arr); // (A) 11} 12main(); 13 14// Output: 15// 'Before sorting:' 16// 'banana' 17// 'orange' 18// 'apple' 19// 'After sorting:' 20// 'apple' 21// 'banana' 22// 'orange'
讓咱們從 StringBuilder 類開始,該類不會複製它公開的內部數據(A行):
1class StringBuilder { 2 constructor() { 3 this._data = []; 4 } 5 add(str) { 6 this._data.push(str); 7 } 8 getParts() { 9 // We expose internals without copying them: 10 return this._data; // (A) 11 } 12 toString() { 13 return this._data.join(''); 14 } 15}
只要不使用 .getParts(),一切就能夠正常工做:
1const sb1 = new StringBuilder(); 2sb1.add('Hello'); 3sb1.add(' world!'); 4assert.equal(sb1.toString(), 'Hello world!');
可是,若是更改了 .getParts() 的結果(A行),則 StringBuilder 會中止正常工做:
1const sb2 = new StringBuilder(); 2sb2.add('Hello'); 3sb2.add(' world!'); 4sb2.getParts().length = 0; // (A) 5assert.equal(sb2.toString(), ''); // not OK
解決方案是在內部 ._data 被公開以前防護性地對它進行復制(A行):
1class StringBuilder { 2 constructor() { 3 this._data = []; 4 } 5 add(str) { 6 this._data.push(str); 7 } 8 getParts() { 9 // Copy defensively 10 return [...this._data]; // (A) 11 } 12 toString() { 13 return this._data.join(''); 14 } 15}
如今,更改 .getParts() 的結果再也不干擾 sb 的操做:
1const sb = new StringBuilder(); 2sb.add('Hello'); 3sb.add(' world!'); 4sb.getParts().length = 0; 5assert.equal(sb.toString(), 'Hello world!'); // OK
咱們將首先探討以破壞性方式和非破壞性方式更新數據之間的區別。而後將學習非破壞性更新如何避免數據改變。
咱們能夠區分兩種不一樣的數據更新方式:
這就是咱們破壞性地設置對象的屬性 .city 的方式:
1const obj = {city: 'Berlin', country: 'Germany'}; 2const key = 'city'; 3obj[key] = 'Munich'; 4assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});
如下函數以非破壞性的方式更改屬性:
1function setObjectNonDestructively(obj, key, value) { 2 const updatedObj = {}; 3 for (const [k, v] of Object.entries(obj)) { 4 updatedObj[k] = (k === key ? value : v); 5 } 6 return updatedObj; 7}
它的用法以下:
1const obj = {city: 'Berlin', country: 'Germany'}; 2const updatedObj = setObjectNonDestructively(obj, 'city', 'Munich'); 3assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'}); 4assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});
傳播使 setObjectNonDestructively() 更加簡潔:
1function setObjectNonDestructively(obj, key, value) { 2 return {...obj, [key]: value}; 3}
注意:setObject NonDestructively() 的兩個版本都進行了較淺的更新。
如下是破壞性地設置數組元素的方式:
1const original = ['a', 'b', 'c', 'd', 'e']; 2original[2] = 'x'; 3assert.deepEqual(original, ['a', 'b', 'x', 'd', 'e']);
非破壞性地更新數組要複雜得多。
1function setArrayNonDestructively(arr, index, value) { 2 const updatedArr = []; 3 for (const [i, v] of arr.entries()) { 4 updatedArr.push(i === index ? value : v); 5 } 6 return updatedArr; 7} 8 9const arr = ['a', 'b', 'c', 'd', 'e']; 10const updatedArr = setArrayNonDestructively(arr, 2, 'x'); 11assert.deepEqual(updatedArr, ['a', 'b', 'x', 'd', 'e']); 12assert.deepEqual(arr, ['a', 'b', 'c', 'd', 'e']);
.slice() 和擴展使 setArrayNonDestructively() 更加簡潔:
1function setArrayNonDestructively(arr, index, value) { 2 return [ 3 ...arr.slice(0, index), value, ...arr.slice(index+1)] 4}
注意:setArrayNonDestructively() 的兩個版本都進行了較淺的更新。
到目前爲止,咱們只是淺層地更新了數據。讓咱們來解決深度更新。如下代碼顯示瞭如何手動執行此操做。咱們正在更改 name 和 employer。
1const original = {name: 'Jane', work: {employer: 'Acme'}}; 2const updatedOriginal = { 3 ...original, 4 name: 'John', 5 work: { 6 ...original.work, 7 employer: 'Spectre' 8 }, 9}; 10 11assert.deepEqual( 12 original, {name: 'Jane', work: {employer: 'Acme'}}); 13assert.deepEqual( 14 updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});
如下函數實現了通用的深度更新。
1function deepUpdate(original, keys, value) { 2 if (keys.length === 0) { 3 return value; 4 } 5 const currentKey = keys[0]; 6 if (Array.isArray(original)) { 7 return original.map( 8 (v, index) => index === currentKey 9 ? deepUpdate(v, keys.slice(1), value) // (A) 10 : v); // (B) 11 } else if (typeof original === 'object' && original !== null) { 12 return Object.fromEntries( 13 Object.entries(original).map( 14 (keyValuePair) => { 15 const [k,v] = keyValuePair; 16 if (k === currentKey) { 17 return [k, deepUpdate(v, keys.slice(1), value)]; // (C) 18 } else { 19 return keyValuePair; // (D) 20 } 21 })); 22 } else { 23 // Primitive value 24 return original; 25 } 26}
若是咱們將 value 視爲要更新的樹的根,則 deepUpdate() 只會深度更改單個分支(A 和 C 行)。全部其餘分支均被淺複製(B 和 D 行)。
如下是使用 deepUpdate() 的樣子:
1const original = {name: 'Jane', work: {employer: 'Acme'}}; 2 3const copy = deepUpdate(original, ['work', 'employer'], 'Spectre'); 4assert.deepEqual(copy, {name: 'Jane', work: {employer: 'Spectre'}}); 5assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}});
使用非破壞性更新,共享數據將變得毫無問題,由於咱們永遠不會改變共享數據。(顯然,這隻有在各方都這樣作的狀況下才有效。)
有趣的是,複製數據變得很是簡單:
1const original = {city: 'Berlin', country: 'Germany'}; 2const copy = original;
僅在必要時以及在咱們進行無損更改的狀況下,才進行 original 的實際複製。
咱們能夠經過使共享數據不變來防止共享數據發生改變。接下來,咱們將研究 JavaScript 如何支持不變性。以後,討論不可變數據如何幫助共享可變狀態。
JavaScript 具備三個級別的保護對象:
Preventing extensions 使得沒法向對象添加新屬性。可是,你仍然能夠刪除和更改屬性。
Sealing 能夠防止擴展,並使全部屬性都沒法配置(大約:您沒法再更改屬性的工做方式)。
方法: Object.seal(obj)
Freezing 使對象的全部屬性不可寫後將其密封。也就是說,對象是不可擴展的,全部屬性都是隻讀的,沒法更改它。
鑑於咱們但願對象是徹底不變的,所以在本文中僅使用 Object.freeze()。
Object.freeze(obj) 僅凍結 obj 及其屬性。它不會凍結那些屬性的值,例如:
1const teacher = { 2 name: 'Edna Krabappel', 3 students: ['Bart'], 4}; 5Object.freeze(teacher); 6 7assert.throws( 8 () => teacher.name = 'Elizabeth Hoover', 9 /^TypeError: Cannot assign to read only property 'name'/); 10 11teacher.students.push('Lisa'); 12assert.deepEqual( 13 teacher, { 14 name: 'Edna Krabappel', 15 students: ['Bart', 'Lisa'], 16 });
若是要深度凍結,則須要本身實現:
1function deepFreeze(value) { 2 if (Array.isArray(value)) { 3 for (const element of value) { 4 deepFreeze(element); 5 } 6 Object.freeze(value); 7 } else if (typeof value === 'object' && value !== null) { 8 for (const v of Object.values(value)) { 9 deepFreeze(v); 10 } 11 Object.freeze(value); 12 } else { 13 // Nothing to do: primitive values are already immutable 14 } 15 return value; 16}
回顧上一節中的例子,咱們能夠檢查 deepFreeze() 是否真的凍結了:
1const teacher = { 2 name: 'Edna Krabappel', 3 students: ['Bart'], 4}; 5deepFreeze(teacher); 6 7assert.throws( 8 () => teacher.name = 'Elizabeth Hoover', 9 /^TypeError: Cannot assign to read only property 'name'/); 10 11assert.throws( 12 () => teacher.students.push('Lisa'), 13 /^TypeError: Cannot add property 1, object is not extensible$/);
用不可變的包裝器包裝可變的集合並提供相同的 API,但沒有破壞性的操做。如今對於同一集合,咱們有兩個接口:一個是可變的,另外一個是不可變的。當咱們具備要安全的公開內部可變數據時,這頗有用。
接下來展現了 Maps 和 Arrays 的包裝器。它們都有如下限制:
類 ImmutableMapWrapper 爲 map 生成包裝器:
1class ImmutableMapWrapper { 2 constructor(map) { 3 this._self = map; 4 } 5} 6 7// Only forward non-destructive methods to the wrapped Map: 8for (const methodName of ['get', 'has', 'keys', 'size']) { 9 ImmutableMapWrapper.prototype[methodName] = function (...args) { 10 return this._self[methodName](...args); 11 } 12}
這是 action 中的類:
1const map = new Map([[false, 'no'], [true, 'yes']]); 2const wrapped = new ImmutableMapWrapper(map); 3 4// Non-destructive operations work as usual: 5assert.equal( 6 wrapped.get(true), 'yes'); 7assert.equal( 8 wrapped.has(false), true); 9assert.deepEqual( 10 [...wrapped.keys()], [false, true]); 11 12// Destructive operations are not available: 13assert.throws( 14 () => wrapped.set(false, 'never!'), 15 /^TypeError: wrapped.set is not a function$/); 16assert.throws( 17 () => wrapped.clear(), 18 /^TypeError: wrapped.clear is not a function$/); 19
對於數組 arr,常規包裝是不夠的,由於咱們不只須要攔截方法調用,並且還須要攔截諸如 arr [1] = true 之類的屬性訪問。JavaScript proxies 使咱們可以執行這種操做:
1const RE_INDEX_PROP_KEY = /^[0-9]+$/; 2const ALLOWED_PROPERTIES = new Set([ 3 'length', 'constructor', 'slice', 'concat']); 4 5function wrapArrayImmutably(arr) { 6 const handler = { 7 get(target, propKey, receiver) { 8 // We assume that propKey is a string (not a symbol) 9 if (RE_INDEX_PROP_KEY.test(propKey) // simplified check! 10 || ALLOWED_PROPERTIES.has(propKey)) { 11 return Reflect.get(target, propKey, receiver); 12 } 13 throw new TypeError(`Property "${propKey}" can’t be accessed`); 14 }, 15 set(target, propKey, value, receiver) { 16 throw new TypeError('Setting is not allowed'); 17 }, 18 deleteProperty(target, propKey) { 19 throw new TypeError('Deleting is not allowed'); 20 }, 21 }; 22 return new Proxy(arr, handler); 23}
讓咱們包裝一個數組:
1const arr = ['a', 'b', 'c']; 2const wrapped = wrapArrayImmutably(arr); 3 4// Non-destructive operations are allowed: 5assert.deepEqual( 6 wrapped.slice(1), ['b', 'c']); 7assert.equal( 8 wrapped[1], 'b'); 9 10// Destructive operations are not allowed: 11assert.throws( 12 () => wrapped[1] = 'x', 13 /^TypeError: Setting is not allowed$/); 14assert.throws( 15 () => wrapped.shift(), 16 /^TypeError: Property "shift" can’t be accessed$/);
若是數據是不可變的,則能夠共享數據而沒有任何風險。特別是無需防護性複製。
非破壞性更新是對不變數據的補充,使其與可變數據同樣通用,但沒有相關風險。
有幾種可用於 JavaScript 的庫,它們支持對不可變數據進行無損更新。其中流行的兩種是:
在其存儲庫中,Immutable.js 的描述爲:
用於 JavaScript 的不可變的持久數據集,可提升效率和簡便性。
Immutable.js 提供了不可變的數據結構,例如:
1import {Map} from 'immutable/dist/immutable.es.js'; 2const map0 = Map([ 3 [false, 'no'], 4 [true, 'yes'], 5]); 6 7const map1 = map0.set(true, 'maybe'); // (A) 8assert.ok(map1 !== map0); // (B) 9assert.equal(map1.equals(map0), false); 10 11const map2 = map1.set(true, 'yes'); // (C) 12assert.ok(map2 !== map1); 13assert.ok(map2 !== map0); 14assert.equal(map2.equals(map0), true); // (D)
說明:
在其存儲庫中,Immer 庫 的描述爲:
經過更改當前狀態來建立下一個不可變狀態。
Immer 有助於非破壞性地更新(可能嵌套)普通對象和數組。也就是說,不涉及特殊的數據結構。
這是使用 Immer 的樣子:
1import {produce} from 'immer/dist/immer.module.js'; 2 3const people = [ 4 {name: 'Jane', work: {employer: 'Acme'}}, 5]; 6 7const modifiedPeople = produce(people, (draft) => { 8 draft[0].work.employer = 'Cyberdyne'; 9 draft.push({name: 'John', work: {employer: 'Spectre'}}); 10}); 11 12assert.deepEqual(modifiedPeople, [ 13 {name: 'Jane', work: {employer: 'Cyberdyne'}}, 14 {name: 'John', work: {employer: 'Spectre'}}, 15]); 16assert.deepEqual(people, [ 17 {name: 'Jane', work: {employer: 'Acme'}}, 18]);
原始數據存儲在 people 中。produce() 爲咱們提供了一個變量 draft。咱們假設這個變量是 people,並使用一般會進行破壞性更改的操做。Immer 攔截了這些操做。代替變異draft,它無損地改變 people。結果由 modifiedPeople 引用。它是一成不變的。
Ron Korvig 提醒我在 JavaScript 中進行深拷貝時使用靜態工廠方法,而不要重載構造函數。