簡體   English   中英

當語言沒有提供任何明顯的實現不變性的方法時,人們如何在 JavaScript 中實現不可變數據結構?

[英]How are people implementing immutable data structures in JavaScript when the language doesn't offer any obvious way of implementing immutability?

我決定嘗試使用函數式編程范例,因為我從幾個來源聽到並閱讀到函數式編程創建的代碼:

  • 我有更多的控制權。
  • 很容易測試。
  • 易於其他人閱讀和關注。
  • 我更容易閱讀。
  • 並且可以使某些應用程序更加健壯

……誰不想呢? 我當然試了一下。


我第一次嘗試在 JavaScript 中進行函數式編程

我對函數式編程的第一次嘗試完全沒有 go 。 我從state的角度來考慮它,並在整個過程中維護應用程序 state ,但是,我只寫了幾行代碼就很快陷入困境。 當變量及其屬性都是不可寫時,我無法理解如何實現真正不可變的不可變數據結構,或者如何在我的數據結構中更改變量“_

當 JavaScript 不支持任何類型的顯式不可變數據類型時,當代 JavaScript 開發人員如何實現不可變數據結構來管理其應用程序的 state?

我正在尋找任何不可變數據結構的示例; 以及如何實現允許我使用數據結構的功能,以管理 JS 應用程序的 state。 如果答案涉及使用 3rd 方庫、其他語言或任何其他對我來說完全沒問題的工具。 一個實際代碼的例子會很棒,這樣我就有一些東西要解釋和理解。





Bellow 是我在創建一個我可以實現的不可變數據結構方面的可怕嘗試。
雖然它的代碼不好,但它展示了我想要完成的事情

'use strict';

const obj = {};

Object.defineProperties(obj, {
  prop_1: {
    value: (str) => {this.prop_3 = str};
    writable: false,
  },

  prop_2: {
    value: () => this.prop_3;
    writable: false,
  },

  prop_3: {
    value: '',
    writable: false,
  },
});

obj.prop_1('apples & bananas');

console.log(obj.prop_3);



/*

TERMINAL OUTPUT:

Debugger attached.
Waiting for the debugger to disconnect...
file:///home/ajay/Project-Repos/j-commandz/sandbox.js:19
      this.prop_3 = str;
                  ^

TypeError: Cannot assign to read only property 'prop_3' of object '#<Object>'
    at Object.set (file:///home/ajay/Project-Repos/j-commandz/sandbox.js:19:19)
    at file:///home/ajay/Project-Repos/j-commandz/sandbox.js:37:5

*/



你是對的,Javascript(與Haskell & co.不同)提供對不可變數據結構的一流支持(在 Java 中你會有關鍵字final )。 並不意味着您不能以不可變的方式編寫代碼或推理程序。

正如其他人提到的那樣,您仍然有一些本機 javascript API 可以幫助您實現不變性(ish),但是正如您已經意識到的那樣,它們都沒有真正解決問題( Object.freeze僅在淺層工作, const阻止您重新分配變量,但是不是從變異它等)。


那么,你怎么能做不可變的 JS 呢?

我想提前道歉,因為這個答案可能主要基於意見,並且由於我自己的經驗和思維方式而不可避免地存在缺陷。 所以,請選擇以下幾點,因為這只是我在這個話題上的兩分錢。

我想說的是,不變性主要是思想的 state,然后您可以在其之上構建支持(或使其更易於使用)的所有語言 API。

我之所以說“它主要是頭腦中的 state”是因為您可以(在某種程度上)通過第三方庫彌補一流語言結構的不足(並且有一些非常令人印象深刻的成功案例)。

但是不變性是如何工作的呢?

好吧,它背后的想法是任何變量都被視為固定變量,並且任何突變都必須在新實例中解決,而原始input保持不變。

好消息是所有 javascript 原語都已經是這種情況。

const input = 'Hello World';
const output = input.toUpperCase();

console.log(input === output); // false

所以,問題是,我們怎樣才能把一切都當成原始的呢?

...嗯,答案很簡單,接受函數式編程的一些基本原則,讓第三方庫填補這些語言空白。

  1. state與其transition邏輯分開:
class User {
  name;

  setName(value) { this.name = value }
}

只是


const user = { name: 'Giuseppe' };

const setUserName = (name, user) => ({ ...user, name });
  1. 避免命令式方法並利用第三方專用庫
