javascript - 如何在函数式 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 数组的引用,这两者都涉及具有某种副作用的函数。
我试图寻找实现这一点的方法,但没有成功。有没有办法使用纯函数来解决这个问题?
提前致谢。
解决方案
模型-视图-控制器模式用于解决您描述的状态问题。我不会写一篇关于 MVC 的冗长文章,而是通过演示来进行教学。假设我们正在创建一个简单的任务列表。以下是我们想要的功能:
- 用户应该能够将新任务添加到列表中。
- 用户应该能够从列表中删除任务。
所以,让我们开始吧。我们将从创建模型开始。我们的模型将是一个摩尔机器:
// 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 之类的库来解决此问题。
你也可以看看React和Redux。但是,我不是它的忠实粉丝,因为:
- 他们使用类,这使得一切都变得冗长而笨拙。虽然,如果你真的想要,你可以制作功能组件。
- 它们结合了视图和控制器,这是糟糕的设计。我喜欢将模型、视图和控制器放在单独的目录中,然后将它们全部组合到第三个应用程序目录中。
- 用于创建模型的 Redux 是一个独立于 React 的库,用于创建视图控制器。不过,这不是一个破坏者。
- 这是不必要的复杂。
但是,它经过了 Facebook 的充分测试和支持。因此,值得一看。
推荐阅读
- awk - 使用 awk 在同一文件中保存更改的问题
- javascript - 如何从 PWA 中的 URL 缓存 json?
- javascript - Square Payment api 集成 Angular 和 Firebase 无服务器
- json - 需要从json多维数组中提取key值
- python - setuptools中有卸载命令吗?
- php - Laravel FFMpeg - 无法在文件错误中加载 FFMpeg
- node.js - 如何在firebase firestore中存储列表?
- python - 如何为初学者在神经网络中使用 RandomSearchCV 或 GridSearchCV 优化超参数?
- arrays - 在函数 (C) 中用作数组的指针
- javascript - 如何从javascript对象键在网页上显示图像