首页 > web前端 > js教程 > 带有MORI的不变数据和功能性JavaScript

带有MORI的不变数据和功能性JavaScript

Joseph Gordon-Levitt
发布: 2025-02-18 08:34:10
原创
731 人浏览过

带有MORI的不变数据和功能性JavaScript

钥匙要点

  • > MORI利用Clojure的持久数据结构,为JavaScript开发人员不可变的数据选项提供了增强代码简单性和可靠性的不可能。
  • >
  • 使用MORI通过强制性不变性来促进JavaScript中的功能编程范式,从而防止意外副作用并确保整个应用程序生命周期中的数据一致性。库促进了处理数据的不同方法,其中函数分别在数据结构上运行,与JavaScript的典型对象方面的方法形成鲜明对比,从而允许使用更清洁,更可预测的代码。
  • MORI的结构共享技术通过在可能的情况下重复现有数据结构来使数据操纵有效,从而可以改善应用程序的性能。
  • >通过诸如具有撤消功能的像素编辑器之类的示例,Mori展示了不变数据结构的实际应用,从而为开发人员提供了构建既复杂且稳健的功能的工具。
  • >。
  • 本文由Craig Bilner和Adrian Sandu进行了同行评审。感谢SitePoint所有的同行评审员制作SitePoint内容的最佳状态!
>功能编程和不变数据是许多JavaScript开发人员的当前重点,因为他们试图找到使代码更简单,更易于推理的方法。

>尽管JavaScript一直支持某些功能性编程技术,但它们在过去几年中才真正流行,传统上也没有对不变数据的本地支持。 JavaScript仍在学习很多方面,最好的想法来自已经尝试并测试了这些技术的语言。 在编程世界的另一个角落,Clojure是一种功能性编程语言,致力于真正的简单性,尤其是在数据结构的情况下。 Mori是一个库,允许我们直接从JavaScript中使用Clojure的持久数据结构。 >本文将探讨这些数据结构设计背后的基本原理,并检查一些使用它们来改善我们的应用程序的模式。我们也可以将其视为对使用Clojure或Clojurescript进行编程的JavaScript开发人员的第一个垫脚石。

>

什么是持续数据?

>

> clojure在无法更改的

持续

的值之间进行区分,而 thristient

值在突变之间具有时间寿命。尝试修改持续数据结构的尝试避免通过返回使用更改的新结构来突变基础数据。>

>可能有助于查看这种区别在理论编程语言中的外观。

<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>
登录后复制
登录后复制
登录后复制
登录后复制

>我们可以看到,当我们将一个值推向其上时,瞬态列表被突变了。 A和B都指向相同的可变值。相比之下,在持久列表上呼叫推送返回了一个新值,我们可以看到c和d指向不同的离散列表。

>

>这些持久的数据结构无法突变,这意味着一旦我们提到了一个值,我们也可以保证它永远不会被更改。这些保证通常可以帮助我们编写更安全,更简单的代码。例如,将持久数据结构作为参数的函数无法突变它们,因此,如果该函数想要传达有意义的更改,则必须来自返回值。这导致编写引用透明的纯函数,易于测试和优化。

更简单,不变的数据迫使我们编写更多功能代码。>

什么是mori?

> Mori使用clojurescript编译器来编译Clojure标准库中数据结构的实现,以汇编JavaScript。编译器发出了优化的代码,这意味着没有其他考虑,与JavaScript的Clojure进行交流并不容易。莫里是其他考虑因素的层。

>就像Clojure一样,Mori的功能与它们操作的数据结构分开,与JavaScript面向对象的趋势对比。我们会发现这种差异改变了我们编写代码的方向。

莫里还使用结构共享来通过尽可能多的原始结构共享对数据进行有效的更改。这使得持续数据结构几乎与常规瞬态效率一样有效。这些概念的实现在此视频中更详细地介绍了。

>
<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>
登录后复制
登录后复制
登录后复制
登录后复制
为什么有用?

> 首先,让我们想象我们正在尝试在我们继承的JavaScript代码库中追踪一个错误。我们正在阅读代码,试图弄清楚为什么我们最终获得了奖学金的错误价值。

>

登录到控制台时奖学金的价值是多少?

>

>不运行代码或阅读deleteperson()的定义,就无法知道。它可能是一个空数组。它可能具有三个新属性。我们希望这是一个删除第二个元素的数组,但是由于我们通过可变的数据结构通过,因此无法保证。
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
登录后复制
登录后复制
登录后复制
登录后复制
更糟糕的是,该函数可以保持参考并在将来异步进行突变。从这里开始对奖学金的所有参考都将与不可预测的价值合作。>

