首頁 > 軟體

JavaScript資料型別對函數語言程式設計的影響範例解析

2023-02-25 06:02:04

前言

本篇文章是JavaScript 函數語言程式設計 學習系列第二篇,感興趣也可以先去看第一篇:

前文 一文理解JavaScript中的函數語言程式設計的概念 中寫了函數語言程式設計的概念,本篇文章繼上文之後,來梳理 JavaScript 資料型別對函數語言程式設計的影響。

函數語言程式設計程式設計的核心就是 純函數 和隔離 副作用 ,為了讓 純函數 保持純粹,純函數的引數或者內部參照的外部資料應該是不可變資料。但 JavaScript 中的資料型別並不是都是不可變的,而資料型別的可變性,很有可能讓 純函數 變的不純。

因此,本篇文章的目的有兩點:

  • 探索 JavaScript 的資料型別來了解的可變資料的根源。
  • JavaScript 的可變資料資料是怎麼讓 純函數 變得不純的?
  • 如何解決 可變資料 的影響?

JavaScript中 的資料型別中的可變資料

在 JavaScript 中,資料型別有以下 8 種:

  • null
  • undefined
  • boolean
  • number
  • symbol -- 在 es6 中被加入
  • bigint -- es6+ 被加入
  • object

注意點:

在 JavaScript 中,變數是沒有型別的,值才有型別。變數可以在任何時候,持有任何值。

原始型別(基本型別)

上面 8 中型別除了 object ,其他都是原始型別,原始型別儲存的都是值,其特點有兩點:

  • 沒有方法可以直接呼叫
  • 原始型別的資料是不可被改變的,改變一個變數的值,並不是把值改變了,而是讓變數擁有新的值。

注意點:

  • '1'.toString()或者false.toString()等可以用的原因是被強制轉換成了 String 型別也就是物件型別,所以可以呼叫 toString 函數。
  • 對於null來說,很多人會認為它是個物件型別,其實是錯誤的。typeof null 會輸出 object,這只是 JS 存在的一個悠久 Bug,而且好像永遠不會也不會被修復,因為有太多已經存在的 web 的內容依存著這個 bug。注: 在 JS 的最初版本中使用的是 32 位系統,為了效能考慮使用低位儲存變數的型別資訊,000開頭代表是物件,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部型別判斷程式碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

物件型別(參照型別)

而除了原始型別,剩下的 object 就是物件型別,和原始型別的不同點在於:原始型別儲存的是值,物件型別儲存的是地址。

經典範例:

var c = 1;
var d = c;
d = 2;
console.log(c === d) // false
var a = {
    name: "張三",
    age: 20
}
var b = a;
b.age = 21;
console.log(a.age === b.age) // true

範例中把變數 a 的值給到了變數 b , b 修改了age 屬性,但是 a 的 age 屬性也跟著變了,是因為 var b = a 是 a 把物件的參照地址賦值給 b ,這時候 a 和 b 指向的是記憶體中的同一個資料。

而 c 給 d 的是值,並不是一個參照,相當於複製了一份資料。

因此可以知道原型型別的資料是不可變的,而物件型別的資料是可變的。

JavaScript 為何能會讓純函數變得不純?

JavaScript 中的物件型別的資料是可變,而可變性,就代表了不確定性,純函數 中使用了不確性的資料就會導致不純,因為其違背了 純函數 的特徵:不受外界影響,不影響外界。

下面來看一個例子:

A 同學寫了這麼一段程式碼,初始化生成了一個 “zhangsan” 使用者。

export const defaultUserInfo = {
    name: "名稱",
    age: 20,
    hobby: ["玩耍"]
};
export function initUser(userTemplate, name, age) {
    const newUser = userTemplate;
    newUser.name = name;
    newUser.age = age;
    return newUser;
}
const zhangsan = userInit(userDefaultInfo, "zhangsan", 21);

然後 B 同學在開發其他頁面的時候,看到有初始化使用者資訊的方法,然後直接複製過去,初始化了一個 “lisi” 使用者。

