不可變數據

不可變數據

引入

我是經過使用 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 = strb = 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 中幾種原生的拷貝方法,瞭解他們能實現的程度。

Object.assign

像這樣:

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} 的地址。還有幾個方法也是這樣的。

Object.freeze

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,看起來是真的「凍結」了,不可變了,其實效果是同樣的,爲了效率,作的淺拷貝。

deconstruction 解構

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 原生對象的方法,是沒有給咱們提供深拷貝功能的。

deep-clone

如何去作深拷貝

  • 原生

拿上面的栗子來講,咱們去實現深拷貝。

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 性能優化。

相關文章
相關標籤/搜索