将其与Mori的替代方案进行比较。

<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>
登录后复制
登录后复制
登录后复制
登录后复制

>不管deleteperson()的实施如何,我们知道原始向量将被记录,仅仅是因为可以保证它不能突变。如果我们希望该函数有用,则应返回并删除指定项目的新向量。>

理解流过在不变数据上工作的函数的流很容易,因为我们知道它们的唯一效果是得出并返回独特的不变价值。

在可变数据上运行的带有MORI的不变数据和功能性JavaScript函数并不总是返回值,它们可以突变其输入,有时还留给程序员再次在另一侧拾取值。

>

带有MORI的不变数据和功能性JavaScript更简单,不变的数据可实现可预测性的文化。

在实践中

>我们将研究如何使用MORI来构建具有撤消功能的像素编辑器。以下代码可作为Codepen可用,您也可以在文章的脚下找到。

>

我们假设您要么跟随Codepen,要么在ES2015环境中使用MORI和以下HTML。 带有MORI的不变数据和功能性JavaScript

设置和实用程序

让我们开始通过破坏Mori名称空间所需的功能开始。

<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>
登录后复制
登录后复制
登录后复制
登录后复制
这主要是一种风格偏好。您也可以通过直接在Mori对象上访问MORI(例如Mori.list())。

>我们要做的第一件事是设置用于查看持续数据结构的助手功能。莫里(Mori)的内部表示形式在控制台中没有多大意义,因此我们将使用tojs()函数将它们转换为可理解的格式。

>

>当我们需要检查Mori的数据结构时,我们可以将此功能用作cons.log()的替代方法。
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
登录后复制
登录后复制
登录后复制
登录后复制
接下来,我们将设置一些配置值和实用程序函数。

>希望您注意到我们的to2d()函数返回向量。向量有点像JavaScript数组,并且支持有效的随机访问。>

构造数据

<span>import <span>{ vector, hashMap }</span> from 'mori';
</span>
<span>const fellowship = vector(
</span>  <span>hashMap(
</span>    <span>"name", "Mori",
</span>    <span>"race", "Hobbit"
</span>  <span>),
</span>  <span>hashMap(
</span>    <span>"name", "Poppin",
</span>    <span>"race", "Hobbit"
</span>  <span>)
</span><span>)
</span>
<span>const newFellowship = deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
登录后复制
登录后复制
>我们将使用我们的to2d()函数来创建一系列坐标,该坐标将代表画布上的所有像素。

>我们使用range()函数来生成一个数字序列在0和高度 *宽度之间(在我们的情况100)之间,并且我们使用map()将其转换为使用to2d的2D坐标列表()辅助功能。

<span><span><span><div</span>></span>
</span>  <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span> id<span>="container"</span>></span>
</span>  <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span>></span>
</span>  <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span>
</span><span><span><span></div</span>></span>
</span>
登录后复制
登录后复制
>可能有助于可视化坐标的结构。

>

这是坐标向量的一维序列。

>在每个坐标的旁边,我们还需要存储一个颜色值。>
<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>
登录后复制
登录后复制
登录后复制
登录后复制

>我们正在使用repot()函数来创建“ #FFF”字符串的无限序列。我们不必担心会填充内存并崩溃我们的浏览器,因为Mori序列支持懒惰评估。我们只在稍后要求时才计算序列中项目的值。

>最后,我们想以哈希地图的形式将坐标与我们的颜色相结合。

<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>
登录后复制
登录后复制
登录后复制
登录后复制
>我们使用zipmap()函数来创建哈希地图,将坐标作为键,颜色作为值。同样,这可能有助于可视化数据的结构。

与JavaScript的对象不同,Mori的Hash Maps可以将任何类型的数据作为键。

绘制像素
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
登录后复制
登录后复制
登录后复制
登录后复制
为了更改像素的颜色,我们将把哈希地图中的一个坐标与新字符串相关联。让我们写一个纯净的函数,为单个像素着色。

>

>我们使用X和Y坐标来创建可以用作键的坐标向量,然后我们使用Assoc()将该密钥与新颜色相关联。请记住,由于数据结构是持续的,因此assoc.)函数将返回a

new

hash映射,而不是突变旧的。

