做者:Dr. Axel Rauschmayer翻译:疯狂的技术宅html
原文:https://2ality.com/2019/10/sh...前端
未经容许严禁转载git
本文回答了如下问题:程序员
标有“(高级)”的部分会更深刻,若是你想更快地阅读本文,能够跳过。es6
共享可变状态的解释以下:github
则可能会有一方修改会致使另外一方没法正常工做的风险。如下是一个例子:面试
function logElements(arr) { while (arr.length > 0) { console.log(arr.shift()); } } function main() { const arr = ['banana', 'orange', 'apple']; console.log('Before sorting:'); logElements(arr); arr.sort(); // changes arr console.log('After sorting:'); logElements(arr); // (A) } main(); // Output: // 'Before sorting:' // 'banana' // 'orange' // 'apple' // 'After sorting:'
这里有两个独立的部分:函数logElements()
和函数main()
。后者想要在对数组进行排序的先后都打印其内容。可是它到用了 logElements()
,会致使数组被清空。因此 main()
会在A行输出一个空数组。正则表达式
在本文的剩余部分,咱们将介绍三种避免共享可变状态问题的方法:编程
针对每一种方法,咱们都会回到刚才看到的示例并进行修复。json
在开始研究如何避免共享以前,咱们须要看一下如何在 JavaScript 中复制数据。
对于数据,有两个可复制的“深度”:
不幸的是,JavaScript 仅内置了对浅拷贝的支持。若是须要深拷贝,则须要本身实现。
让咱们看一下浅拷贝的几种方法。
const copyOfObject = {...originalObject}; const copyOfArray = [...originalArray];
可是传播有几个限制:
class MyClass {} const original = new MyClass(); assert.equal(MyClass.prototype.isPrototypeOf(original), true); const copy = {...original}; assert.equal(MyClass.prototype.isPrototypeOf(copy), false);
copy
中没有 original
的继承属性 .inheritedProp
,由于咱们仅复制本身的属性,而未保留原型。const proto = { inheritedProp: 'a' }; const original = {__proto__: proto, ownProp: 'b' }; assert.equal(original.inheritedProp, 'a'); assert.equal(original.ownProp, 'b'); const copy = {...original}; assert.equal(copy.inheritedProp, undefined); assert.equal(copy.ownProp, 'b');
.length
不可枚举,也不能复制:const arr = ['a', 'b']; assert.equal(arr.length, 2); assert.equal({}.hasOwnProperty.call(arr, 'length'), true); const copy = {...arr}; assert.equal({}.hasOwnProperty.call(copy, 'length'), false);
const original = Object.defineProperties({}, { prop: { value: 1, writable: false, configurable: false, enumerable: true, }, }); assert.deepEqual(original, {prop: 1}); const copy = {...original}; // Attributes `writable` and `configurable` of copy are different: assert.deepEqual(Object.getOwnPropertyDescriptors(copy), { prop: { value: 1, writable: true, configurable: true, enumerable: true, }, });
这意味着,getter 和 setter 都不会被如实地被复制:value
属性(用于数据属性),get
属性(用于 getter)和set
属性(用于 setter)是互斥的。
const original = { get myGetter() { return 123 }, set mySetter(x) {}, }; assert.deepEqual({...original}, { myGetter: 123, // not a getter anymore! mySetter: undefined, });
const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = {...original}; // Property .name is a copy copy.name = 'John'; assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}}); assert.deepEqual(copy, {name: 'John', work: {employer: 'Acme'}}); // The value of .work is shared copy.work.employer = 'Spectre'; assert.deepEqual( original, {name: 'Jane', work: {employer: 'Spectre'}}); assert.deepEqual( copy, {name: 'John', work: {employer: 'Spectre'}});
这些限制有的能够消除,而其余则不能:
class MyClass {} const original = new MyClass(); const copy = { __proto__: Object.getPrototypeOf(original), ...original, }; assert.equal(MyClass.prototype.isPrototypeOf(copy), true);
另外,咱们能够在副本建立后经过 Object.setPrototypeOf()
设置原型。
咱们能够用 Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
复制对象(操做方法稍后说明):
value
),所以正确地复制了getters,setters,只读属性等。Object.getOwnPropertyDescriptors()
检索可枚举和不可枚举的属性。Object.assign()
进行浅拷贝(高级)Object.assign()
的工做原理就像传播到对象中同样。也就是说如下两种复制方式大体相同:
const copy1 = {...original}; const copy2 = Object.assign({}, original);
使用方法而不是语法的好处是能够经过库在旧的 JavaScript 引擎上对其进行填充。
不过 Object.assign()
并不彻底像传播。它在一个相对微妙的方面有所不一样:它以不一样的方式建立属性。
Object.assign()
使用 assignment 建立副本的属性。除其余事项外,assignment 会调用本身的和继承的设置器,而 definition 不会(关于 assignment 与 definition 的更多信息)。这种差别不多引发注意。如下代码是一个例子,但它是人为设计的:
const original = {['__proto__']: null}; const copy1 = {...original}; // copy1 has the own property '__proto__' assert.deepEqual( Object.keys(copy1), ['__proto__']); const copy2 = Object.assign({}, original); // copy2 has the prototype null assert.equal(Object.getPrototypeOf(copy2), null);
Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
进行浅拷贝(高级)JavaScript 使咱们能够经过属性描述符建立属性,这些对象指定属性属性。例如,经过 Object.defineProperties()
,咱们已经看到了它。若是将该方法与 Object.getOwnPropertyDescriptors()
结合使用,则能够更加忠实地进行复制:
function copyAllOwnProperties(original) { return Object.defineProperties( {}, Object.getOwnPropertyDescriptors(original)); }
这消除了经过传播复制对象的两个限制。
首先,可以正确复制本身 property 的全部 attribute。咱们如今能够复制本身的 getter 和 setter:
const original = { get myGetter() { return 123 }, set mySetter(x) {}, }; assert.deepEqual(copyAllOwnProperties(original), original);
其次,因为使用了 Object.getOwnPropertyDescriptors()
,非枚举属性也被复制了:
const arr = ['a', 'b']; assert.equal(arr.length, 2); assert.equal({}.hasOwnProperty.call(arr, 'length'), true); const copy = copyAllOwnProperties(arr); assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
如今该解决深拷贝了。首先咱们将手动进行深拷贝,而后再研究通用方法。
若是嵌套传播,则会获得深层副本:
const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = {name: original.name, work: {...original.work}}; // We copied successfully: assert.deepEqual(original, copy); // The copy is deep: assert.ok(original.work !== copy.work);
尽管这是一个 hack,可是在紧要关头,它提供了一个快速的解决方案:为了对 `original
对象进行深拷贝”,咱们首先将其转换为 JSON 字符串,而后再解析该它:
function jsonDeepCopy(original) { return JSON.parse(JSON.stringify(original)); } const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = jsonDeepCopy(original); assert.deepEqual(original, copy);
这种方法的主要缺点是,咱们只能复制具备 JSON 支持的键和值的属性。
一些不受支持的键和值将被忽略:
assert.deepEqual( jsonDeepCopy({ [Symbol('a')]: 'abc', b: function () {}, c: undefined, }), {} // empty object );
其余致使的例外:
assert.throws( () => jsonDeepCopy({a: 123n}), /^TypeError: Do not know how to serialize a BigInt$/);
能够用如下函数进行通用深拷贝:
function deepCopy(original) { if (Array.isArray(original)) { const copy = []; for (const [index, value] of original.entries()) { copy[index] = deepCopy(value); } return copy; } else if (typeof original === 'object' && original !== null) { const copy = {}; for (const [key, value] of Object.entries(original)) { copy[key] = deepCopy(value); } return copy; } else { // Primitive value: atomic, no need to copy return original; } }
该函数处理三种状况:
original
是一个数组,咱们建立一个新的 Array,并将 original
的元素复制到其中。original
是一个对象,咱们将使用相似的方法。original
是原始值,则无需执行任何操做。让咱们尝试一下deepCopy()
:
const original = {a: 1, b: {c: 2, d: {e: 3}}}; const copy = deepCopy(original); // Are copy and original deeply equal? assert.deepEqual(copy, original); // Did we really copy all levels // (equal content, but different objects)? assert.ok(copy !== original); assert.ok(copy.b !== original.b); assert.ok(copy.b.d !== original.b.d);
注意,deepCopy()
仅解决了一个扩展问题:浅拷贝。而其余全部内容:不复制原型,仅部分复制特殊对象,忽略不可枚举的属性,忽略大多数属性。
一般彻底彻底实现复制是不可能的:并不是全部数据的都是一棵树,有时你并不须要全部属性,等等。
deepCopy()
版本若是咱们使用 .map()
和 Object.fromEntries()
,可使之前的 deepCopy()
实现更加简洁:
function deepCopy(original) { if (Array.isArray(original)) { return original.map(elem => deepCopy(elem)); } else if (typeof original === 'object' && original !== null) { return Object.fromEntries( Object.entries(original) .map(([k, v]) => [k, deepCopy(v)])); } else { // Primitive value: atomic, no need to copy return original; } }
一般使用两种技术能够实现类实例的深拷贝:
.clone()
方法.clone()
方法该技术为每一个类引入了一个方法 .clone()
,其实例将被深拷贝。它返回 this
的深层副本。如下例子显示了能够克隆的三个类。
class Point { constructor(x, y) { this.x = x; this.y = y; } clone() { return new Point(this.x, this.y); } } class Color { constructor(name) { this.name = name; } clone() { return new Color(this.name); } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } clone() { return new ColorPoint( this.x, this.y, this.color.clone()); // (A) } }
A 行展现了此技术的一个重要方面:复合实例属性值也必须递归克隆。
拷贝构造函数是用当前类的另外一个实例来设置当前实例的构造函数。拷贝构造函数在静态语言(例如 C++ 和 Java)中很流行,你能够在其中经过 static 重载(static 表示它在编译时发生)提供构造函数的多个版本。
在 JavaScript 中,你能够执行如下操做(但不是很优雅):
class Point { constructor(...args) { if (args[0] instanceof Point) { // Copy constructor const [other] = args; this.x = other.x; this.y = other.y; } else { const [x, y] = args; this.x = x; this.y = y; } } }
这是使用方法:
const original = new Point(-1, 4); const copy = new Point(original); assert.deepEqual(copy, original);
相反,静态工厂方法在 JavaScript 中效果更好(static 意味着它们是类方法)。
在如下示例中,三个类 Point
,Color
和 ColorPoint
分别具备静态工厂方法 .from()
:
class Point { constructor(x, y) { this.x = x; this.y = y; } static from(other) { return new Point(other.x, other.y); } } class Color { constructor(name) { this.name = name; } static from(other) { return new Color(other.name); } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } static from(other) { return new ColorPoint( other.x, other.y, Color.from(other.color)); // (A) } }
在 A 行中,咱们再次使用递归复制。
这是 ColorPoint.from()
的工做方式:
const original = new ColorPoint(-1, 4, new Color('red')); const copy = ColorPoint.from(original); assert.deepEqual(copy, original);
只要咱们仅从共享状态读取,就不会有任何问题。在修改它以前,咱们须要经过复制(必要的深度)来“取消共享”。
防护性复制是一种在问题可能出现时始终进行复制的技术。其目的是确保当前实体(函数、类等)的安全:
请注意,这些措施能够保护咱们免受其余各方的侵害,同时也能够保护其余各方免受咱们的侵害。
下一节说明两种防护性复制。
请记住,在本文开头的例子中,咱们遇到了麻烦,由于 logElements()
修改了其参数 arr
:
function logElements(arr) { while (arr.length > 0) { console.log(arr.shift()); } }
让咱们在此函数中添加防护性复制:
function logElements(arr) { arr = [...arr]; // defensive copy while (arr.length > 0) { console.log(arr.shift()); } }
如今,若是在 main()
内部调用 logElements()
不会再引起问题:
function main() { const arr = ['banana', 'orange', 'apple']; console.log('Before sorting:'); logElements(arr); arr.sort(); // changes arr console.log('After sorting:'); logElements(arr); // (A) } main(); // Output: // 'Before sorting:' // 'banana' // 'orange' // 'apple' // 'After sorting:' // 'apple' // 'banana' // 'orange'
让咱们从 StringBuilder
类开始,该类不会复制它公开的内部数据(A行):
class StringBuilder { constructor() { this._data = []; } add(str) { this._data.push(str); } getParts() { // We expose internals without copying them: return this._data; // (A) } toString() { return this._data.join(''); } }
只要不使用 .getParts()
,一切就能够正常工做:
const sb1 = new StringBuilder(); sb1.add('Hello'); sb1.add(' world!'); assert.equal(sb1.toString(), 'Hello world!');
可是,若是更改了 .getParts()
的结果(A行),则 StringBuilder
会中止正常工做:
const sb2 = new StringBuilder(); sb2.add('Hello'); sb2.add(' world!'); sb2.getParts().length = 0; // (A) assert.equal(sb2.toString(), ''); // not OK
解决方案是在内部 ._data
被公开以前防护性地对它进行复制(A行):
class StringBuilder { constructor() { this._data = []; } add(str) { this._data.push(str); } getParts() { // Copy defensively return [...this._data]; // (A) } toString() { return this._data.join(''); } }
如今,更改 .getParts()
的结果再也不干扰 sb
的操做:
const sb = new StringBuilder(); sb.add('Hello'); sb.add(' world!'); sb.getParts().length = 0; assert.equal(sb.toString(), 'Hello world!'); // OK
咱们将首先探讨以破坏性方式和非破坏性方式更新数据之间的区别。而后将学习非破坏性更新如何避免数据改变。
咱们能够区分两种不一样的数据更新方式:
后一种方法相似于先复制而后破坏性地更改它,但二者同时进行。
这就是咱们破坏性地设置对象的属性 .city
的方式:
const obj = {city: 'Berlin', country: 'Germany'}; const key = 'city'; obj[key] = 'Munich'; assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});
如下函数以非破坏性的方式更改属性:
function setObjectNonDestructively(obj, key, value) { const updatedObj = {}; for (const [k, v] of Object.entries(obj)) { updatedObj[k] = (k === key ? value : v); } return updatedObj; }
它的用法以下:
const obj = {city: 'Berlin', country: 'Germany'}; const updatedObj = setObjectNonDestructively(obj, 'city', 'Munich'); assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'}); assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});
传播使 setObjectNonDestructively()
更加简洁:
function setObjectNonDestructively(obj, key, value) { return {...obj, [key]: value}; }
注意:setObject NonDestructively()
的两个版本都进行了较浅的更新。
如下是破坏性地设置数组元素的方式:
const original = ['a', 'b', 'c', 'd', 'e']; original[2] = 'x'; assert.deepEqual(original, ['a', 'b', 'x', 'd', 'e']);
非破坏性地更新数组要复杂得多。
function setArrayNonDestructively(arr, index, value) { const updatedArr = []; for (const [i, v] of arr.entries()) { updatedArr.push(i === index ? value : v); } return updatedArr; } const arr = ['a', 'b', 'c', 'd', 'e']; const updatedArr = setArrayNonDestructively(arr, 2, 'x'); assert.deepEqual(updatedArr, ['a', 'b', 'x', 'd', 'e']); assert.deepEqual(arr, ['a', 'b', 'c', 'd', 'e']);
.slice()
和扩展使 setArrayNonDestructively()
更加简洁:
function setArrayNonDestructively(arr, index, value) { return [ ...arr.slice(0, index), value, ...arr.slice(index+1)] }
注意:setArrayNonDestructively()
的两个版本都进行了较浅的更新。
到目前为止,咱们只是浅层地更新了数据。让咱们来解决深度更新。如下代码显示了如何手动执行此操做。咱们正在更改 name
和 employer
。
const original = {name: 'Jane', work: {employer: 'Acme'}}; const updatedOriginal = { ...original, name: 'John', work: { ...original.work, employer: 'Spectre' }, }; assert.deepEqual( original, {name: 'Jane', work: {employer: 'Acme'}}); assert.deepEqual( updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});
如下函数实现了通用的深度更新。
function deepUpdate(original, keys, value) { if (keys.length === 0) { return value; } const currentKey = keys[0]; if (Array.isArray(original)) { return original.map( (v, index) => index === currentKey ? deepUpdate(v, keys.slice(1), value) // (A) : v); // (B) } else if (typeof original === 'object' && original !== null) { return Object.fromEntries( Object.entries(original).map( (keyValuePair) => { const [k,v] = keyValuePair; if (k === currentKey) { return [k, deepUpdate(v, keys.slice(1), value)]; // (C) } else { return keyValuePair; // (D) } })); } else { // Primitive value return original; } }
若是咱们将 value
视为要更新的树的根,则 deepUpdate()
只会深度更改单个分支(A 和 C 行)。全部其余分支均被浅复制(B 和 D 行)。
如下是使用 deepUpdate()
的样子:
const original = {name: 'Jane', work: {employer: 'Acme'}}; const copy = deepUpdate(original, ['work', 'employer'], 'Spectre'); assert.deepEqual(copy, {name: 'Jane', work: {employer: 'Spectre'}}); assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}});
使用非破坏性更新,共享数据将变得毫无问题,由于咱们永远不会改变共享数据。 (显然,这只有在各方都这样作的状况下才有效。)
有趣的是,复制数据变得很是简单:
const original = {city: 'Berlin', country: 'Germany'}; const copy = original;
仅在必要时以及在咱们进行无损更改的状况下,才进行 original
的实际复制。
咱们能够经过使共享数据不变来防止共享数据发生改变。接下来,咱们将研究 JavaScript 如何支持不变性。以后,讨论不可变数据如何帮助共享可变状态。
JavaScript 具备三个级别的保护对象:
Preventing extensions 使得没法向对象添加新属性。可是,你仍然能够删除和更改属性。
Object.preventExtensions(obj)
Sealing 能够防止扩展,并使全部属性都没法配置(大约:您没法再更改属性的工做方式)。
Object.seal(obj)
Freezing 使对象的全部属性不可写后将其密封。也就是说,对象是不可扩展的,全部属性都是只读的,没法更改它。
Object.freeze(obj)
有关更多信息,请参见 “Speaking JavaScript”。
鉴于咱们但愿对象是彻底不变的,所以在本文中仅使用 Object.freeze()
。
Object.freeze(obj)
仅冻结 obj 及其属性。它不会冻结那些属性的值,例如:
const teacher = { name: 'Edna Krabappel', students: ['Bart'], }; Object.freeze(teacher); assert.throws( () => teacher.name = 'Elizabeth Hoover', /^TypeError: Cannot assign to read only property 'name'/); teacher.students.push('Lisa'); assert.deepEqual( teacher, { name: 'Edna Krabappel', students: ['Bart', 'Lisa'], });
若是要深度冻结,则须要本身实现:
function deepFreeze(value) { if (Array.isArray(value)) { for (const element of value) { deepFreeze(element); } Object.freeze(value); } else if (typeof value === 'object' && value !== null) { for (const v of Object.values(value)) { deepFreeze(v); } Object.freeze(value); } else { // Nothing to do: primitive values are already immutable } return value; }
回顾上一节中的例子,咱们能够检查 deepFreeze()
是否真的冻结了:
const teacher = { name: 'Edna Krabappel', students: ['Bart'], }; deepFreeze(teacher); assert.throws( () => teacher.name = 'Elizabeth Hoover', /^TypeError: Cannot assign to read only property 'name'/); assert.throws( () => teacher.students.push('Lisa'), /^TypeError: Cannot add property 1, object is not extensible$/);
用不可变的包装器包装可变的集合并提供相同的 API,但没有破坏性的操做。如今对于同一集合,咱们有两个接口:一个是可变的,另外一个是不可变的。当咱们具备要安全的公开内部可变数据时,这颇有用。
接下来展现了 Maps 和 Arrays 的包装器。它们都有如下限制:
类 ImmutableMapWrapper
为 map 生成包装器:
class ImmutableMapWrapper { constructor(map) { this._self = map; } } // Only forward non-destructive methods to the wrapped Map: for (const methodName of ['get', 'has', 'keys', 'size']) { ImmutableMapWrapper.prototype[methodName] = function (...args) { return this._self[methodName](...args); } }
这是 action 中的类:
const map = new Map([[false, 'no'], [true, 'yes']]); const wrapped = new ImmutableMapWrapper(map); // Non-destructive operations work as usual: assert.equal( wrapped.get(true), 'yes'); assert.equal( wrapped.has(false), true); assert.deepEqual( [...wrapped.keys()], [false, true]); // Destructive operations are not available: assert.throws( () => wrapped.set(false, 'never!'), /^TypeError: wrapped.set is not a function$/); assert.throws( () => wrapped.clear(), /^TypeError: wrapped.clear is not a function$/);
对于数组 arr
,常规包装是不够的,由于咱们不只须要拦截方法调用,并且还须要拦截诸如 arr [1] = true
之类的属性访问。 JavaScript proxies 使咱们可以执行这种操做:
const RE_INDEX_PROP_KEY = /^[0-9]+$/; const ALLOWED_PROPERTIES = new Set([ 'length', 'constructor', 'slice', 'concat']); function wrapArrayImmutably(arr) { const handler = { get(target, propKey, receiver) { // We assume that propKey is a string (not a symbol) if (RE_INDEX_PROP_KEY.test(propKey) // simplified check! || ALLOWED_PROPERTIES.has(propKey)) { return Reflect.get(target, propKey, receiver); } throw new TypeError(`Property "${propKey}" can’t be accessed`); }, set(target, propKey, value, receiver) { throw new TypeError('Setting is not allowed'); }, deleteProperty(target, propKey) { throw new TypeError('Deleting is not allowed'); }, }; return new Proxy(arr, handler); }
让咱们包装一个数组:
const arr = ['a', 'b', 'c']; const wrapped = wrapArrayImmutably(arr); // Non-destructive operations are allowed: assert.deepEqual( wrapped.slice(1), ['b', 'c']); assert.equal( wrapped[1], 'b'); // Destructive operations are not allowed: assert.throws( () => wrapped[1] = 'x', /^TypeError: Setting is not allowed$/); assert.throws( () => wrapped.shift(), /^TypeError: Property "shift" can’t be accessed$/);
若是数据是不可变的,则能够共享数据而没有任何风险。特别是无需防护性复制。
非破坏性更新是对不变数据的补充,使其与可变数据同样通用,但没有相关风险。
有几种可用于 JavaScript 的库,它们支持对不可变数据进行无损更新。其中流行的两种是:
List
,Map
,Set
和 Stack
。在其存储库中,Immutable.js 的描述为:
用于 JavaScript 的不可变的持久数据集,可提升效率和简便性。
Immutable.js 提供了不可变的数据结构,例如:
List
Map
(不一样于JavaScript的内置Map
)Set
(不一样于JavaScript的内置 Set
)Stack
在如下示例中,咱们使用不可变的 Map
:
import {Map} from 'immutable/dist/immutable.es.js'; const map0 = Map([ [false, 'no'], [true, 'yes'], ]); const map1 = map0.set(true, 'maybe'); // (A) assert.ok(map1 !== map0); // (B) assert.equal(map1.equals(map0), false); const map2 = map1.set(true, 'yes'); // (C) assert.ok(map2 !== map1); assert.ok(map2 !== map0); assert.equal(map2.equals(map0), true); // (D)
说明:
map0
的不一样版本 map1
,其中 true
映射到了 'maybe'
。map1
,并撤消在 A 行中所作的更改。.equals()
方法来检查是否确实撤消了更改。在其存储库中,Immer 库 的描述为:
经过更改当前状态来建立下一个不可变状态。
Immer 有助于非破坏性地更新(可能嵌套)普通对象和数组。也就是说,不涉及特殊的数据结构。
这是使用 Immer 的样子:
import {produce} from 'immer/dist/immer.module.js'; const people = [ {name: 'Jane', work: {employer: 'Acme'}}, ]; const modifiedPeople = produce(people, (draft) => { draft[0].work.employer = 'Cyberdyne'; draft.push({name: 'John', work: {employer: 'Spectre'}}); }); assert.deepEqual(modifiedPeople, [ {name: 'Jane', work: {employer: 'Cyberdyne'}}, {name: 'John', work: {employer: 'Spectre'}}, ]); assert.deepEqual(people, [ {name: 'Jane', work: {employer: 'Acme'}}, ]);
原始数据存储在 people
中。 produce()
为咱们提供了一个变量 draft
。咱们假设这个变量是 people
,并使用一般会进行破坏性更改的操做。 Immer 拦截了这些操做。代替变异draft
,它无损地改变 people
。结果由 modifiedPeople
引用。它是一成不变的。
传播:
Property attributes:
原型链: