本文是对shared-mutable-state这篇文章的一个解读分析,带你从头理解下共享可变数据的前世此生,这篇文章主要阐述了如下3个问题:javascript
共享可变数据就是超过2个以上的实例可以改变同一个数据,好比以下例子:html
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:'
复制代码
这个例子,main()
和logElements()
这两个函数都引用了数组arr
,其中logElements
方法体内经过调用shift
方法改变了arr
数组,致使了后面一个logElements
方法输出了空数组。java
文中说起了能够经过拷贝数据来解决这个问题,其中拷贝又分为浅拷贝和深拷贝:git
不论是浅拷贝仍是深拷贝,都有其使用的场景,好比若是一个对象或者数组层级很简单,值都是基本数据类型,那使用浅拷贝便可,相比深拷贝,代码执行效率更高且占用更少的内存。github
文中同时还提到了不少实现拷贝的方式,我下面来一一介绍下,其中不少你可能听过但你不必定了解的很透彻。正则表达式
首先说起的是经过...
扩展符的方式,拷贝实现方式以下:json
const copyOfObject = {...originalObject};
const copyOfArray = [...originalArray];
复制代码
然而经过扩展符实现拷贝存在几个局限性:数组
enumerable
(可枚举)属性可以被复制writable``configurable
等面对这些问题,文中也提供了一些解决的方式,感兴趣的能够查看原文。数据结构
接着,又介绍了经过对象的assign()
方法的方式:app
const copy1 = {...original};
const copy2 = Object.assign({}, original);
复制代码
Object.assign()
的使用方式以及局限性和扩展符差很少,但也有点区别:
Object.assign()
经过重新赋值来修改原始对象来建立拷贝对象...
扩展符经过使用现有对象的自身属性来建立新的普通对象文中还列举了一些解决浅拷贝缺陷的一些解决方式,众所周知,浅拷贝本质上是经过Object.defineProperties()
这个方法,直接在一个对象上定义新的属性或修改现有属性,并返回该对象来实现的,结合Object.getOwnPropertyDescriptors()
方法,咱们可以实现一种拷贝方式,能够轻松解决扩展符拷贝存在的局限性。
function copyAllOwnProperties(original) {
return Object.defineProperties(
{}, Object.getOwnPropertyDescriptors(original));
}
复制代码
经过上述方式,咱们如今不只可以拷贝本身的属性,同时非枚举一样可以被拷贝。
而后,文中接着介绍了几种深拷贝的方式
第一种就是手工拷贝,这种方式比较适合事先知道要拷贝的对象的数据结构
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);
复制代码
第二种就是经过JSON字符串进行转换,这种方式须要确保原始数据有着正确的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);
复制代码
同时,Symbol
和空值拷贝的时候都会被忽略
实现一个深拷贝函数
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;
}
}
复制代码
一个更加简洁的方式,若是拷贝的是对象,先经过Object.entries
获取全部自身可枚举属性的键值对数组,遍历键值对数组,而后经过Object.fromEntries
还原成对象。
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;
}
}
复制代码
还介绍了深拷贝class
类的方式
.clone()
方法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)
}
}
复制代码
这里须要注意的是,组合实例属性须要递归拷贝。
前面花了那么多时间来介绍拷贝的方式,那拷贝是如何帮助咱们共享可变状态的呢?其实只要控制好两个方面就行,一个是进入,一个是输出。
假如,有一个共享数据,在访问这个数据前(进入),咱们能够经过合适的拷贝这份数据,那无论咱们怎么操做拷贝后的数据,都不会影响原始数据。另外一个就是输出,假如咱们将输入暴露出去供别人使用,咱们不要直接暴露原始数据,能够暴露一份拷贝数据,这样无论他人如何操控这份暴露出去的数据,都不会影响咱们的原始数据。
拿最开始的例子来说:
初始是这样
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());
}
}
复制代码
如今再去执行后面的操做就不会发生数据为空的状况
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'
复制代码
同理,输出拷贝的数据也是同样的缘由。
数据的共享不只是获取,有时候咱们还会更新数据,那原则就是必须非破坏性的更新,不要破坏性的更新。
非破坏性就是指不要直接去操做原始数据,强行变化原始的数据结构,尽可能经过拷贝的方式,对原始数据侵入性最弱的方式去更新数据。
看以下例子:
直接赋值改变原始值的方式就输入破坏性的操做方式
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');
复制代码
文中还说起了非破坏性的更新数组,经过深拷贝非破坏性的更新数据的方式,感兴趣的能够查看原文。
为何要经过非破坏性的方式更新数据呢?
由于经过非破坏性更新,共享数据就不会由于破坏性更新数据致使数据先后不一致的问题,同时也利于数据回溯。
既然无论怎样都不直接操纵原始数据,这里就引伸出了如今愈来愈流行的一个概念,对原始数据的一个新称呼 不可变数据
那如何使数据不可变呢?javascript
提供了3种方式:
那如何实现深度冻结?
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;
}
复制代码
文中最后说起了两个提供了建立不可变数据以及非暴力更新数据的能力的库
List
Map
Set
Stack
的不可变数据结构经过阅读全文,咱们知道了在共享同一份数据,为什么要保持数据不可变,这也是为何使用Redux
的进行状态管理的时候,不容许咱们直接改变数据,以及咱们通常会配套使用Immutable.js
的真正缘由。Redux
只有一份所有的状态,那么多组件引用它,若是不保持数据的纯洁性,数据管理就会变得异常困难,遇到问题也会难以追溯。
最后,但愿这篇文章可以提高你对数据管理的深度认知以及扩展管理数据的一些方式。