摘要:為什么要有不可變數(shù)據(jù)首先,不可變數(shù)據(jù)類型是源于函數(shù)式編程中的,是一條必備的準(zhǔn)則。另外在中的廣泛應(yīng)用,也讓函數(shù)式編程火熱,而函數(shù)式編程最重要的原則之一就是不可變數(shù)據(jù),所以你在使用的時候,改變必須返回新的。
不可變數(shù)據(jù) 引入
我是通過使用 React 才去關(guān)注 immutable data 這個概念的。事實上,你去搜 immutable 的 JS 相關(guān)文章,也基本都是近兩年的,大概是隨著 React 的推廣才備受關(guān)注。但是這篇文章不會去介紹 React 是如何在意 immutable data 的,而是從原生 JS,寫一些自己的思考。
個人 blog,歡迎 star。https://github.com/sunyongjian
可變/不可變對象可變對象是一個可在其創(chuàng)建后修改狀態(tài)的對象,而不可變對象則是創(chuàng)建之后,不能再修改狀態(tài),對其任何刪改操作,都應(yīng)返回一個新的對象。
一個例子開始:
var x = { a: 1 } var y = x; x.a = 2; console.log(y); //{ a: 2 }
這在我們剛開始學(xué) js 的時候就知道了,js 中的對象都是參考(reference)類型,x = y 是對象賦值引用,兩者共用一個對象的空間,所以 x 改動了,y 自然也改變。
數(shù)組也是一樣的:
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 都是可變數(shù)據(jù)。
不過字符串和數(shù)字就不會。
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 的賦值操作,都不會影響之前的數(shù)據(jù)。
為什么要有不可變數(shù)據(jù)首先,不可變數(shù)據(jù)類型是源于函數(shù)式編程中的,是一條必備的準(zhǔn)則。函數(shù)式對數(shù)據(jù)處理的時候,通過把問題抽象成一個個的純函數(shù),每個純函數(shù)的操作都會返回新的數(shù)據(jù)類型,都不會影響之前的數(shù)據(jù),保證了變量/參數(shù)的不可變性,增加代碼可讀性。
另外,js 中對象可變的好處可能是為了節(jié)約內(nèi)存,相比字符串、數(shù)字,它承載的數(shù)據(jù)量更大更多,不可變帶來每次操作都要產(chǎn)生新的對象,新的數(shù)據(jù)結(jié)構(gòu),這與 js 設(shè)計之初用來做網(wǎng)頁中表單驗證等簡單操作是有悖的。而且,我們最開始也確實感受到可變帶來的便捷,但是反之它帶來的副作用遠(yuǎn)超過這種便捷,程序越大代碼的可讀性,復(fù)雜度也越來越高。
舉一個栗子:
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 已經(jīng)把原始數(shù)據(jù) data 改變了,當(dāng)我再去使用的時候,已經(jīng)是被污染的數(shù)據(jù)。這個栗子其實沒有那么的典型,因為沒有結(jié)合業(yè)務(wù),但是也可以說明一些問題,就是可變數(shù)據(jù)帶來的不確定影響。這兩個函數(shù)都是有“副作用”的,即對傳入數(shù)據(jù)做了修改,當(dāng)你調(diào)用兩次 addAge,得到的卻是兩個完全不同的結(jié)果,這顯然不是我們想要的。如果遵循不可變數(shù)據(jù)的原則,每次對原始數(shù)據(jù)結(jié)構(gòu)的修改、操作,都返回新的數(shù)據(jù)結(jié)構(gòu),就不會出現(xiàn)這種情況。關(guān)于返回新的數(shù)據(jù)結(jié)構(gòu),就需要用到數(shù)據(jù)拷貝。
數(shù)據(jù)拷貝之前 y = x 這樣的操作,顯然是無法完成數(shù)據(jù)拷貝的,這只是賦值引用,為了避免這種對象間的賦值引用,我們應(yīng)該更多的使用 const 定義數(shù)據(jù)對象,去避免這種操作。
而我們要給新對象(數(shù)據(jù))創(chuàng)建一個新的引用,也就是需要數(shù)據(jù)拷貝。然而對象的數(shù)據(jù)結(jié)構(gòu)通常是不同的(嵌套程度等),在數(shù)據(jù)拷貝的時候,需要考慮到這個問題,如果對象是深層次的
比較一下 JS 中幾種原生的拷貝方法,了解他們能實現(xiàn)的程度。
Object.assign像這樣:
const x = { a: 1 }; const y = Object.assign({}, x); x.a = 11; console.log(y); // { a: 1 }
誠然,此次對 y 的賦值,再去改變 x.a 的時候,y.a 并沒有發(fā)生變化,保持了不變性。你以為就這么簡單嗎?看另一個栗子:
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,而當(dāng)?shù)谝粚拥?value 出現(xiàn) object/array 的時候,它還是會做賦值引用操作,即 x,y 的 b 共用一個 {c: 2} 的地址。還有幾個方法也是這樣的。
Object.freezeconst 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,看起來是真的“凍結(jié)”了,不可變了,其實效果是一樣的,為了效率,做的淺拷貝。
deconstruction 解構(gòu)const x = { a: 1, b: { c: 2 } }; const y = { ...x }; x.a = 11; console.log(y); x.b.c = 22; console.log(y);
es6 中的新方法,解構(gòu)。數(shù)組也一樣:
const x = [1, 2, [3, 4]]; const y = [...x]; x[2][0] = 33; console.log(y); // [1, 2, [33, 4]]
同樣是淺拷貝。
JS 原生對象的方法,是沒有給我們提供深拷貝功能的。
deep-clone如何去做深拷貝
原生
拿上面的栗子來說,我們?nèi)崿F(xiàn)深拷貝。
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 } }
不過這只是嵌套不多的時候,而更深層次的,就需要更復(fù)雜的操作了。實際上,deep-clone 確實沒有一個統(tǒng)一的方法,需要考慮的地方挺多,比如效率,以及是否應(yīng)用場景(是否每次都需要 deep-clone)。還有在 js 中,還要加上 hasOwnProperty 這樣的判斷。寫個簡單的方法:
function clone(obj) { // 類型判斷。 isActiveClone 用來防止重復(fù) 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
通常實現(xiàn) deep-clone 的庫:lodash,$.extend(true, )... 目前最好用的是 immutable.js。 關(guān)于 immutable 的常用用法,之后會整理一下。
數(shù)據(jù)持久化不變性可以讓數(shù)據(jù)持久化變得容易。當(dāng)數(shù)據(jù)不可變的時候,我們的每次操作,都不會引起初始數(shù)據(jù)的改變。也就是說在一定時期內(nèi),這些數(shù)據(jù)是永久存在的,而你可以通過讀取,實現(xiàn)類似于“回退/切換快照”般的操作。這是我們從函數(shù)式編程來簡單理解這個概念,而不涉及硬盤存儲或者數(shù)據(jù)庫存儲的概念。
首先,無論數(shù)據(jù)結(jié)構(gòu)的深淺,每次操作都對整個數(shù)據(jù)結(jié)構(gòu)進(jìn)行完整的深拷貝,效率會很低。這就牽扯到在做數(shù)據(jù)拷貝的時候,利用數(shù)據(jù)結(jié)構(gòu),做一些優(yōu)化。例如,我們可以觀察某次操作,到底有沒有引起深層次數(shù)據(jù)結(jié)構(gòu)的變化,如果沒有,我們是不是可以只做部分改變,而沒變化的地方,還是可以共用的。這就是部分持久化。我知道的 immutable 就是這么做的,兩個不可變數(shù)據(jù)是會共用某部分的。
思考
js 的對象天生是可變的?
我覺得作者應(yīng)該是設(shè)計之初就把 js 作為一種靈活性較高的語言去做的,而不可變數(shù)據(jù)涉及到數(shù)據(jù)拷貝的算法問題,深拷貝是可以實現(xiàn)的,但是如何最優(yōu)、效率最高的實現(xiàn)拷貝,并保持?jǐn)?shù)據(jù)不可變。這個地方是可以繼續(xù)研究的。
為什么不可變數(shù)據(jù)的熱度越來越高?
隨著 js 應(yīng)用的場景越來越多,業(yè)務(wù)場景也越來越復(fù)雜,一些早就沉淀下來的編程思維,也被引入 js 中,像 MVC,函數(shù)式等等。經(jīng)典的編程思想,設(shè)計模式永遠(yuǎn)都是不過時的,而不可變數(shù)據(jù)結(jié)構(gòu)也是如此。而我覺得真正讓它受關(guān)注的,還是 React 的推出,因為 React 內(nèi)部就是通過 state/props 比較(===)去判斷是否 render 的,三個等號的比較就要求新的 state 必須是新的引用。另外 Redux 在 React 中的廣泛應(yīng)用,也讓函數(shù)式編程火熱,而函數(shù)式編程最重要的原則之一就是不可變數(shù)據(jù),所以你在使用
Redux 的時候,改變 store 必須返回新的 state。所以,React-Redux 全家桶,讓 immutable data 備受關(guān)注,而 immutable,就是目前最好的實現(xiàn)方案。
最后之后會探究 immutable data 在 React 中的重要性,包括 diff,re-render,redux。自然而然也可以總結(jié)出這方面的 React 性能優(yōu)化。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/89779.html
摘要:所有變量的類型在編譯時已知在程序運行之前,因此編譯器也可以推導(dǎo)出所有表達(dá)式的類型。像變量的類型一樣,這些聲明是重要的文檔,對代碼讀者很有用,并由編譯器進(jìn)行靜態(tài)檢查。對象類型的值對象類型的值是由其類型標(biāo)記的圓。 大綱 1.編程語言中的數(shù)據(jù)類型2.靜態(tài)與動態(tài)數(shù)據(jù)類型3.類型檢查4.易變性和不變性5.快照圖6.復(fù)雜的數(shù)據(jù)類型:數(shù)組和集合7.有用的不可變類型8.空引用9.總結(jié) 編程語言中的數(shù)據(jù)...
摘要:關(guān)心性能的情況下,需要手動設(shè)置這時就需要引入狀態(tài)管理庫。現(xiàn)在常用的狀態(tài)管理庫有和,本文會重點介紹,然后會將和進(jìn)行對比,最后展望下未來的狀態(tài)管理方面趨勢。如果在任何地方都修改可觀察數(shù)據(jù),將導(dǎo)致頁面狀態(tài)難以管理。 React 是一個專注于視圖層的庫。React 維護了狀態(tài)到視圖的映射關(guān)系,開發(fā)者只需關(guān)心狀態(tài)即可,由 React 來操控視圖。 在小型應(yīng)用中,單獨使用 React 是沒什么問題...
摘要:性能當(dāng)字符串是不可變時,字符串常量池才有意義。字符串常量池的出現(xiàn),可以減少創(chuàng)建相同字面量的字符串,讓不同的引用指向池中同一個字符串,為運行時節(jié)約很多的堆內(nèi)存。 在學(xué)習(xí)Java的過程中,我們會被告知 String 被設(shè)計成不可變的類型。為什么 String 會被 Java 開發(fā)者有如此特殊的對待?他們的設(shè)計意圖和設(shè)計理念到底是什么?因此,我?guī)е韵氯齻€問題,對 String 進(jìn)行剖析: ...
摘要:除此之外,還可以通過函數(shù)獨立指定紋理的每個的級別。這種繪圖時檢查可能代價很高,而使用不可變紋理可以避免這種情形。不可變紋理使用不可變紋理,可以減少上文中提到的因檢查而導(dǎo)致的性能開銷。不可變紋理指的是紋理的一種分配方式,而不是值紋理的內(nèi)容。 紋理背景知識 在WebGL1中,紋理包括2D紋理和立方體紋理,在實際的使用中,如果紋理的圖片是寬和高是2的冪,可以自動生成紋理的mipmap。除此之...
閱讀 3021·2021-11-22 12:06
閱讀 603·2021-09-03 10:29
閱讀 6553·2021-09-02 09:52
閱讀 2023·2019-08-30 15:52
閱讀 3417·2019-08-29 16:39
閱讀 1195·2019-08-29 15:35
閱讀 2068·2019-08-29 15:17
閱讀 1425·2019-08-29 11:17