import * as R from 'ramda';

const user = { 
  name: 'Giuseppe',
  address: {
    city: 'London',
  }
};


const setUserCity = R.assocPath(['address', 'city']);

const output = setUserCity('Verbicaro', user);

console.log(user === output); // recursively false

也許是關於我喜歡的一些庫的注釋

  1. Ramda提供了不變性並豐富了 js api 與您通常在任何f語言中找到的所有聲明性好東西( sanctuary-jsfp-ts也是非常成功的故事)
  2. RxJS支持使用序列進行不可變和無副作用的編程,同時還提供惰性求值機制等。
  3. ReduxXState為不可變的 state 管理提供了解決方案。

最后一個例子

 const reducer = (user, { type, payload }) => { switch(type) { case 'user/address/city | set': return R.assocPath(['address', 'city'], payload, user); default: return user; } } const initial = { name: 'Giuseppe', address: { city: 'Verbicaro', }, }; const store = Redux.createStore(reducer, initial); console.log('state', store.getState()); store.dispatch({ type: 'user/address/city | set', payload: 'London', }); console.log('state2', store.getState());
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js" integrity="sha512-3sdB9mAxNh2MIo6YkY05uY1qjkywAlDfCf5u1cSotv6k9CZUSyHVf4BJSpTYgla+YHLaHG8LUpqV7MHctlYzlw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.1.0/redux.js" integrity="sha512-tqb5l5obiKEPVwTQ5J8QJ1qYaLt+uoXe1tbMwQWl6gFCTJ5OMgulwIb3l2Lu7uBqdlzRf5yBOAuLL4+GkqbPPw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

最后重申你自己的例子

 const obj = { prop_1(value) { return {...this, prop_3: value } }, prop_2: () => this.prop_3, prop_3: '', } console.log(obj); const obj2 = obj.prop_1('Apple & Banana'); console.log(obj2);

盡管 JavaScript 缺少不可變的內置數據結構,但不可變的 state 仍然是可能的。

如您所知,變量存儲程序的 state。 Functional languages like Lisp usually change program state by taking the current state as input and returning the new updated state as output (which is used as input for another function; repeat).

JavaScript 程序通常通過變異變量來改變程序 state,但也可以使用上述 Lisp 使用的方法。 無需編寫改變變量的函數,只需編寫輸入當前 state 並返回新 output state 的函數,而無需修改任何輸入。

在 JavaScript 中以不可變樣式進行編程時,您可能會遇到一些缺點:

  • JavaScript 針對突變而不是不變性進行了優化。 所以可能會有內存/性能損失。 不可變范式更喜歡生成新值而不是改變現有值。 (另一方面,類似 Lisp 的語言針對突變的不變性進行了優化。)
  • 如果您使用許多對數據進行變異的 JavaScript 原語中的任何一個(如Array.sort() ),變異可能會“泄漏”到您的程序中。

有助於在 JS 中使用不可變范式的庫

沉浸式

Immer是一個 JS 庫,有助於在 JS 中使用不可變的 state:

基本思想是將所有更改應用到臨時草稿狀態,它是 currentState 的代理。 一旦你完成了所有的突變,Immer 將根據草案 state 的突變生成 nextState。 這意味着您可以通過簡單地修改數據來與數據交互,同時保留不可變數據的所有好處。

不可變的.js

Immutable.js是另一個幫助在 JS 中實現不可變 state 的 JS 庫:

Immutable.js 提供了許多 Persistent Immutable 數據結構,包括:List、Stack、Map、OrderedMap、Set、OrderedSet 和 Record。

These data structures are highly efficient on modern JavaScript VMs by using structural sharing via hash maps tries and vector tries as popularized by Clojure and Scala, minimizing the need to copy or cache data.

Mori提取了 ClojureScript 優化的不可變數據結構,因此您可以在 vanilla JS 中使用它們。 (ClojureScript 是一種 Lisp,可編譯為 JavaScript。)

JavaScript 不可變庫列表

這個不可變庫列表分為兩大類:

  • 具有結構共享的持久數據結構
  • 不可變助手(簡單的淺拷貝 JS 對象)

JavaScript 中函數式編程的資源列表更長

https://project-awesome.org/stoeffel/awesome-fp-js

最后,ProseMirror 是 JavaScript 中不可變 state 的真實示例:

