天天看點

從JS對象開始,談一談究竟什麼是“不可變資料”和函數式程式設計

作為前端開發者,你會感受到js中對象(object)這個概念的強大。我們說“js中一切皆對象”。最核心的特性,例如從string,到數組,再到浏覽器的apis,對象這個概念無處不在。這裡你可以了解到js objects中的一切。

同時,随着react的強勢崛起,不管你有沒有關注過這個架構,也一定聽說過一個概念—不可變資料(immutable.js)。究竟什麼是不可變資料?這篇文章會從js源頭—對象談起,讓你逐漸了解這個函數式程式設計裡的重要概念。

js中的對象是那麼美妙:我們可以随意複制他們,改變并删除他們的某項屬性等。但是要記住一句話:

“伴随着特權,随之而來的是更大的責任。” (with great power comes great responsibility)

的确,js objects裡概念太多了,我們切不可随意使用對象。下面,我就從基本對象說起,聊一聊不可變資料和js的一切。

這篇文章緣起于daniel leite在本月16日的文章:things you should know about objects and

immutability in javascript,我進行了翻譯,并改寫了用到的例子,以及進行了大量更多的擴充。

不可變資料其實是函數式程式設計相關的重要概念。相對的,函數式程式設計中認為可變性是萬惡之源。但是,為什麼會有這樣的結論呢?

這個問題可能很多程式員都會有。其實,如果你的代碼邏輯可變,這并不是“政治錯誤”的。比如js中的數組操作,很對都會對原數組進行直接改變,這當然并沒有什麼問題。比如:

let arr = [1, 2, 3, 4, 5]; arr.splice(1, 1); // 傳回[2]; console.log(arr); // [1, 3, 4, 5];

這是我們常用的“删除數組某一項”的操作。好吧,他一點問題也沒有。

問題其實出現在“濫用”可變性上,這樣會給你的程式帶來“副作用”。先不必關心什麼是“副作用”,他又是一個函數式程式設計的概念。

我們先來看一下代碼執行個體:

我們發現,盡管建立了一個新的對象student2,但是老的對象student1也被改動了。這是因為js對象中的指派是“引用指派”,即在指派過程中,傳遞的是在記憶體中的引用(memory

reference)。具體說就是“棧存儲”和“堆存儲”的問題。具體圖我就不畫了,了解不了可以單找我。

我們說的“不可變”,其實是指保持一個對象狀态不變。這樣做的好處是使得開發更加簡單,可回溯,測試友好,減少了任何可能的副作用。

函數式程式設計認為:

隻有純的沒有副作用的函數,才是合格的函數。

好吧,現在開始解釋下“副作用”(side effect):在計算機科學中,函數副作用指當調用函數時,除了傳回函數值之外,還對主調用函數産生附加的影響。例如修改全局變量(函數外的變量)或修改參數。

函數副作用會給程式設計帶來不必要的麻煩,給程式帶來十分難以查找的錯誤,并降低程式的可讀性。嚴格的函數式語言要求函數必須無副作用。

那麼我們避免副作用,建立不可變資料的主要實作思路就是:一次更新過程中,不應該改變原有對象,隻需要新建立一個對象用來承載新的資料狀态。

我們使用純函數(pure functions)來實作不可變性。純函數指無副作用的函數。

那麼,具體怎麼構造一個純函數呢?我們可以看一下代碼實作,我對上例進行改造:

需要注意的是,我使用了es6中的解構(destructuring)指派。

這樣,我們達到了想要的效果:根據參數,産生了一個新對象,并正确指派,最重要的就是并沒有改變原對象。

現在,我們知道了“不可變”到底指的是什麼。接下來,我們就要分析一下純函數應該如何實作,進而生産不可變資料。

其實建立不可變資料方式有很多,在使用原生js的基礎上,我推薦的方法是使用現有的objects api和es6當中的解構指派(上例已經示範)。現在看一下objects.assign的實作方式:

同樣,如果是處理數組相關的内容,我們可以使用:.map, .filter或者.reduce去達成目标。這些apis的共同特點就是不會改變原數組,而是産生并傳回一個新數組。這和純函數的思想不謀而合。

但是,再說回來,使用object.assign請務必注意以下幾點:

1)他的複制,是将所有可枚舉屬性,複制到目标對象。換句話說,不可枚舉屬性是無法完成複制的。

2)對象中如果包含undefined和null類型内容,會報錯。

3)最重要的一點:object.assign方法實行的是淺拷貝,而不是深拷貝。

第三點很重要,也就是說,如果源對象某個屬性的值是對象,那麼目标對象拷貝得到的是這個對象的引用。這也就意味着,當對象存在嵌套時,還是有問題的。比如下面代碼:

對student2 friends清單當中的friend1的修改,同時也影響了student1 friends清單當中的friend1。

以上,我們分析了純js如何實作不可變資料。這樣處理帶來的一個負面影響在于:一些經典apis都是shallow處理,比如上文提到的object.assign。如果遇到嵌套很深的結構,我們就需要手動遞歸。這樣做呢,又會存在性能上的問題。

比如我自己動手用遞歸實作一個深拷貝,需要考慮循環引用的“死環”問題,另外,當使用大規模資料結構時,性能劣勢盡顯無疑。我們熟悉的jquery

extends方法,某一版本(最新版本情況我不太了解)的實作是進行了三層拷貝,也沒有達到完備的deep copy。

總之,實作不可變資料,我們必然要關心性能問題。針對于此,我推薦一款已經“大名鼎鼎”的——immutable.js類庫來處理不可變資料。

他的實作原理很有意思,下面這段話,我摘自camsong前輩的文章:

immutable實作的原理是persistent data structure(持久化資料結構),也就是使用舊資料建立新資料時,要保證舊資料同時可用且不變。 同時為了避免deepcopy把所有節點都複制一遍帶來的性能損耗,immutable使用了structural sharing(結構共享),即如果對象樹中一個節點發生變化,隻修改這個節點和受它影響的父節點,其它節點則進行共享。

感興趣的讀者可以深入研究下,這是很有意思的。

我們使用javascript操縱對象,這樣的方式很簡單便捷。但是,這樣操控的基礎是在javascript靈活的對象機制的熟練掌握上。不然很容易使你“頭大”。

在我開發的百度某部門私信項目中,因為使用了react+redux技術棧,并且資料結構較為負責,也采用了immutable.js實作。

最後,在前端開發中,函數式程式設計越來越熱,并且在某種程度上已經取代了“過程式”程式設計和面向對象思想。

我的感想是在某些特定的場景下,不要畏懼變化,擁抱未來。

就像我很喜歡的葡萄牙詩人安德拉德一首詩中那樣說的:

我同樣不知道什麼是海, 赤腳站在沙灘上, 急切地等待着黎明的到來。

繼續閱讀