不学习函数和对象,你不可能成为一名JavaScript程序员,并且当他们一起使用时,是构建块,我们需要从一个称为组合(composition)的强大对象范例开始。今天我们来看一些惯用的模式,使用工厂函数来组成函数,对象和 Promises 。
注:组合模式是将一批子对象组织为树形结构,一条顶层的命令会在操作树中所有的对象。
当一个函数返回一个对象时,我们称之他为 工厂函数(factory function)。
让我们来看一个简单的例子。
function createJelly() {
return {
type: 'jelly',
colour: 'red'
scoops: 3
};
}
每次我们调用这个工厂函数,它将返回一个新的 jelly(果冻) 对象实例。
要注意的重点是,我们不必在工厂函数名称前面加上create,但它可以让其他人更清楚函数的意图。
对于type属性也是如此,但通常它可以帮助我们区分我们程序的对象。
带参数的工厂函数
像所有函数一样,我们可以通过参数来定义我们的工厂函数 (icecream 冰淇淋),这可以用来改变返回对象的模型。
function createIceCream(flavour='Vanilla') {
return {
type: 'icecream',
scoops: 3,
flavour
}
}
理论上,您可以使用带有数百个参数的工厂函数来返回非常特使的深层嵌套对象,但正如我们将看到的,这根本不是组合的精髓。
组合的工厂函数
在一个工厂函数中定义另一个工厂函数,可以帮助我们把复杂的工厂函数拆分成更小的,可重用的碎片。
例如,我们可以创建一个 dessert(甜点)工厂函数,通过前面的 jelly(果冻)和 icecream(冰淇淋)工厂函数来定义。
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;
// 或者
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()
};
}
异步的工厂函数
并非所有工厂都会立即返回数据。例如,有些必须先获取数据。
在这些情况下,我们可以返回 Promises 来定义工厂函数。
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 这样的工具返回 Promises 来定义工厂函数。
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或WebSockets发送它们,并将它们放入数据存储很重要。
例如,不是在 jelly(果冻) 对象上定义 eat 方法,我们可以定义一个新的函数,它接受一个对象作为参数并返回一个修改的版本。
function eatJelly(jelly) {
if(jelly.scoops > 0) {
jelly.scoops -= 1;
}
return jelly;
}
一点点句法帮助使这是一个可行的模式,那些喜欢编程而不改变数据结构的人。
对于那些喜欢编程而不改变数据结构的人来说,使用 ES6 的...语法 是一个可行的模式。
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中的原始类型,所以可以说工厂不是抽象的!
使用这些简单的构建块使得我们的代码对于没有经验的程序员更加友好,这绝对是我们应该关心的事情。工厂鼓励我们用原始数据来模拟复杂和异步数据,原始数据具有组合的自然能力,而不强迫我们去做一些高级抽象。 当我们坚持简单时,JavaScript更甜蜜!