首页 > 解决方案 > 如何在函数式 JavaScript 中存储数组的状态?

问题描述

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

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

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 数组的引用,这两者都涉及具有某种副作用的函数。

我试图寻找实现这一点的方法,但没有成功。有没有办法使用纯函数来解决这个问题?

提前致谢。

标签: javascriptfunctional-programming

解决方案


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

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

所以,让我们开始吧。我们将从创建模型开始。我们的模型将是一个摩尔机器

// 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

接下来,我们将创建视图,它是一个函数,给定模型的输出会返回一个 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];
};

最后,我们创建连接模型和视图的控制器:

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. 它们结合了视图和控制器,这是糟糕的设计。我喜欢将模型、视图和控制器放在单独的目录中,然后将它们全部组合到第三个应用程序目录中。
  3. 用于创建模型的 Redux 是一个独立于 React 的库,用于创建视图控制器。不过,这不是一个破坏者。
  4. 这是不必要的复杂。

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


推荐阅读