- 原文地址:JavaScript Factory Functions with ES6+
- 原文做者:Eric Elliott
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:lampui
- 校对者:IridescentMia、sunui
注意:这是“软件编写”系列文章的第八部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 Composability)。后续还有更多精彩内容,敬请期待!
< 上一篇 | << 第一篇 | 下一篇 >javascript
工厂函数是一个能返回对象的函数,它既不是类也不是构造函数。在 JavaScript 中,任何函数均可以返回一个对象,若是函数前面没有使用 new
关键字,却又返回一个对象,那这个函数就是一个工厂函数。前端
由于工厂函数提供了轻松生成对象实例的能力,且无需深刻学习类和 new
关键字的复杂性,因此工厂函数在 JavaScript 中一直很具吸引力。java
JavaScript 提供了很是方便的对象字面量语法,代码以下:react
const user = {
userName: 'echo',
avatar: 'echo.png'
};复制代码
就像 JSON 的语法(JSON 就是基于 JavaScript 的对象字面量语法),:
(冒号)左边是属性名,右边是属性值。你可使用点运算符访问变量:android
console.log(user.userName); // "echo"复制代码
或者使用方括号及属性名访问变量:ios
const key = 'avatar';
console.log( user[key] ); // "echo.png"复制代码
若是在做用域内还有变量和你的属性名相同,那你能够直接在对象字面量中使用这个变量,这样就省去了冒号和属性值:git
const userName = 'echo';
const avatar = 'echo.png';
const user = {
userName,
avatar
};
console.log(user);
// { "avatar": "echo.png", "userName": "echo" }复制代码
对象字面量支持简洁表示法。咱们能够添加一个 .setUserName()
的方法:es6
const userName = 'echo';
const avatar = 'echo.png';
const user = {
userName,
avatar,
setUserName (userName) {
this.userName = userName;
return this;
}
};
console.log(user.setUserName('Foo').userName); // "Foo"复制代码
在简洁表示法中,this
指向的是调用该方法的对象,要调用一个对象的方法,只须要简单地使用点运算符访问方法并使用圆括号调用便可,例如 game.play()
就是在 game
这一对象上调用 .play()
。要使用点运算符调用方法,这个方法必须是对象属性。你也可使用函数原型方法 .call()
、.apply()
或 .bind()
把一个方法应用于一个对象上。github
本例中,user.setUserName('Foo')
是在 user
对象上调用 .setUserName()
,所以 this === user
。在.setUserName()
方法中,咱们经过 this
这个引用修改了 .userName
的值,而后返回了相同的对象实例,以便于后续方法链式调用。typescript
若是你须要建立多个对象,你应该考虑把对象字面量和工厂函数结合使用。
使用工厂函数,你能够根据须要建立任意数量的用户对象。假如你正在开发一个聊天应用,你会用一个用户对象表示当前用户,以及用不少个用户对象表示其余已登陆和在聊天的用户,以便显示他们的名字和头像等等。
让咱们把 user
对象转换为一个 createUser()
工厂方法:
const createUser = ({ userName, avatar }) => ({
userName,
avatar,
setUserName (userName) {
this.userName = userName;
return this;
}
});
console.log(createUser({ userName: 'echo', avatar: 'echo.png' }));
/*
{
"avatar": "echo.png",
"userName": "echo",
"setUserName": [Function setUserName]
}
*/复制代码
箭头函数(=>
)具备隐式返回的特性:若是函数体由单个表达式组成,则能够省略 return
关键字。()=>'foo'
是一个没有参数的函数,并返回字符串 "foo"
。
返回对象字面量时要当心。当使用大括号时,JavaScript 默认你建立的是一个函数体,例如 { broken: true }
。若是你须要返回一个明确的对象字面量,那你就须要经过使用圆括号将对象字面量包起来以消除歧义,以下所示:
const noop = () => { foo: 'bar' };
console.log(noop()); // undefined
const createFoo = () => ({ foo: 'bar' });
console.log(createFoo()); // { foo: "bar" }复制代码
在第一个例子中,foo:
被解释为一个标签,bar
被解释为一个没有被赋值或者返回的表达式,所以函数返回 undefined
。
在 createFoo()
例子中,圆括号强制着大括号,使其被解释为要求值的表达式,而不是一个函数体。
请特别注意函数声明:
const createUser = ({ userName, avatar }) => ({复制代码
这一行里,大括号 ({, }
) 表示对象解构。这个函数有一个参数(即一个对象),可是从这个参数中,却解构出了两个形参,userName
和 avatar
。这些形参能够做为函数体内的变量使用。解构还能够用于数组:
const swap = ([first, second]) => [second, first];
console.log( swap([1, 2]) ); // [2, 1]复制代码
你可使用扩展语法 (...varName
) 获取数组(或参数列表)余下的值,而后将这些值回传成单个元素:
const rotate = ([first, ...rest]) => [...rest, first];
console.log( rotate([1, 2, 3]) ); // [2, 3, 1]复制代码
前面咱们使用方括号的方法动态访问对象的属性值:
const key = 'avatar';
console.log( user[key] ); // "echo.png"复制代码
咱们也能够计算属性值来赋值:
const arrToObj = ([key, value]) => ({ [key]: value });
console.log( arrToObj([ 'foo', 'bar' ]) ); // { "foo": "bar" }复制代码
本例中,arrToObj
接受一个包含键值对(又称元组
)的数组,并将其转化成一个对象。由于咱们并不知道属性名,所以咱们须要计算属性名以便在对象上设置属性值。为了作到这一点,咱们使用了方括号表示法,来设置属性名,并将其放在对象字面量的上下文中来建立对象:
{ [key]: value }复制代码
在赋值完成后,咱们就能获得像下面这样的对象:
{ "foo": "bar" }复制代码
JavaScript 函数支持默认参数值,给咱们带来如下优点:
1
表示参数能够接受的数据类型为 Number
。使用默认参数,咱们能够为咱们的 createUser
工厂函数描述预期的接口,此外,若是用户没有提供信息,能够自动地补充某些
细节:
const createUser = ({
userName = 'Anonymous',
avatar = 'anon.png'
} = {}) => ({
userName,
avatar
});
console.log(
// { userName: "echo", avatar: 'anon.png' }
createUser({ userName: 'echo' }),
// { userName: "Anonymous", avatar: 'anon.png' }
createUser()
);复制代码
函数签名的最后一部分可能看起来有点搞笑:
} = {}) => ({复制代码
在参数声明最后那部分的 = {}
表示:若是传进来的实参不符合要求,则将使用一个空对象做为默认参数。当你尝试从空对象解构赋值的时候,属性的默认值会被自动填充,由于这就是默认值所作的工做:用预先定义好的值替换 undefined
。
若是没有 = {}
这个默认值,且没有向 createUser()
传递有效的实参,则将会抛出错误,由于你不能从 undefined
中访问属性。
在写这篇文章的时候,JavaScript 都尚未内置的类型注解,可是近几年涌现了一批格式化工具或者框架来填补这一空白,包括 JSDoc(因为出现了更好的选择其呈现出降低趋势)、Facebook 的 Flow、还有微软的 TypeScript。我我的使用 rtype,由于我以为它在函数式编程方面比 TypeScript 可读性更强。
直至写这篇文章,各类类型注解方案其实都不相上下。没有一个得到 JavaScript 规范的庇护,并且每一个方案都有它明显的不足。
类型推断是基于变量所在的上下文推断其类型的一个过程,在 JavaScript 中,这是对类型注解很是好的一个替代。
若是你在标准的 JavaScript 函数中提供足够的线索去推断,你就能得到类型注解的大部分好处,且不用担忧任何额外成本或风险。
即便你决定使用像 TypeScript 或 Flow 这样的工具,你也应该尽量利用类型推断的好处,并保存在类型推断抽风时的类型注解。例如,原生 JavaScript 是不支持定义共享接口的。但使用 TypeScript 或 rtype 均可以方便有效地定义接口。
Tern.js 是一个流行的 JavaScript 类型推断工具,它在不少代码编辑器或 IDE 上都有插件。
微软的 Visual Studio Code 不须要 Tern,由于它把 TypeScript 的类型推断功能附带到了 JavaScript 代码的编写中。
当你在 JavaScript 函数中指定默认参数值时,不少诸如 Tern.js、TypeScript 和 Flow 的类型推断工具就能够在 IDE 中给予提示以帮助开发者正确地使用 API。
没有默认值,各类 IDE(更多的时候,连咱们本身)都没有足够的信息来判断函数预期的参数类型。
有了默认值,IDE (更多的时候,咱们本身) 能够从代码中推断出类型。
将参数限制为固定类型(这会使通用函数和高阶函数更加受限)是不怎么合理的。但要说这种方法何时有意义的话,使用默认参数一般就是,即便你已经在使用 TypeScript 或 Flow 作类型推断。
工厂函数擅于利用一个优秀的 API 建立对象。一般来讲,它们能知足基本需求,但不久以后,你就会遇到这样的状况,总会把相似的功能构建到不一样类型的对象中,因此你须要把这些功能抽象为 mixin 函数,以便轻松重用。
mixin 的工厂函数就要大显身手了。咱们来构建一个 withConstructor
的 mixin 函数,把 .constructor
属性添加到全部的对象实例中。
with-constructor.js:
const withConstructor = constructor => o => {
const proto = Object.assign({},
Object.getPrototypeOf(o),
{ constructor }
);
return Object.assign(Object.create(proto), o);
};复制代码
如今你能够导入和使用其余 mixins:
import withConstructor from `./with-constructor'; const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x); // 或者 `import pipe from 'lodash/fp/flow';` // 设置一些 mixin 的功能 const withFlying = o => { let isFlying = false; return { ...o, fly () { isFlying = true; return this; }, land () { isFlying = false; return this; }, isFlying: () => isFlying } }; const withBattery = ({ capacity }) => o => { let percentCharged = 100; return { ...o, draw (percent) { const remaining = percentCharged - percent; percentCharged = remaining > 0 ? remaining : 0; return this; }, getCharge: () => percentCharged, get capacity () { return capacity } }; }; const createDrone = ({ capacity = '3000mAh' }) => pipe( withFlying, withBattery({ capacity }), withConstructor(createDrone) )({}); const myDrone = createDrone({ capacity: '5500mAh' }); console.log(` can fly: ${ myDrone.fly().isFlying() === true } can land: ${ myDrone.land().isFlying() === false } battery capacity: ${ myDrone.capacity } battery status: ${ myDrone.draw(50).getCharge() }% battery drained: ${ myDrone.draw(75).getCharge() }% `); console.log(` constructor linked: ${ myDrone.constructor === createDrone } `);复制代码
正如你所见,可重用的 withConstructor()
mixin 与其余 mixin 一块儿被简单地放入 pipeline
中。withBattery()
能够被其余类型的对象使用,如机器人、电动滑板或便携式设备充电器等等。withFlying()
能够被用来模型飞行汽车、火箭或气球。
对象组合更多的是一种思惟方式,而不是写代码的某一特定技巧。你能够在不少地方用到它。功能组合只是从头开始构建你思惟方式的最简单方法,工厂函数就是将对象组合有关实现细节包装成一个友好 API 的简单方法。
对于对象的建立和工厂函数,ES6 提供了一种方便的语法,大多数时候,这样就足够了,但由于这是 JavaScript,因此还有一种更方便并更像 Java 的语法:class
关键字。
在 JavaScript 中,类比工厂更冗长和受限,当进行代码重构时更容易出现问题,但也被像是 React 和 Angular 等主流前端框架所采纳使用,并且还有一些少见的用例,使得类更有存在乎义。
“有时,最优雅的实现仅仅是一个函数。不是方法,不是类,不是框架。仅仅只是一个函数。” ~ John Carmack
最后,你还要切记,不要把事情搞复杂,工厂函数不是必需的,对于某个问题,你的解决思路应当是:
纯函数 > 工厂函数 > 函数式 Mixin > 类
Next: Why Composition is Harder with Classes >
想更深刻学习关于 JavaScript 的对象组合?
跟着 Eric Elliott 学 Javacript,机不可失时再也不来!
Eric Elliott 是 “编写 JavaScript 应用” (O’Reilly) 以及 “跟着 Eric Elliott 学 Javascript” 两书的做者。他为许多公司和组织做过贡献,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等 , 也是不少机构的顶级艺术家,包括但不限于 Usher、Frank Ocean 以及 Metallica。
大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一块儿。
感谢 JS_Cheerleader.
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。