我是經過使用 React 纔去關注 immutable data 這個概念的。事實上,你去搜 immutable 的 JS 相關文章,也基本都是近兩年的,大概是隨着 React 的推廣才備受關注。可是這篇文章不會去介紹 React 是如何在乎 immutable data 的,而是從原生 JS,寫一些本身的思考。git
我的 blog,歡迎 star。https://github.com/sunyongjianes6
可變對象是一個可在其建立後修改狀態的對象,而不可變對象則是建立以後,不能再修改狀態,對其任何刪改操做,都應返回一個新的對象。github
一個例子開始:算法
var x = { a: 1 } var y = x; x.a = 2; console.log(y); //{ a: 2 }
這在咱們剛開始學 js 的時候就知道了,js 中的對象都是參考(reference)類型,x = y
是對象賦值引用,二者共用一個對象的空間,因此 x 改動了,y 天然也改變。數據庫
數組也是同樣的:編程
var ary = [1, 2, 3]; var list = ary; ary.push(4); console.log(list); // [1, 2, 3, 4]
在 JS 中,objects, arrays,functions, classes, sets, maps 都是可變數據。
不過字符串和數字就不會。redux
var str = 'hello world'; var sub = str; str = str.slice(0, 5); console.log(sub); // 'hello world' var a = 1; var b = a; a += 2; console.log(b); // 1
像這樣,sub = str
,b = a
的賦值操做,都不會影響以前的數據。設計模式
首先,不可變數據類型是源於函數式編程中的,是一條必備的準則。函數式對數據處理的時候,經過把問題抽象成一個個的純函數,每一個純函數的操做都會返回新的數據類型,都不會影響以前的數據,保證了變量/參數的不可變性,增長代碼可讀性。數組
另外,js 中對象可變的好處多是爲了節約內存,相比字符串、數字,它承載的數據量更大更多,不可變帶來每次操做都要產生新的對象,新的數據結構,這與 js 設計之初用來作網頁中表單驗證等簡單操做是有悖的。並且,咱們最開始也確實感覺到可變帶來的便捷,可是反之它帶來的反作用遠超過這種便捷,程序越大代碼的可讀性,複雜度也愈來愈高。性能優化
舉一個栗子:
const data = { name: 'syj', age: 24, hobby: 'girl', location: 'beijing' } // 有一個改變年齡的方法 function addAge(obj) { obj.age += 1; return obj; } // 一個改變地址的方法 function changeLocation(obj, v) { obj.location = v; return obj; } // 這兩個方法我期待的是獲得只改變想改變的屬性的 data console.log(addAge(data)); console.log(changeLocation(obj, 'shanghai'));
但實際上 addAge 已經把原始數據 data 改變了,當我再去使用的時候,已是被污染的數據。這個栗子其實沒有那麼的典型,由於沒有結合業務,可是也能夠說明一些問題,就是可變數據帶來的不肯定影響。這兩個函數都是有「反作用」的,即對傳入數據作了修改,當你調用兩次 addAge,獲得的倒是兩個徹底不一樣的結果,這顯然不是咱們想要的。若是遵循不可變數據的原則,每次對原始數據結構的修改、操做,都返回新的數據結構,就不會出現這種狀況。關於返回新的數據結構,就須要用到數據拷貝。
以前 y = x
這樣的操做,顯然是沒法完成數據拷貝的,這只是賦值引用,爲了不這種對象間的賦值引用,咱們應該更多的使用 const
定義數據對象,去避免這種操做。
而咱們要給新對象(數據)建立一個新的引用,也就是須要數據拷貝。然而對象的數據結構一般是不一樣的(嵌套程度等),在數據拷貝的時候,須要考慮到這個問題,若是對象是深層次的
比較一下 JS 中幾種原生的拷貝方法,瞭解他們能實現的程度。
像這樣:
const x = { a: 1 }; const y = Object.assign({}, x); x.a = 11; console.log(y); // { a: 1 }
誠然,這次對 y 的賦值,再去改變 x.a 的時候,y.a 並無發生變化,保持了不變性。你覺得就這麼簡單嗎?看另外一個栗子:
const x = { a: 1, b: { c: 2 } }; const y = Object.assign({}, x); x.b.c = 22; console.log(y); // { a: 1, b: { c: 22}}
對 x 的操做,使 y.b.c 也變成了 22。爲何?由於 Object.assign 是淺拷貝,也就是它只會賦值對象第一層的 kv,而當第一層的 value 出現 object/array 的時候,它仍是會作賦值引用操做,即 x,y 的 b 共用一個 {c: 2}
的地址。還有幾個方法也是這樣的。
const x = { a: 1, b: { c: 2 } }; const y = Object.freeze(x); x.a = 11; console.log(y); x.b.c = 22; console.log(y); // { a: 1, b: { c: 22}}
freeze,看起來是真的「凍結」了,不可變了,其實效果是同樣的,爲了效率,作的淺拷貝。
const x = { a: 1, b: { c: 2 } }; const y = { ...x }; x.a = 11; console.log(y); x.b.c = 22; console.log(y);
es6 中的新方法,解構。數組也同樣:
const x = [1, 2, [3, 4]]; const y = [...x]; x[2][0] = 33; console.log(y); // [1, 2, [33, 4]]
一樣是淺拷貝。
JS 原生對象的方法,是沒有給咱們提供深拷貝功能的。
如何去作深拷貝
原生
拿上面的栗子來講,咱們去實現深拷貝。
const x = { a: 1, b: { c: 2 } }; const y = Object.assign({}, x, { b: Object.assign({}, x.b) }) x.b.c = 22; console.log(y); // { a: 1, b: { c: 2 } }
不過這只是嵌套很少的時候,而更深層次的,就須要更復雜的操做了。實際上,deep-clone 確實沒有一個統一的方法,須要考慮的地方挺多,好比效率,以及是否應用場景(是否每次都須要 deep-clone)。還有在 js 中,還要加上 hasOwnProperty 這樣的判斷。寫個簡單的方法:
function clone(obj) { // 類型判斷。 isActiveClone 用來防止重複 clone,效率問題。 if (obj === null || typeof obj !== 'object' || 'isActiveClone' in obj) { return obj; } //多是 Date 對象 const result = obj instanceof Date ? new Date(obj) : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { obj['isActiveClone'] = null; result[key] = clone(obj[key]); delete obj['isActiveClone']; } } return result; } var x = { a: 1, b: 2, c: { d: 3 } } console.log(clone(x));
JSON
最簡單,偷懶的一種方式,JSON 的序列化再反序列化。
const y = JSON.parse(JSON.stringify(x));
普通的 string,number,object,array 都是能夠作深拷貝的。不過這個方法比較偷懶,是存在坑的,好比不支持 NaN,正則,function 等。舉個栗子:
const x = { a: function() { console.log('aaa') }, b: NaN, } const y = JSON.parse(JSON.stringify(x)); console.log(y.b); y.a()
試一下就知道了。
Library
一般實現 deep-clone 的庫:lodash
,$.extend(true, )
... 目前最好用的是 immutable.js
。 關於 immutable 的經常使用用法,以後會整理一下。
不變性可讓數據持久化變得容易。當數據不可變的時候,咱們的每次操做,都不會引發初始數據的改變。也就是說在必定時期內,這些數據是永久存在的,而你能夠經過讀取,實現相似於「回退/切換快照」般的操做。這是咱們從函數式編程來簡單理解這個概念,而不涉及硬盤存儲或者數據庫存儲的概念。
首先,不管數據結構的深淺,每次操做都對整個數據結構進行完整的深拷貝,效率會很低。這就牽扯到在作數據拷貝的時候,利用數據結構,作一些優化。例如,咱們能夠觀察某次操做,到底有沒有引發深層次數據結構的變化,若是沒有,咱們是否是能夠只作部分改變,而沒變化的地方,仍是能夠共用的。這就是部分持久化。我知道的 immutable 就是這麼作的,兩個不可變數據是會共用某部分的。
js 的對象天生是可變的?
我以爲做者應該是設計之初就把 js 做爲一種靈活性較高的語言去作的,而不可變數據涉及到數據拷貝的算法問題,深拷貝是能夠實現的,可是如何最優、效率最高的實現拷貝,並保持數據不可變。這個地方是能夠繼續研究的。
爲何不可變數據的熱度愈來愈高?
隨着 js 應用的場景愈來愈多,業務場景也愈來愈複雜,一些早就沉澱下來的編程思惟,也被引入 js 中,像 MVC,函數式等等。經典的編程思想,設計模式永遠都是不過期的,而不可變數據結構也是如此。而我以爲真正讓它受關注的,仍是 React 的推出,由於 React 內部就是經過 state/props 比較(===
)去判斷是否 render 的,三個等號的比較就要求新的 state 必須是新的引用。另外 Redux 在 React 中的普遍應用,也讓函數式編程火熱,而函數式編程最重要的原則之一就是不可變數據,因此你在使用
Redux 的時候,改變 store 必須返回新的 state。因此,React-Redux 全家桶,讓 immutable data 備受關注,而 immutable,就是目前最好的實現方案。
以後會探究 immutable data 在 React 中的重要性,包括 diff,re-render,redux。天然而然也能夠總結出這方面的 React 性能優化。