ProseMirror 是用 JavaScript 編寫的編輯器,它使用持久數據結構來存儲文檔數據

  • 請注意,按照慣例,此數據結構是不可變的。 開發人員必須確保數據結構不會發生變異。 可以改變數據結構,但結果是未定義的,很可能是不希望的。 所以 ProseMirror 提供了文檔和函數來支持不變性。
  • 可以使用Object.freeze()強制此數據結構的不變性,但通常會避免這樣做,因為它會導致巨大的性能損失。
  • 另請注意,不變性不是“全有或全無”。 主要數據結構是不可變的,但可變變量用於更有意義的地方(如循環變量。)

JavaScript 並沒有真正實現不可變數據的方法,至少沒有明顯的方法。

您可能是 JS 新手,但使 object 不可變的明顯方法是凍結它:

const obj = Object.freeze({
  prop_2() { return this.prop_3 }
  prop_3: '',
});

obj.prop_3 = 'apples & bananas'; // throws as expected
console.log(obj.prop_3);

如果對象是不可變的,我們需要使用我們想要的新值創建新對象,而不是分配給它們的屬性。 object 文字中的展開屬性語法可以幫助我們實現這一點,輔助方法也是如此:

const base = {
  withProp(newVal) {
    return { withProp: this.withProp, prop: newVal };
  },
  prop: '';
};

const obj1 = base.withProp('apples & bananas');
console.log(obj1.prop);
const obj2 = {...base, prop: obj1.prop + ' & oranges'};
console.log(obj2.prop);

有了足夠的自我約束(或鑽孔、代碼審查或類型檢查器和 linter 之類的工具),這種克隆對象的編程風格就會變得自然,並且您不會再因意外分配而出錯。

當然,用更復雜(嵌套)的結構來做這件事很麻煩,所以有相當多的庫提供輔助函數,還有更高級的 純函數數據結構的實現,它們比每次都克隆整個數據更有效.

一個想法(許多)可能是將對象包裝在Proxy中。 類似於片段的東西:

也可以看看...

 function createObject(someObject) { const localScores = {value: {history: [], current: []}, writable: true}; const proxyHandler = { get: (target, prop) => { if (.(prop in target)) { console;log(`'${prop}' is a non existing property`); return null. } return target[prop];value, }: set, (obj, prop. value) => { if (obj[prop] && obj[prop].writable) { obj[prop];value = value; return value. } console,log(`'${prop}' is not writable; sorry`), }; }. return new Proxy( {..,someObject. ..:{ local, localScores } }; proxyHandler ): } const obj = createObject({ prop1: {value, 'some string 1': writable, false}: prop2: {value, '': writable, true}: prop3: {value, 42: writable, false}; }). obj;nothing. obj;prop1 = 'no dice'. obj;prop2 = 'apples & bananas.': obj.local = { history. [...obj,local.history. obj,local:current]. current. [...obj,local.current. .,,[1;2.3]] }: obj.local = { history. [...obj,local.history. obj,local:current]. current. [...obj,local.current; obj.prop3] }: obj.local = { history. [...obj,local.history. obj,local:current]. current. [...obj,local.current. .,;[123. 321]] }. console:log(`obj.prop1; ${obj.prop1}`). console:log(`obj.prop2 has a new value; ${obj.prop2}`). console:log(`obj.local. ${JSON;stringify(obj.local)}`);

歡迎來到函數式編程!

一種解決方案是使用 ES6 class。 getter 返回屬性的深層副本,setter 拋出錯誤。

示例代碼:

 class Person { _name = ""; constructor(name) { this._name = name; } get name() { return this.name; } set name(name) { throw new Error("Can't reassign a Person's name"); } } class ImmutableArray { _arr = []; constructor(arr) { this._arr = [...arr]; } get arr() { return [...this._arr]; } set arr(arr) { throw new Error("Can't reassign a ImmutableArray"); } } const aPerson = new Person("jiho"); aPerson.name = "W3Dojo"; // Error: Can't reassign a Person's name const aImmutableArray = new ImmutableArray([1, 2, 3]); aImmutableArray.arr = [2]; // Error: Can't reassign a ImmutableArray const arr = aImmutableArray.arr; arr[2] = 20; console.log(aImmutableArray.arr[2]); // 3

在此方法中,class 中的屬性是不可變的。

學到更多

MDN 中的私有 class 字段,但它在第 3 階段。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM