草庐IT

javascript - 如何在函数式 JavaScript 中存储数组的状态?

coder 2024-07-20 原文

我最近一直在学习一些使用 JavaScript 的函数式编程,并且想通过仅使用函数式编程编写一个简单的 ToDo 应用来测试我的知识。但是,我不确定如何以纯函数的方式存储列表的状态,因为函数不允许有副作用。让我用一个例子来解释。

假设我有一个名为“Item”的构造函数,它只有要完成的任务,以及一个标识该项目的 uuid。我还有一个项目数组,它包含所有当前项目,以及一个“添加”和“删除”函数,如下所示:

function Item(name){
    this.name = name;
    this.uuid = uuid(); //uuid is a function that returns a new uuid
}

const items = [];

function addItem(name){
    const newItem = new Item(name);
    items.push(newItem);
}

function deleteItem(uuid){
    const filteredItems = items.filter(item => item.uuid !== uuid);
    items = filteredItems
}

现在这很完美,但正如您所见,函数并不纯粹:它们确实有副作用并且不返回任何内容。考虑到这一点,我试着让它像这样发挥作用:

function Item(name){
    this.name = name;
    this.uuid = uuid(); //uuid is a function that returns a new uuid
}

const items = [];

function addItem(array, constructor, name){
    const newItem = new constructor(name);
    return array.concat(newItem);
}

function removeItem(array, uuid){
    return array.filter(item => item.uuid !== uuid);
}

现在函数是纯函数(或者我认为,如果我错了请纠正我),但是为了存储项目列表,我需要在每次添加或删除项目时创建一个新数组。这不仅看起来效率低得令人难以置信,而且我也不确定如何正确实现它。假设我想在每次按下 DOM 中的按钮时向列表中添加一个新项目:

const button = document.querySelector("#button") //button selector
button.addEventListener("click", buttonClicked)

function buttonClicked(){
    const name = document.querySelector("#name").value
    const newListOfItems = addItem(items, Item, name);
}

这又不是纯函数式的,但还有另一个问题:这将无法正常工作,因为每次调用该函数时,它都会使用现有的“items”数组创建一个新数组,而该数组本身不是改变(总是一个空数组)。为了解决这个问题,我只能想到两个解决方案:修改原始“items”数组或存储对当前 items 数组的引用,这两种方法都涉及具有某种副作用的函数。

我曾尝试寻找实现此功能的方法,但没有成功。有什么方法可以使用纯函数来解决这个问题吗?

提前致谢。

最佳答案

model–view–controller模式用于解决您描述的状态问题。我不会写一篇关于 MVC 的冗长文章,而是通过演示进行教学。假设我们正在创建一个简单的任务列表。以下是我们想要的功能:

  1. 用户应该能够向列表中添加新任务。
  2. 用户应该能够从列表中删除任务。

那么,让我们开始吧。我们将从创建模型开始。我们的模型将是 Moore machine :

// The arguments of createModel are the state of the Moore machine.
//                    |
//                    v
const createModel = tasks => ({
    // addTask and deleteTask are the transition functions of the Moore machine.
    // They return new updated Moore machines and are purely functional.
    addTask(task) {
        if (tasks.includes(task)) return this;
        const newTasks = tasks.concat([task]);
        return createModel(newTasks);
    },
    deleteTask(someTask) {
        const newTasks = tasks.filter(task => task !== someTask);
        return createModel(newTasks);
    },
    // Getter functions are the outputs of the Moore machine.
    // Unlike the above transition functions they can return anything.
    get tasks() {
        return tasks;
    }
});

const initialModel = createModel([]); // initially the task list is empty

接下来,我们将创建一个 View ,它是一个函数,给定模型的输出返回一个 DOM 列表:

// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
    const input = document.createElement("input");
    input.setAttribute("type", "text");
    input.setAttribute("id", "newTask");

    const button = document.createElement("input");
    button.setAttribute("type", "button");
    button.setAttribute("value", "Add Task");
    button.setAttribute("id", "addTask");

    const list = document.createElement("ul");

    for (const task of tasks) {
        const item = document.createElement("li");

        const span = document.createElement("span");
        span.textContent = task;

        const remove = document.createElement("input");
        remove.setAttribute("type", "button");
        remove.setAttribute("value", "Delete Task");
        remove.setAttribute("class", "remove");
        remove.setAttribute("data-task", task);

        item.appendChild(span);
        item.appendChild(remove);

        list.appendChild(item);
    }

    return [input, button, list];
};

最后,我们创建连接模型和 View 的 Controller :

