本文是對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
只有一份所有的狀態,那麼多組件引用它,若是不保持數據的純潔性,數據管理就會變得異常困難,遇到問題也會難以追溯。
最後,但願這篇文章可以提高你對數據管理的深度認知以及擴展管理數據的一些方式。