做爲前端開發者,你會感覺到JS中對象(Object)這個概念的強大。咱們說「JS中一切皆對象」。最核心的特性,例如從String,到數組,再到瀏覽器的APIs,「對象」這個概念無處不在。在這裏你能夠了解到JS Objects中的一切。javascript
同時,隨着React的強勢崛起,無論你有沒有關注過這個框架,也必定據說過一個概念—不可變數據(immutable.js)。究竟什麼是不可變數據?這篇文章會從JS源頭—對象談起,讓你逐漸瞭解這個函數式編程裏的重要概念。前端
JS中的對象是那麼美妙:咱們能夠隨意複製他們,改變並刪除他們的某項屬性等。可是要記住一句話:java
「伴隨着特權,隨之而來的是更大的責任。」
(With great power comes great responsibility)jquery
的確,JS Objects裏概念太多了,咱們切不可隨意使用對象。下面,我就從基本對象提及,聊一聊不可變數據和JS的一切。git
這篇文章緣起於Daniel Leite在2017年3月16日的新鮮出爐文章:Things you should know about Objects and Immutability in JavaScript,我進行了大體翻譯並進行大範圍「改造」,同時改寫了用到的例子,進行了大量更多的擴展。程序員
不可變數據實際上是函數式編程相關的重要概念。相對的,函數式編程中認爲可變性是萬惡之源。可是,爲何會有這樣的結論呢?github
這個問題可能不少程序員都會有。其實,若是你的代碼邏輯可變,不要驚慌,這並非「政治錯誤」的。好比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)來實現不可變性。純函數指無反作用的函數。
那麼,具體怎麼構造一個純函數呢?咱們能夠看一下代碼實現,我對上例進行改造:
const student1 = {
school: "Baidu",
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => {
return {
...student, // 使用解構
name: newName, // 覆蓋name屬性
birthdate: newBday // 覆蓋birthdate屬性
}
}
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"}複製代碼
須要注意的是,我使用了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如何實現不可變數據。這樣處理帶來的一個負面影響在於:一些經典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實現。
最後,在前端開發中,函數式編程愈來愈熱,而且在某種程度上已經取代了「過程式」編程和麪向對象思想。
個人感想是在某些特定的場景下,不要畏懼變化,擁抱將來。
就像我很喜歡的葡萄牙詩人安德拉德一首詩中那樣說的:
我一樣不知道什麼是海,
赤腳站在沙灘上,
急切地等待着黎明的到來。
Happy Coding!
PS:百度知識搜索部大前端繼續招兵買馬,高級工程師、實習生職位均有,產品、運營一樣崗位多多。有意向者火速聯繫。。。