import { defaultUserInfo, initUser } from "xxx模組"。
const lisi = userInit(userDefaultInfo, "lisi", 21);

檢測的時候看到自己初始化的使用者資訊正確的就沒有去檢查之前 A 同學的是否是正確的,上線後發現所有的使用者都變成了 lisi 。因為 userDefaultInfo 是一個參照型別,userInit(userDefaultInfo, "xxx", xx) 操作的都是記憶體中的同一個物件。其原因就是因為 A 和 B 開發者犯了一個錯誤,把可變資料傳遞到了 userInit 函數內部進行處理,哪怕進行了淺層拷貝,也出現了問題。究其原因還是因為給函數傳遞進去了一個 可變資料。

我們校驗一個 純函數 有效性的關鍵依據,永遠是“針對已知的輸入,能否給出符合預期的輸出”,而上面例子中 initUser 函數沒有違背這個規則,但是在可變資料的影響下,讓它產生了 副作用,對外界已有的資料造成了影響。

如何解決可變資料的影響?

資料拷貝

從使用函數方的角度來看,既然造成這個問題的原因是因為傳遞進去的資料是 可變資料 ,那麼我就複製一份資料傳遞給函數內部使用,隨便你怎麼修改,都不會影響外界其他資料。

比如我們使用前面例子中的 initUser 函數時,先拷貝一份資料:

function copyFunc(object) {
    return JSON.parse(JSON.string(object));
}
const zhangsan = userInit(copyFunc(userDefaultInfo), "zhangsan", 21);
const lisi = userInit(copyFunc(userDefaultInfo), "lisi", 21);
console.log(zhangsan.name === lisi.name); // false

進行拷貝後的資料傳遞給 userInit 函數,就不會出現問題了。這裡的 copyFunc 只能針對部分資料型別,對不少型別是不支援的,具體可以去看一下 關於JSON.parse(JSON.stringify(obj))實現深拷貝應該注意的坑 這篇文章。

從被呼叫函數方來看,在使用 object 型別資料時,函數內部儘量不要去修改外界 object 資料(通過引數傳遞,或者直接使用外界的物件都不建議去修改),修改之前可以拷貝一份再修改。

比如:

export function initUser(userTemplate, name, age) {
    const newUser = copyFunc(userTemplate);
    newUser.name = name;
    newUser.age = age;
    return newUser;
}

使用不可變資料方案

拷貝的資料比較大的時候,會出現效能問題,因此出現了不可變資料的方案。

現在不可變資料常見的有兩種: Immutable.js 和 immer.js 。它們都能實現在運算元據後,返回新的一個資料,而不影響之前的資料。

Immutable.js 實現了持久化資料結構,實現原理說明(參照於immutable.js 和 immer):

  • 使用舊資料建立新資料時,要保證舊資料同時可用且不變。同時為了避免 deepCopy 把所有節點都複製一遍帶來的效能問題,immutable 使用了結構共用方式,即如果物件樹中的一個節點改變,只修改這個節點和受它影響的父節點,其他節點共用。
  • immutable-js 使用了另一套資料結構 api,它會將原生資料型別都轉化為 immutable-js 內部物件。

因此 Immutable.js 需要嚴格使用它自定義的運算元據的方法才行。

immer.js 利用了 es6 的 Proxy 來進行對資料操作的攔截實現,具體原理可去 剖析 Immer.js 工作原理與設計模式 這裡看看,也可以去網上查詢。

總結

  • 分析 JavaScript中 的資料型別中的可變資料根源:Object 資料結構。
  • 探索了其可變資料資料是怎麼對 純函數 造成的影響:Object 資料的不確定性。
  • 分析瞭如何解決 可變資料 的影響:深拷貝 和使用 不可變資料結構.

參考:

以上就是JavaScript資料型別對函數語言程式設計的影響範例解析的詳細內容,更多關於JavaScript資料型別函數語言程式設計的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com