繁体   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