绘画图片
<span>import <span>{ vector, hashMap }</span> from 'mori';
</span>
<span>const fellowship = vector(
</span>  <span>hashMap(
</span>    <span>"name", "Mori",
</span>    <span>"race", "Hobbit"
</span>  <span>),
</span>  <span>hashMap(
</span>    <span>"name", "Poppin",
</span>    <span>"race", "Hobbit"
</span>  <span>)
</span><span>)
</span>
<span>const newFellowship = deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
登录后复制
登录后复制

>现在,我们有所有需要将简单图像绘制到画布上的一切。让我们创建一个函数,该函数将坐标图的哈希映射针对像素,并将它们绘制到renderingContext2d。 让我们花一点时间了解这里发生了什么。

>

>我们正在使用每个()在像素上迭代哈希地图上迭代。它将每个密钥和值(作为序列一起)作为p传递到回调函数中。然后,我们使用inarray()函数将其转换为可能破坏的数组,因此我们可以挑选出所需的值。

>

最后,我们使用帆布方法将彩色矩形绘制到上下文本身上。
<span><span><span><div</span>></span>
</span>  <span><span><span><h3</span>></span>Mori Painter<span><span></h3</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span> id<span>="container"</span>></span>
</span>  <span><span><span><canvas</span> id<span>='canvas'</span>></span><span><span></canvas</span>></span>
</span><span><span><span></div</span>></span>
</span><span><span><span><div</span>></span>
</span>  <span><span><span><button</span> id<span>='undo'</span>></span>↶<span><span></button</span>></span>
</span><span><span><span></div</span>></span>
</span>
登录后复制
登录后复制

将其接线

现在,我们需要进行一些管道,以使所有这些部分将所有这些零件一起工作。

<span>const {
</span>  list<span>, vector, peek, pop, conj, map, assoc, zipmap,
</span>  range<span>, repeat, each, count, intoArray, toJs
</span><span>} = mori;
</span>
登录后复制
>我们将掌握画布,并使用它来创建上下文来呈现我们的图像。我们还将适当地调整它的大小以反映我们的尺寸。

>

>最后,我们将使用像素通过油漆方法绘制的像素来传递上下文。幸运的是,您的画布应该像白色像素一样渲染。不是最令人兴奋的揭露,但我们越来越接近。
<span>const log = (<span>...args</span>) => {
</span>  <span>console.log(...args.map(toJs))
</span><span>};
</span>
登录后复制
>

互动

>我们想收听点击事件,并使用它们以较早的draw()函数来更改特定像素的颜色。

>
<span>// the dimensions of the canvas
</span><span>const [height, width] = [20, 20];
</span>
<span>// the size of each canvas pixel
</span><span>const pixelSize = 10;
</span>
<span>// converts an integer to a 2d coordinate vector
</span><span>const to2D = (i) => vector(
</span>  i <span>% width,
</span>  <span>Math.floor(i / width)
</span><span>);
</span>
登录后复制

>我们将单击侦听器附加到我们的画布上,并使用事件坐标来确定要绘制哪个像素。我们使用此信息使用我们的draw()函数创建新的像素哈希地图。然后,我们将其绘制到我们的上下文中,并覆盖我们画的最后一帧。

在这一点上,我们可以将黑色像素绘制到画布中,每个帧将基于上一个,创建一个复合图像。

>跟踪帧

要实施撤消,我们将需要将每个历史性的修订存储到像素哈希地图上,因此我们将来可以再次检索它们。

我们正在使用列表来存储我们绘制的不同的“帧”。列表支持在头部的有效添加,O(1)查找第一项,这使其非常适合表示堆栈。
<span>// transient list
</span>a <span>= [1, 2, 3];
</span>b <span>= a.push(4);
</span><span>// a = [1, 2, 3, 4]
</span><span>// b = [1, 2, 3, 4]
</span>
<span>// persistent list
</span>c <span>= #[1, 2, 3]
</span>d <span>= c.push(4);
</span><span>// c = #[1, 2, 3]
</span><span>// d = #[1, 2, 3, 4]
</span>
登录后复制
登录后复制
登录后复制
登录后复制
我们需要修改单击侦听器以与帧堆栈一起使用。

我们正在使用PEEK()函数将框架放在堆栈顶部。然后,我们使用它来使用draw()函数创建一个新帧。最后,我们将conj()用于

conjoin

框架顶部的新框架。
<span>// standard library
</span><span>Array(1, 2, 3).map(x => x * 2);
</span><span>// => [2, 4, 6]
</span>
<span>// mori
</span><span>map(x => x * 2, vector(1, 2, 3))
</span><span>// => [2, 4, 6]
</span>
登录后复制
登录后复制
登录后复制
登录后复制

