- 作者:Chidume Nnamdi
- 英文原文:blog.bitsrc.io/understandi…
[譯] 理解 JavaScript Mutation 突變和 PureFunction 純函式
不可變性、純函式、副作用,狀態可變這些單詞我們幾乎每天都會見到,但我們幾乎不知道他們是如何工作的,以及他們是什麼,他們為軟體開發帶來了什麼好處。
在這篇文章中,我們將深入研究所有這些,以便真正瞭解它們是什麼以及如何利用它們來提高我們的Web應用程式的效能。
Javascript:原始資料型別和引用資料型別
我們將首先了解JS如何維護以及訪問到我們的資料型別。
在JS中,有原始資料型別和引用資料型別。原始資料型別由值引用,而非原始/引用資料型別指向記憶體地址。
原始資料型別是:
- Boolean
- Number
- String
- Null
- Undefined
- Symbol
引用資料型別:
- Object
- Arrays
當我們寫原始資料型別時是這個樣子:
let one = 1
在呼叫堆疊中,one
變數直接指向值 1:
Call Stack
#000 one -> | 1 |
#001 | |
#002 | |
#003 | |
複製程式碼
如果我們改變這個值:
let one = 1
one = 3
複製程式碼
變數 one
的記憶體地址 #000 原本儲存 1 這個值,會直接變成 3
但是,如果我們像這樣寫一個引用資料型別:
let arr = {
one: 1
}
複製程式碼
或:
let arr = new Object()
arr.one = 1
複製程式碼
JS將在記憶體的堆中建立物件,並將物件的記憶體地址儲存在堆上:
Call Stack Heap
#000 arr -> | #101 | #101 | one: 1 |
#001 | | #102 | |
#002 | | #103 | |
#003 | | #104 | |
複製程式碼
看到 arr
不直接儲存物件,而是指向物件的記憶體位置(#101)。與直接儲存其值的原始資料型別不同。
let arr = { one: 1 }
// arr holds the memory location of the object {one: 1}
// `arr` == #101
let one = 1;
// `one` a primitive data type holds the value `1`
// one == 1
複製程式碼
如果我們改變 arr
中的屬性,如下所示:
arr.one = 2
那麼基本上我們就是在告訴程式更改 arr
對物件屬性值的指向。如果你對 C/C++ 等語言的指標和引用比較熟悉,那麼這些你都會很容易理解。
傳遞引用資料型別時,你只是在傳遞其記憶體位置的遞值,而不是實際的值。
function chg(arg) {
//arg points to the memory address of { one: 1 }
arg.one = 99
// This modification will affect { one: 1 } because arg points to its memory address, 101
}
let arr = { one: 1 }
// address of `arr` is `#000`
// `arr` contains `#101`, adrress of object, `{one: 1}` in Heap
log(arr); // { one: 1 }
chg(arr /* #101 */)
// #101 is passed in
log(arr) // { one: 99 }
// The change affected `arr`
複製程式碼
譯者注:arr 本身的記憶體地址是 #000;arr 其中儲存了一個地址 #101;這個地址指向物件 {one:1};在呼叫 chg 函式的時候,那麼修改 arg 屬性 one 就會修改 arr 對應的 #101 地址指向的物件 {one:1}
因為引用資料型別儲存的是記憶體地址,所以對他的任何修改都會影響到他指向的記憶體。
如果我們傳入一個原始資料型別:
function chg(arg) {
arg++
}
let one = 1; // primitive data types holds the actual value of the variable.
log(one) // 1
chg(one /* 1 */)
// the value of `one` is passed in.
log(one) // one is still `1`. No change because primitives only hold the value
複製程式碼
譯者注:不像原始資料型別,他的值是多少就是多少如果修改了這個值,那麼直接修改所在記憶體對應的這個值
狀態突變和不可變性
在生物學領域,我們知道 DNA 以及 DNA 突變。DNA 有四個基本元素,分別是 ATGC。這些生成了編碼資訊,在人體內產生一種蛋白質。
ATATGCATGCGATA
||||||||||||||
TACGAGCTAGGCTA
|
|
v
AProteinase
Information to produce a protein (eg, insulin etc)
複製程式碼
上述DNA鏈編碼資訊以產生可用於骨結構比對的AP蛋白酶蛋白。
如果我們改變DNA鏈配對,即使是一對:
ATATGCATGCGATA
||||||||||||||
TACGAGCTAGGCTA
|
v
GTATGCATGCGATA
||||||||||||||
TACGAGCTAGGCTA
複製程式碼
DNA將產生不同的蛋白質,因為產生蛋白質AP蛋白酶的資訊已經被篡改。因此產生了另一種蛋白質,其可能是良性的或在某些情況下是有毒的。
GTATGCATGCGATA
||||||||||||||
TACGAGCTAGGCTA
|
|
V
Now produces _AProtienase
複製程式碼
我們稱這種變化突變
或DNA突變
。
突變引起DNA狀態的改變。
而對於 JS 來說,引用資料型別(陣列,物件)都被稱為資料結構。這些資料結構儲存資訊,以操縱我們的應用程式。
let state = {
wardens: 900,
animals: 800
}
複製程式碼
上面名為 state 的物件儲存了 Zoo 應用程式的資訊。如果我們改變了 animals 屬性的值:
let state = {
wardens: 900,
animals: 800
}
state.animals = 90
複製程式碼
我們的 state 物件會儲存或編碼一個新的資訊:
state = {
wardens: 900,
animals: 90
}
複製程式碼
這就叫突變 mutation
我們的 state 從:
state = {
wardens: 900,
animals: 800
}
複製程式碼
變為:
state = {
wardens: 900,
animals: 90
}
複製程式碼
當我們想要保護我們的 state 時候,這就需要用到不可變性了 immutability。為了防止我們的 state 物件發生變化,我們必須建立一個 state 物件的新例項。
function bad(state) {
state.prp = 'yes'
return state
}
function good(state) {
let newState = { ...state }
newState.prp = 'yes'
return newState
}
複製程式碼
不可變性使我們的應用程式狀態可預測,提高我們的應用程式的效能速率,並輕鬆跟蹤狀態的變化。
純函式和副作用
純函式是接受輸入並返回值而不修改其範圍之外的任何資料的函式(副作用)。它的輸出或返回值必須取決於輸入/引數,純函式必須返回一個值。
譯者注:純函式必須要滿足的條件:不產生副作用、返回值只取決於傳入的引數,純函式必須返回一個值
function impure(arg) {
finalR.s = 90
return arg * finalR.s
}
複製程式碼
上面的函式不是純函式,因為它修改了其範圍之外的狀態 finalR.s
。
function impure(arg) {
let f = finalR.s * arg
}
複製程式碼
上面的函式也不是純函式,因為雖然它沒有修改任何外部狀態,但它沒有返回值。
function impure(arg) {
return finalR.s * 3
}
複製程式碼
上面的函式是不純的,雖然它不影響任何外部狀態,但它的輸出返回 finalR.s * 3
不依賴於輸入 arg
。純函式不僅必須返回一個值,還必須依賴於輸入。
function pure(arg) {
return arg * 4
}
複製程式碼
上面的函式才是純函式。它不會對任何外部狀態產生副作用,它會根據輸入返回輸出。
能夠帶來的好處
就個人而言,我發現的唯一能夠讓人理解的好處是 mutation tracking
變異追蹤。
知道何時渲染你的狀態是非常重要的事情。很多 JS 框架設計了不錯的方法來檢測何時去渲染其狀態。但是最重要的是,要知道在首次渲染完畢後,何時觸發再渲染 re-render
。這就被稱為變異追蹤了。這需要知道什麼時候狀態被改變了或者說變異了。以便去觸發再渲染 re-render
。
於我們已經實現了不變性,我們確信我們的應用程式狀態不會在應用程式中的任何位置發生變異,況且純函式完全準尋其處理邏輯和原則(譯者注:不會產生副作用)。這就很容易看出來到底是哪裡出現變化了(譯者注:反正不是純函式也不是 immutable 變數)。
let state = {
add: 0,
}
funtion render() {
//...
}
function effects(state,action) {
if(action == 'addTen') {
return {...state, add: state.add + 10}
}
return state;
}
function shouldUpdate(s) {
if(s === state){
return false
}
return true
}
state = effects(state, 'addTen')
if(shouldUpdate(state)) {
render();
}
複製程式碼
這裡有個小程式。這裡有個 state 物件,物件只有一個屬性 add。render 函式正常渲染程式的屬性。他並不會在程式的任何改變時每次都觸發渲染 state 物件,而是先檢查 state 物件是否改變。
就像這樣,我們有一個 effects 函式和一個純函式,這兩個函式都用來去修改我們的 state 物件。你會看到它返回了一個新的 state 物件,當要更改狀態時返回新狀態,並在不需要修改時返回相同的狀態。
因此,我們有一個shouldUpdate函式,它使用===運算子檢查舊狀態和新狀態是否相同。如果它們不同,則呼叫render函式,以更新新狀態。
結論
我們研究了 Web 開發中這幾個最常見的術語,並展示了它們的含義以及它們的用途。如果你付諸實踐,這將是非常有益的。
如果有任何對於這篇文章的問題,如我應該增加、修改或刪除,請隨時評論、傳送電子郵件或直接 DM 我。乾杯 ?