原文地址:https://medium.com/javascript-scene/javascript-factory-functions-with-es6-4d224591a8b1
工厂函数是既不是类,也不是构造函数,但返回值是(可能是新的)对象的任何一个函数。在JavaScript中,任何函数都可以返回一个对象。当没有new
关键字时,它就是一个工厂函数。
由于工厂函数提供了轻松生成对象实例的能力,又无需深入学习类和new
关键字的复杂性,因此工厂函数在JavaScript中一直很具吸引力。
JavaScript提供了非常方便的对象字面量语法,代码如下:
1 | const user = { |
很像JSON的语法(JSON就是基于JavaScript的对象字面量的语法),:
左边是属性名,右边是变量值。我们可以使用点符号访问变量:
1 | console.log(user.userName); // "echo" |
或者也可以使用方括号及属性名访问变量:
1 | const key = 'avatar'; |
如果在作用域内还有变量和你的属性名相同,那你也可以直接使用这一变量创建对象字面量:
1 | const userName = 'echo'; |
对象字面量支持简单函数语法。我们可以添加一个 .setUserName()
的方法:
1 | const userName = 'echo'; |
在这一方法中,this
指向的是调用方法的对象,要调用一个对象的方法,只需要使用点符号访问方法并使用括号调用即可,例如game.play()
就是在game
这一对象上调用.play()
。要使用点符号调用方法,这个方法必须是对象属性。你也可以使用函数原型方法.call()
,.apply()
或.bind()
把一个方法应用于任何对象。
本例中, user.setUserName('Foo')
是在user
对象上调用.setUserName()
,因此this === user
。在.setUserName()
方法中,我们通过 this
这个引用修改了.userName
的值,然后返回了相同的对象示例,以便于后续方法链调用。
创建一个对象用字面量,创建多个对象用工厂
如果需要创建很多对象,就应该结合使用对象字面量和工厂。
使用工厂函数,我们就可以根据需要,创建任意数量的用户对象。假如正在做一个聊天应用,你会拥有一个表示当前用户的用户对象,以及当前登录和聊天的其他所有用户的许多用户对象,以便显示他们的名字和头像等等。
让我们把 user
对象转换成一个createUser()
的工厂方法:
1 | const createUser = ({ userName, avatar }) => ({ |
返回对象
箭头函数(=>
)具有隐式返回的特性:如果函数体由单个表达式组成,那么就可以省略return
关键字。()=>'foo'
是一个不需要参数的函数, 并返回字符串"foo"
。
返回对象字面量时要小心。当使用括号时,JavaScript 默认你会想要创建一个函数体,例如{ broken: true }
。如果你要隐式返回一个对象字面量,就需要消除歧义,把对象字面量使用括号包起来:
1 | const noop = () => { foo: 'bar' }; |
在第一个例子中, foo:
被解释为一个标签,bar
被解释成一个没有赋值或者返回值的表达式。因此这个函数输出是undefined
。
在第二个createFoo()
例中,圆括号把大括号强制解释成需要求值的表达式,而不是一个函数体。
解构
请特别注意函数声明:
1 | const createUser = ({ userName, avatar }) => ({ |
这一行里,大括号{
, }
表示了对象解构。这个函数需要一个参数(一个对象),但是从这个单一参数对象中,解构了两个正式的参数,userName
和 avatar
。这些参数可以作为函数范围内的变量使用。函数声明中还可以解构数组:
1 | const swap = ([first, second]) => [second, first]; |
并且我们可以用rest和spread语法(... varName
)从数组(或参数列表)中获取剩余值,然后将这些数组元素扩展回成单个元素:
1 | const rotate = ([first, ...rest]) => [...rest, first]; |
计算属性值
前面我们使用方括号的方法动态访问元素属性取得属性值:
1 | const key = 'avatar'; |
我们也可以计算得到属性名来赋值:
1 | const arrToObj = ([key, value]) => ({ [key]: value }); |
本例中,arrToObj
以一个包含键值对的数组为输入,并将其转化成一个对象。因为我们并不知道属性名,因此我们需要计算属性名以便设置对象的属性值。为了做到这一点,我们使用了方括号表示法,来设置属性名,并将其放在对象字面量的上下文中来创建对象:
1 | { [key]: value } |
在语句解释完成后,我们就能得到如下的最终对象:
1 | { "foo": "bar" } |
默认参数
JavaScript 函数支持默认参数值,这给我们带来以下这些优势:
用户可以通过适当的默认值省略参数。
函数自身可读性更好,因为默认值提供了参数输入样例。
IDE和默认分析工具可以使用默认值来推测可能的参数类型。比如,一个默认值是
1
的设定就暗含了函数可能会使用Number
的参数类型。
使用默认参数,我们相当于给createUser
工厂提供文档,并且如果没有提供用户信息,那么就自动的设为默认值 'Anonymous'
:
1 | const createUser = ({ |
函数定义的最后一部分其实看起来有点搞笑:
1 | } = {}) => ({ |
在参数部分结束之前的最后一个= {}
用于表示如果没有传入这一参数,那么将使用一个空对象作为默认值传入函数体。当你尝试从空对象中解构对象时,将会自动使用属性的默认值。这其实就是默认值的意义所在,用预定义的值(即空对象)替换undefined
。
假如没有= {}
这个默认值, createUser()
就会报错,因为你无法访问undefined
的属性值。
类型推断
JavaScript 没有任何原生的类型注解,但是近几年涌现了一批格式化工具或者框架来填补这一空白,包括JSDoc(由于出现了更好的选择其呈现出下降趋势),Facebook’s Flow,还有Microsoft’s TypeScript。我个人使用rtype,因为我觉得它在函数式编程方面比TypeScript更易读。
在我写本文的时候,各种类型解决方案并没有一个明显的获胜者。没有一个获得JavaScript规范的庇护,而且每一个都有明显的短处。
类型推断是指基于使用变量的上下文推断类型的过程。 在JavaScript中,类型注解是非常好的选择。
如果在标准的JavaScript函数签名中提供了足够多信息线索,就能获得类型注解的大部分好处,而不用担心增加任何成本或风险。
即便决定使用像TypeScript或者Flow这样的工具,也应该尽可能多的带上类型定义,这样就可以减少某些情况下发生的强类型推断。例如,原生JavaScript是不支持定义共享接口的。但使用TypeScript或 rtype都可以方便有效的定义接口。
Tern.js是一个流行的JavaScript类型推断工具,它在很多代码编辑器或IDE上都有插件。
微软的Visual Studio Code不需要Tern,因为它已经把TypeScript的类型定义功能带到了普通的JavaScript代码中去。
当在JavaScript函数中指定默认参数值时,很多类型推断工具就已经可以在IDE中给予提示以帮助正确的使用API。
没有默认值,各种IDE(更多的时候,连我们自己)都没有足够的信息来判断函数预期的参数类型。
没有默认值,userName
的类型是未知的。
有了默认值,userName
的类型是string。
通过默认值,IDE就可以显示userName
预计的输入是一个字符串。
将参数限制为固定类型(这会使通用函数和高阶函数更加受限)并不总是合理的。但是在这一方法合理时,使用默认参数通常就是最佳方式,即便是你已经在使用TypeScript或Flow做类型推断。
Mixin结构的工厂函数
工厂函数善于使用好的API创建对象。通常来说,它们基本满足需要,但不久之后,你就会遇到这样的情况,总会把类似的功能构建到不同类型的对象中,所以你需要把这些功能抽象为mixin函数,以便轻松重用。
mixin的工厂函数就要大显身手了。我们来构建一个withConstructor
的mixin函数,把.constructor
属性添加到所有的对象实例中。
with-constructor.js
:
1 | const withConstructor = constructor => o => { |
现在你就可以引入它并和其他的mixin一起使用了:
1 | import withConstructor from `./with-constructor'; |
如你所见,可重用的withConstructor()
mixin与其他mixin一起被简单地放入pipeline中。 withBattery()
可以用于其他类型的对象,如机器人,电动滑板或便携式设备充电器等等。 withFlying()
可以用来给飞行的汽车,火箭或飞行气球建模。
对象组合更多的是一种思维方式,而不是写代码的某一特定技巧。你可以在很多地方用到它。功能组合只是从头开始构建你思维方式的最简单的方法,工厂函数就是在对象组合有关实现细节方面包装一个友好的API的简单方法。
结论
ES6提供了一个方便的语法来处理对象创建和工厂函数。它们可以满足绝大多数需求,但这只因为它是JavaScript,还有一种方法使它更像Java:使用class
关键字。
在JavaScript中,类比工厂更加冗长和受限,而涉及到重构的话,这更像一个雷区,但是它们也被React和Angular等主要前端框架所接受,而且还有一些少见的情况使用,使得类更有存在意义。
“有时候,最优雅的实现方仅仅是一个函数。不是方法,不是类,也不是框架,仅仅只是一个函数。”–John Carmack.
从最简单的实现开始,只有在必要时才转移到更复杂的实现。涉及到对象时,这种变化看起来像这样:
纯函数 -> 工厂 -> 函数式 mixin -> 类