尽管我们正在更改本地状态(frame = conj(框架,newFrame)),但我们实际上并未突变任何数据。 撤消更改

>最后,我们需要实现一个撤消按钮,以从堆栈中弹出顶框。>

>单击“撤消按钮”时,我们检查当前是否有任何帧要撤消,然后使用pop()函数用不再包含顶帧的新列表替换帧。>最后,我们将新堆栈上的顶帧传递给油漆()函数以反映更改。在这一点上,您应该能够绘制并撤消对画布的更改。> demo

这是我们最终得到的:
<span>const fellowship = [
</span>  <span>{
</span>    <span>title: 'Mori',
</span>    <span>race: 'Hobbit'
</span>  <span>},
</span>  <span>{
</span>    <span>title: 'Poppin',
</span>    <span>race: 'Hobbit'
</span>  <span>}
</span><span>];
</span>
<span>deletePerson(fellowship, 1);
</span><span>console.log(fellowship);
</span>
登录后复制
登录后复制
登录后复制
登录后复制

请参阅codepen上的sitepoint(@sitepoint)的笔莫里像素。

扩展

这是您可以改善此应用程序的方式的想法列表:>

添加一个调色板,允许用户在绘制

之前选择颜色

>使用本地存储来保存会话之间的帧

使CTRL Z键盘快捷键撤消更改

允许用户在拖动鼠标时绘制

    通过移动索引指针来实现重做,而不是从堆栈中删除帧
  • >仔细阅读相同程序的clojurescript源
  • 结论
  • >我们已经研究了向量,列表,范围和哈希地图,但是Mori还附带了集合,排序的集合和队列,这些数据结构中的每一个都带有用于与之合作的多态性功能的补充。
  • >我们几乎没有刮过可能的表面,但是希望您能看到足够的时间来重视将持久数据与一组功能强大的简单功能配对的重要性。

    经常询问有关不变数据和功能性JavaScript的问题

    JavaScript中不变性的概念是什么?这意味着一旦分配了一个值,就无法更改它。这个概念对于功能编程至关重要,因为它有助于避免副作用,并使您的代码更容易预测和易于理解。它还可以通过允许有效的数据检索和内存使用量来提高应用程序的性能。

    >

    > MORI库如何帮助处理JavaScript中的不变数据?

    持续的数据结构中的JavaScript。这些数据结构是不可变的,这意味着它们一旦创建就无法更改。这有助于保持数据的完整性并避免意外修改。 MORI还提供了一套丰富的功能性编程实用程序,使操纵这些数据结构变得更容易。

    >

    >使用MORI而不是本机JavaScript方法处理不变数据有什么好处?确实提供了处理不变数据的方法,Mori提供了一种更有效,更健壮的方法。与本机JavaScript方法相比,MORI的持续数据结构更快,并且消耗的内存更少。此外,莫里(Mori 。由于创建一旦创建就无法更改不变的对象,因此可以在多个函数调用中安全地重复使用,而不会被修改。这会导致有效的内存使用和更快的数据检索,从而提高了应用程序的整体性能。

    >可突变和不可变的数据结构之间有什么区别?

    > MORI如何处理数据操作?

    MORI提供了丰富的功能编程实用程序来操纵数据。这些实用程序允许您在数据结构上执行诸如地图,减少,过滤等的各种操作,而无需更改原始数据。

    Mori中的持续数据结构是什么?在Mori中,是不可变的数据结构,在修改后可以保留先前版本的数据。这意味着每次您对持久数据结构执行操作时,都会创建一个新版本,并保留旧版本。

    MORI如何确保数据完整性?

    MORI通过提供不可变的数据结构来确保数据完整性。由于创建后无法修改这些数据结构,因此消除了意外数据修改的风险。这有助于维持数据的完整性。

    >

    在JavaScript中使用MORI?

    在JavaScript中使用MORI的功能编程具有什么优势。通过避免副作用,它使您的代码更容易预测,并且更容易理解。它还通过允许有效的数据检索和内存使用量来增强您的应用程序的性能。

    >

    >我如何在我的JavaScript项目中开始使用Mori?

    在您的JavaScript项目中开始使用Mori,您需要在您的项目中包括Mori库。您可以通过通过NPM安装它或将其直接包含在HTML文件中来执行此操作。包含库后,您可以在代码中开始使用MORI的功能和数据结构。

    >

以上是带有MORI的不变数据和功能性JavaScript的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板