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