我最近一直在学习一些使用JavaScript的函数式编程,并且想通过编写一个仅包含函数式编程的简单ToDo应用程序来对我的知识进行测试。但是,我不确定如何以一种纯函数的方式存储列表状态,因为不允许函数产生副作用。让我用一个例子来解释。
比方说,我有一个名为“ Item”的构造函数,该构造函数仅具有要完成的任务,还有一个用于标识该项目的uuid。我还有一个items数组,其中包含所有当前项目,以及一个“ add”和“ delete”函数,如下所示:
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”数组创建一个新数组,而这本身并不是变化(总是一个空数组)。要解决此问题,我只能想到两种解决方案:修改原始的“项目”数组或存储对当前项目数组的引用,这两种方法都涉及具有某种副作用的功能。
我试图寻找实现此目标的方法,但没有成功。有什么办法可以使用纯函数来解决此问题?
谢谢。
答案 0 :(得分:1)
model–view–controller模式用于解决您描述的状态问题。我不会写冗长的有关MVC的文章,而是通过演示进行教学。假设我们正在创建一个简单的任务列表。这是我们想要的功能:
因此,让我们开始吧。我们将从创建模型开始。我们的模型将是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
接下来,我们将创建视图,该视图是一个函数,给定模型的输出返回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。但是,我不喜欢它,因为:
但是,它已经过Facebook的严格测试和支持。因此,值得一看。