javascript
从JS对象开始,谈一谈“不可变数据”和函数式编程
文章轉載自:https://segmentfault.com/a/1190000008780076
作為前端開發者,你會感受到JS中對象(Object)這個概念的強大。我們說“JS中一切皆對象”。最核心的特性,例如從String,到數組,再到瀏覽器的APIs,“對象”這個概念無處不在。在這里你可以了解到JS Objects中的一切。
同時,隨著React的強勢崛起,不管你有沒有關注過這個框架,也一定聽說過一個概念—不可變數據(immutable.js)。究竟什么是不可變數據?這篇文章會從JS源頭—對象談起,讓你逐漸了解這個函數式編程里的重要概念。
JS中的對象是那么美妙:我們可以隨意復制他們,改變并刪除他們的某項屬性等。但是要記住一句話:
“伴隨著特權,隨之而來的是更大的責任。”
(With great power comes great responsibility)
的確,JS Objects里概念太多了,我們切不可隨意使用對象。下面,我就從基本對象說起,聊一聊不可變數據和JS的一切。
這篇文章緣起于Daniel Leite在2017年3月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];這是我們常用的“刪除數組某一項”的操作。好吧,他一點問題也沒有。
問題其實出現在“濫用”可變性上,這樣會給你的程序帶來“副作用”。先不必關心什么是“副作用”,他又是一個函數式編程的概念。
我們先來看一下代碼實例:
const student1 = {school: 'Baidu',name: 'HOU Ce',birthdate: '1995-12-15', }const changeStudent = (student, newName, newBday) => {const newStudent = student;newStudent.name = newName;newStudent.birthdate = newBday;return newStudent; }const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10'); // both students will have the name properties console.log(student1, student2); // Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"} // Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}我們發現,盡管創建了一個新的對象student2,但是老的對象student1也被改動了。這是因為JS對象中的賦值是“引用賦值”,即在賦值過程中,傳遞的是在內存中的引用(memory reference)。具體說就是“棧存儲”和“堆存儲”的問題。具體圖我就不畫了,理解不了可以單找我。
不可變數據的強大和實現
我們說的“不可變”,其實是指保持一個對象狀態不變。這樣做的好處是使得開發更加簡單,可回溯,測試友好,減少了任何可能的副作用。
函數式編程認為:
只有純的沒有副作用的函數,才是合格的函數。
好吧,現在開始解釋下“副作用”(Side effect):
在計算機科學中,函數副作用指當調用函數時,除了返回函數值之外,還對主調用函數產生附加的影響。例如修改全局變量(函數外的變量)或修改參數。
-維基百科
函數副作用會給程序設計帶來不必要的麻煩,給程序帶來十分難以查找的錯誤,并降低程序的可讀性。嚴格的函數式語言要求函數必須無副作用。
那么我們避免副作用,創建不可變數據的主要實現思路就是:一次更新過程中,不應該改變原有對象,只需要新創建一個對象用來承載新的數據狀態。
我們使用純函數(pure functions)來實現不可變性。純函數指無副作用的函數。
那么,具體怎么構造一個純函數呢?我們可以看一下代碼實現,我對上例進行改造:
需要注意的是,我使用了ES6中的解構(destructuring)賦值。
這樣,我們達到了想要的效果:根據參數,產生了一個新對象,并正確賦值,最重要的就是并沒有改變原對象。
創建純函數,過濾副作用
現在,我們知道了“不可變”到底指的是什么。接下來,我們就要分析一下純函數應該如何實現,進而生產不可變數據。
其實創建不可變數據方式有很多,在使用原生JS的基礎上,我推薦的方法是使用現有的Objects API和ES6當中的解構賦值(上例已經演示)。現在看一下Objects.assign的實現方式:
const student1 = {school: "Baidu", name: 'HOU Ce',birthdate: '1995-12-15', }const changeStudent = (student, newName, newBday) => Object.assign({}, student, {name: newName, birthdate: newBday})const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');// both students will have the name properties console.log(student1, student2); // Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15"}; // Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"};同樣,如果是處理數組相關的內容,我們可以使用:.map, .filter或者.reduce去達成目標。這些APIs的共同特點就是不會改變原數組,而是產生并返回一個新數組。這和純函數的思想不謀而合。
但是,再說回來,使用Object.assign請務必注意以下幾點:
1)他的復制,是將所有可枚舉屬性,復制到目標對象。換句話說,不可枚舉屬性是無法完成復制的。
2)對象中如果包含undefined和null類型內容,會報錯。
3)最重要的一點:Object.assign方法實行的是淺拷貝,而不是深拷貝。
第三點很重要,也就是說,如果源對象某個屬性的值是對象,那么目標對象拷貝得到的是這個屬性對象的引用。這也就意味著,當對象存在嵌套時,還是有問題的。比如下面代碼:
const student1 = {school: "Baidu", name: 'HOU Ce',birthdate: '1995-12-15',friends: {friend1: 'ZHAO Wenlin',friend2: 'CHENG Wen'} }const changeStudent = (student, newName, newBday, friends) => Object.assign({}, student, {name: newName, birthdate: newBday})const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');// both students will have the name properties console.log(student1, student2); // Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15", friends: Object} // Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10", friends: Object}student2.friends.friend1 = 'MA xiao'; console.log(student1.friends.friend1); // "MA xiao"對student2 friends列表當中的friend1的修改,同時也影響了student1 friends列表當中的friend1。
JS本身的蒼白無力VS不可變數據類庫
以上,我們分析了純JS如何實現不可變數據。這樣處理帶來的一個負面影響在于:一些經典APIs都是shallow處理,比如上文提到的Object.assign就是典型的淺拷貝。如果遇到嵌套很深的結構,我們就需要手動遞歸。這樣做呢,又會存在性能上的問題。
比如我自己動手用遞歸實現一個深拷貝,需要考慮循環引用的“死環”問題,另外,當使用大規模數據結構時,性能劣勢盡顯無疑。我們熟悉的jquery extends方法,某一版本(最新版本情況我不太了解)的實現是進行了三層拷貝,也沒有達到完備的deep copy。
總之,實現不可變數據,我們必然要關心性能問題。針對于此,我推薦一款已經“大名鼎鼎”的——immutable.js類庫來處理不可變數據。
他的實現既保證了不可變性,又保證了性能大限度優化。原理很有意思,下面這段話,我摘自camsong前輩的文章:
Immutable實現的原理是Persistent Data Structure(持久化數據結構),也就是使用舊數據創建新數據時,要保證舊數據同時可用且不變。
同時為了避免deepCopy把所有節點都復制一遍帶來的性能損耗,Immutable使用了Structural Sharing(結構共享),即如果對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。
感興趣的讀者可以深入研究下,這是很有意思的。如果有需要,我也愿意再寫一篇immutable.js源碼分析。
總結
我們使用JavaScript操縱對象,這樣的方式很簡單便捷。但是,這樣操控的基礎是在JavaScript靈活機制的熟練掌握上。不然很容易使你“頭大”。
在我開發的百度某部門私信項目中,因為使用了React+Redux技術棧,并且數據結構較為負責,所以我也采用了immutable.js實現。
最后,在前端開發中,函數式編程越來越熱,并且在某種程度上已經取代了“過程式”編程和面向對象思想。
我的感想是在某些特定的場景下,不要畏懼變化,擁抱未來。
就像我很喜歡的葡萄牙詩人安德拉德一首詩中那樣說的:
我同樣不知道什么是海,
赤腳站在沙灘上,
急切地等待著黎明的到來。
總結
以上是生活随笔為你收集整理的从JS对象开始,谈一谈“不可变数据”和函数式编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RF - 完整用例展示
- 下一篇: Go语言中如何进行测试