当 JavaScript 没有提供任何明显的实现不变性的方法时,人们如何在 JavaScript 中实现不可变的数据结构?

JΛY*_*ÐΞV 2 javascript functional-programming immutability

我决定尝试使用函数式编程范式,因为我从几个来源听说并阅读了函数式编程创建的代码:

  • 我有更多的控制权。
  • 很容易测试。
  • 易于其他人阅读和关注。
  • 对我来说更容易阅读。

……谁不想要?我当然试过了。


我第一次尝试用 JavaScript 进行函数式编程

我对函数式编程的第一次尝试并不顺利。我从state 的角度考虑它,并在整个过程中维护应用程序状态,但是,我写了几行代码后很快就卡住了。我无法解决如何实现真正不可变的不可变数据结构,或者如何在我的数据结构中更改变量,当这些变量及其属性都是不可写时"_

当 JavaScript 不提供任何类型的显式不可变数据类型或支持时,当代 JavaScript 开发人员如何实现不可变数据结构来管理其应用程序的状态?

我正在寻找任何不可变数据结构的示例;以及如何实现允许我使用数据结构的功能来管理 JS 应用程序的状态。如果答案涉及使用 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

*/



Run Code Online (Sandbox Code Playgroud)

Hit*_*nds 6

你是对的,Javascript(与Haskell & co. 不同)没有为不可变数据结构提供一流的支持(在 Java 中你会有关键字final)。这并不意味着您不能以不可变的方式编写代码或推理程序。

正如其他人所提到的,您仍然有一些本机 javascript API 可以帮助您实现不变性(ish),但是正如您已经意识到的那样,它们都没有真正解决问题(Object.freeze只能在浅层工作,const阻止您重新分配变量,但不能改变它, 等等。)。


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

我想提前道歉,因为这个答案可能主要基于意见,并且不可避免地会因我自己的经验和思维方式而有缺陷。所以,请用少许盐选择以下内容,因为这只是我在这个主题上的两分钱。

我想说的是不变性主要是一种思维状态,在此之上您可以构建所有支持(或使其更易于使用)它的语言 API。

我之所以说“这主要是一种心态”,是因为您可以(某种程度上)用第三方库来弥补一流语言结构的不足(并且有一些非常令人印象深刻的成功案例)。

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

嗯,它背后的想法是,任何变量都被视为固定的,任何突变都必须在新实例中解决,而原始实例input保持不变。

好消息是,这已经适用于所有 javascript 原语。

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

console.log(input === output); // false
Run Code Online (Sandbox Code Playgroud)

所以,问题是,我们如何能对待一切,因为它是一个原始的

...嗯,答案很简单,接受函数式编程的一些基本原则,并让第三方库填补这些语言空白。

  1. state从他们的transition逻辑中分离出来:
class User {
  name;

  setName(value) { this.name = value }
}
Run Code Online (Sandbox Code Playgroud)

只是


const user = { name: 'Giuseppe' };

const setUserName = (name, user) => ({ ...user, name });
Run Code Online (Sandbox Code Playgroud)
  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
Run Code Online (Sandbox Code Playgroud)

也许是我喜欢的一些库的注释

  1. Ramda提供不变性,并用您通常在任何f语言中找到的所有声明性好东西来丰富 js api (sanctuary-jsfp-ts也是伟大的成功案例)
  2. RxJS 使用序列实现不可变和无副作用的编程,同时还提供惰性评估机制等。
  3. ReduxXState为不可变状态管理提供解决方案。

最后一个例子

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

console.log(input === output); // false
Run Code Online (Sandbox Code Playgroud)
class User {
  name;

  setName(value) { this.name = value }
}
Run Code Online (Sandbox Code Playgroud)

最后重申你自己的例子


const user = { name: 'Giuseppe' };

const setUserName = (name, user) => ({ ...user, name });
Run Code Online (Sandbox Code Playgroud)

  • +1 表示“*函数式编程主要是一种心态*” (2认同)

Lef*_*ium 5

尽管 JavaScript 缺乏不可变的内置数据结构,但不可变状态仍然是可能的。

如您所知,变量存储程序的状态。像 Lisp 这样的函数式语言通常通过将当前状态作为输入并返回新的更新状态作为输出(用作另一个函数的输入;重复)来更改程序状态。

JavaScript 程序通常通过改变变量来改变程序状态,但也可以使用上述 Lisp 使用的方法。无需编写改变变量的函数,只需编写输入当前状态并返回新输出状态的函数,而不修改任何输入。

在 JavaScript 中以不可变风格进行编程时,您可能会遇到一些缺点:

  • JavaScript 针对突变进行了优化,而不是针对不变性进行了优化。因此可能会存在内存/性能损失。不可变范式更喜欢生成新值而不是改变现有值。(另一方面,类似 Lisp 的语言针对不变性而不是突变进行了优化。)
  • 如果您使用任何会改变数据的 JavaScript 原语(例如Array.sort()),则突变可能会“泄漏”到您的程序中。

协助在 JS 中使用不可变范例的库

伊梅尔

Immer是一个 JS 库,有助于在 JS 中使用不可变状态:

基本思想是,您将把所有更改应用到临时的 DraftState,它是 currentState 的代理。一旦完成所有突变,Immer 将根据草稿状态的突变生成 nextState。这意味着您只需修改数据即可与数据进行交互,同时保留不可变数据的所有优点。

不可变.js

Immutable.js是另一个 JS 库,有助于在 JS 中实现不可变状态:

Immutable.js 提供了许多持久不可变数据结构,包括:List、Stack、Map、OrderedMap、Set、OrderedSet 和 Record。

这些数据结构在现代 JavaScript VM 上非常高效,通过使用由 Clojure 和 Scala 推广的哈希映射尝试和向量尝试进行结构共享,最大限度地减少了复制或缓存数据的需要。

Mori提取了 ClojureScript 的优化不可变数据结构,以便您可以在 vanilla JS 中使用它们。(ClojureScript 是一个可以编译为 JavaScript 的 Lisp。)

JavaScript 不可变库列表

这个不可变库列表分为两个主要类别:

  • 具有结构共享的持久数据结构
  • 不可变助手(简单的浅拷贝 JS 对象)

更长的 JavaScript 函数式编程资源列表

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

最后,ProseMirror 是 JavaScript 中不可变状态的一个很好的现实示例:

ProseMirror 是一个用 JavaScript 编写的编辑器,它使用持久数据结构来存储文档数据

  • 请注意,按照惯例,此数据结构是不可变的。开发人员必须确保数据结构不被改变。可以改变数据结构,但结果是未定义的,并且很可能是不需要的。因此ProseMirror提供了文档和函数来支持不变性。
  • 可以使用 来强制此数据结构的不变性Object.freeze(),但这通常会被避免,因为它会带来巨大的性能损失。
  • 另请注意,不变性并不是“全有或全无”。主要数据结构是不可变的,但可变变量用在更有意义的地方(例如循环变量)。

  • “*像 Lisp 这样的函数式语言*”——不确定这是一个很好的例子。Lisp 有很多种,而且大多数都支持与 JS 一样多的范式。 (2认同)