Jeff Mott对本文进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
作为JavaScript程序员,如果不了解函数和对象,您将无所不能。当它们一起使用时,它们便是我们开始使用强大的对象范式(称为composition)的基础 。 今天,我们将介绍一些使用工厂函数来构成函数,对象和promise的惯用模式。
当一个函数返回一个对象时,我们称它为工厂函数 。
让我们看一个简单的例子。
function createJelly() {
return {
type: 'jelly',
colour: 'red'
scoops: 3
};
}
每次我们调用该工厂时,它将返回一个果冻对象的新实例。
重要的是要注意,我们不必在工厂名称前加上create
前缀,但可以使其他人更清楚该功能的意图。 type
属性也是如此,但通常可以帮助我们区分流经程序的对象。
参数化工厂功能
像所有函数一样,我们可以使用更改返回对象形状的参数定义工厂。
function createIceCream(flavour='Vanilla') {
return {
type: 'icecream',
scoops: 3,
flavour
}
}
从理论上讲,您可以使用带有数百个参数的参数化工厂来返回非常具体且深度嵌套的对象,但是正如我们将看到的那样,这根本不符合组合的精神。
可组合的工厂功能
用一个工厂定义另一个工厂可以帮助我们将复杂的工厂分解成较小的,可重复使用的片段。
例如,我们可以创建一个甜点工厂,该工厂根据之前的果冻和冰淇淋工厂来定义。
function createDessert() {
return {
type: 'dessert',
bowl: [
createJelly(),
createIceCream()
]
};
}
我们可以组成工厂来建造任意复杂的对象,这些对象不需要我们弄乱new或this 。
可以用has-a关系而不是is-a表示的对象可以通过合成而不是继承来实现。
例如,带有继承。
// A trifle *is a* dessert
function Trifle() {
Dessert.apply(this, arguments);
}
Trifle.prototype = Dessert.prototype;
// or
class Trifle extends Dessert {
constructor() {
super();
}
}
我们可以用构图表达相同的想法。
// A trifle *has* layers of jelly, custard and cream. It also *has a* topping.
function createTrifle() {
return {
type: 'trifle',
layers: [
createJelly(),
createCustard(),
createCream()
],
topping: createAlmonds()
};
}
异步工厂功能
并非所有工厂都准备好立即返回数据。 例如,有些将必须首先获取数据。
在这种情况下,我们可以定义返回承诺的工厂。
function getMeal(menuUrl) {
return new Promise((resolve, reject) => {
fetch(menuUrl)
.then(result => {
resolve({
type: 'meal',
courses: result.json()
});
})
.catch(reject);
});
}
这种深层嵌套的缩进会使异步工厂难以阅读和测试。 将它们分解为多个不同的工厂,然后进行组合通常会很有帮助。
function getMeal(menuUrl) {
return fetch(menuUrl)
.then(result => result.json())
.then(json => createMeal(json));
}
function createMeal(courses=[]) {
return {
type: 'meal',
courses
};
}
当然我们可以改用回调,但是我们已经有了Promise.all
工具来组成返回promise的工厂。
function getWeeksMeals() {
const menuUrl = 'jsfood.com/';
return Promise.all([
getMeal(`${menuUrl}/monday`),
getMeal(`${menuUrl}/tuesday`),
getMeal(`${menuUrl}/wednesday`),
getMeal(`${menuUrl}/thursday`),
getMeal(`${menuUrl}/friday`)
]);
}
我们使用get
而不是create
作为命名约定,以表明这些工厂进行了一些异步工作并返回了Promise。
功能与方法
到目前为止,我们还没有看到任何工厂使用方法返回对象,这是故意的。 这是因为一般而言, 我们不需要 。
工厂使我们能够将数据与计算分开。
这意味着我们将始终能够将对象序列化为JSON,这对于在会话之间持久化它们,通过HTTP或WebSocket发送它们并将它们放入数据存储区而言非常重要。
例如,我们不必定义在果冻对象上的eat方法,而只需定义一个新函数即可将对象作为参数并返回修改后的版本。
function eatJelly(jelly) {
if(jelly.scoops > 0) {
jelly.scoops -= 1;
}
return jelly;
}
一点点的语法帮助使这种模式成为那些希望在不更改数据结构的情况下进行编程的人的可行模式。
function eat(jelly) {
if(jelly.scoops > 0) {
return { ...jelly, scoops: jelly.scoops - 1 };
} else {
return jelly;
}
}
现在,而不是写:
import { createJelly } from './jelly';
createJelly().eat();
我们将写:
import { createJelly, eatJelly } from './jelly';
eatJelly(createJelly());
最终结果是一个接受对象并返回对象的函数。
我们怎么称呼返回对象的函数? 一个工厂!
高阶工厂
作为高阶函数传递工厂给我们带来了巨大的控制权。 例如,我们可以使用此概念来创建增强器 。
function giveTimestamp(factory) {
return (...args) => {
const instance = factory(...args);
const time = Date.now();
return { time, instance };
};
}
const createOrder = giveTimestamp(function(ingredients) {
return {
type: 'order',
ingredients
};
});
此增强器采用现有工厂并将其包装以创建工厂,该工厂返回带有时间戳的实例。
另外,如果我们要确保工厂返回不可变的对象,则可以使用freezer进行增强。
function freezer(factory) {
return (...args) => Object.freeze(factory(...args)));
}
const createImmutableIceCream = freezer(createIceCream);
createImmutableIceCream('strawberry').flavour = 'mint'; // Error!
结论
正如一个明智的程序员曾经说过的:
从无抽象中恢复要比错误的抽象容易得多。
由于经常鼓励我们构建复杂的抽象层,因此JavaScript项目倾向于难以测试和重构。
原型和类实现与像复杂和不自然的工具,一个简单的想法, new
和this
仍然引起种种困惑甚至现在年,他们加入后语。
对象和函数对大多数背景的程序员来说都是有意义的,并且它们都是JavaScript中的原始类型,因此可以说工厂根本不是抽象的!
使用这些简单的构建块可使我们的代码对没有经验的程序员更加友好,这绝对是我们所有人都应该关注的事情。 工厂鼓励我们使用具有自然合成能力的原语对复杂和异步数据进行建模,而不必强迫我们也无法达到高级抽象水平。 当我们坚持简单性时,JavaScript会变得更甜美!
From: https://www.sitepoint.com/factory-functions-javascript/