const controller = model => {
    const app = document.getElementById("app"); // the place we'll display our app

    while (app.firstChild) app.removeChild(app.firstChild); // remove all children

    for (const element of createView(model)) app.appendChild(element);

    const newTask = app.querySelector("#newTask");
    const addTask = app.querySelector("#addTask");
    const buttons = app.querySelectorAll(".remove");

    addTask.addEventListener("click", () => {
        const task = newTask.value;
        if (task === "") return;
        const newModel = model.addTask(task);
        controller(newModel);
    });

    for (const button of buttons) {
        button.addEventListener("click", () => {
            const task = button.getAttribute("data-task");
            const newModel = model.deleteTask(task);
            controller(newModel);
        });
    }
};

controller(initialModel); // start the app

综合起来:

// The arguments of createModel are the state of the Moore machine.
//                    |
//                    v
const createModel = tasks => ({
    // addTask and deleteTask are the transition functions of the Moore machine.
    // They return new updated Moore machines and are purely functional.
    addTask(task) {
        if (tasks.includes(task)) return this;
        const newTasks = tasks.concat([task]);
        return createModel(newTasks);
    },
    deleteTask(someTask) {
        const newTasks = tasks.filter(task => task !== someTask);
        return createModel(newTasks);
    },
    // Getter functions are the outputs of the Moore machine.
    // Unlike the above transition functions they can return anything.
    get tasks() {
        return tasks;
    }
});

const initialModel = createModel([]); // initially the task list is empty

// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
    const input = document.createElement("input");
    input.setAttribute("type", "text");
    input.setAttribute("id", "newTask");

    const button = document.createElement("input");
    button.setAttribute("type", "button");
    button.setAttribute("value", "Add Task");
    button.setAttribute("id", "addTask");

    const list = document.createElement("ul");

    for (const task of tasks) {
        const item = document.createElement("li");

        const span = document.createElement("span");
        span.textContent = task;

        const remove = document.createElement("input");
        remove.setAttribute("type", "button");
        remove.setAttribute("value", "Delete Task");
        remove.setAttribute("class", "remove");
        remove.setAttribute("data-task", task);

        item.appendChild(span);
        item.appendChild(remove);

        list.appendChild(item);
    }

    return [input, button, list];
};

const controller = model => {
    const app = document.getElementById("app"); // the place we'll display our app

    while (app.firstChild) app.removeChild(app.firstChild); // remove all children

    for (const element of createView(model)) app.appendChild(element);

    const newTask = app.querySelector("#newTask");
    const addTask = app.querySelector("#addTask");
    const buttons = app.querySelectorAll(".remove");

    addTask.addEventListener("click", () => {
        const task = newTask.value;
        if (task === "") return;
        const newModel = model.addTask(task);
        controller(newModel);
    });

    for (const button of buttons) {
        button.addEventListener("click", () => {
            const task = button.getAttribute("data-task");
            const newModel = model.deleteTask(task);
            controller(newModel);
        });
    }
};

controller(initialModel); // start the app
<div id="app"></div>

当然,这不是很有效,因为每次更新模型时您都在更新整个 DOM。但是,您可以使用像 virtual-dom 这样的库解决这个问题。

您还可以查看 ReactRedux .但是,我不太喜欢它,因为:

  1. 他们使用类,这使得一切都冗长笨拙。不过,如果您真的愿意,您可以制作功能组件。
  2. 它们结合了 View 和 Controller ,这是糟糕的设计。我喜欢将模型、 View 和 Controller 放在单独的目录中,然后将它们全部组合到第三个应用程序目录中。
  3. 用于创建模型的 Redux 是一个独立于 React 的库,用于创建 View Controller 。不过,这不是一个破坏交易的因素。
  4. 它不必要地复杂。

但是,它经过了 Facebook 的充分测试和支持。因此,值得一看。

关于javascript - 如何在函数式 JavaScript 中存储数组的状态?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55538823/

有关javascript - 如何在函数式 JavaScript 中存储数组的状态?的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  3. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  4. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  5. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  6. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  7. ruby - 多次弹出/移动 ruby​​ 数组 - 2

    我的代码目前看起来像这样numbers=[1,2,3,4,5]defpop_threepop=[]3.times{pop有没有办法在一行中完成pop_three方法中的内容?我基本上想做类似numbers.slice(0,3)的事情,但要删除切片中的数组项。嗯...嗯,我想我刚刚意识到我可以试试slice! 最佳答案 是numbers.pop(3)或者numbers.shift(3)如果你想要另一边。 关于ruby-多次弹出/移动ruby​​数组,我们在StackOverflow上找到一

  8. ruby - 将数组的内容转换为 int - 2

    我需要读入一个包含数字列表的文件。此代码读取文件并将其放入二维数组中。现在我需要获取数组中所有数字的平均值,但我需要将数组的内容更改为int。有什么想法可以将to_i方法放在哪里吗?ClassTerraindefinitializefile_name@input=IO.readlines(file_name)#readinfile@size=@input[0].to_i@land=[@size]x=1whilex 最佳答案 只需将数组映射为整数:@land边注如果你想得到一条线的平均值,你可以这样做:values=@input[x]

  9. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  10. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

随机推荐