疯狂的技术宅 前端先锋 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 中进行深拷贝时使用静态工厂方法,而不要重载构造函数。