[翻译]JavaScript Factory Function with ES6+(ES6下的JS工厂函数)

原文地址:JavaScript Factory Functions with ES6+

工厂函数是一种不使用类或者构造函数来返回一个对象的函数。在JavaScript中,任何一个函数都可以返回一个对象。当该函数没有使用关键字new的时候,那么它就是一个工厂函数。

 

因为工厂函数能够轻松地产生对象而不入深入classnew关键字的复杂性,所以工厂函数在JavaScript中一直很受欢迎。

 

JavaScript 提供了一种十分友好的对象文法。就像下面示例中:

const user = {
    name:'test',
    age:18
}

 

就像JSON(JavaScript对象表示法)一样,在 : 左边是对象名,右边是对象属性。你可以通过 . 的方式来获取对象属性:

console.log(user.name); // test

 

同样,你也可以使用 [ ] 的方式来获取:

const keyValue= 'age', 
console.log(user[keyValue]]); //18

 

如果域内已经申明的变量与你想要的属性名称相同,你可以省略冒号和创建对象的值:

const name = 'test';
const age = '18';
const user = {
  name,
  age
};
console.log(user); // {name:'test', age:18}

 

对象文法支持简明的方法。我们可以直接添加一个 .setUserName() 方法:

const name = 'test';
const age = '18';
const user = {
  name,
  age,
  setUserName(name){
      this.name = name;
      return this;
  }
};
console.log(user.setUserName(name).name); // 'test'

 

这种方法通过 this 关键字指向调用该方法的对象。调用对象当中的一个方法,可以简单通过 . 文法并加上圆括号来调用。为了能够使用 . 文法,这一方法必须是函数的一个属性。另外,你也可以使用函数原型的方式来添加新方法, .call(), .apply() .bind()

 

在上面的例子当中,user.setUserName('test').setUserName() 绑定到对象user上了,因此这里 this == user..setUserName() 方法中,我们改变了user 对象的name属性并且也为方法提供相同的实例。

 

对象文法对应一个,工厂方法对应多个

如果你想要创建很多个新对象,你会想要把对象和工厂的能力组合起来使用。

 

通过一个工厂函数方法,你想要多少个对象就可以创建多少个。举个例子,如果你正在开发一个聊天应用,你可以用一个用户对象来代表当前的一个用户。同时,也会存在很多个其他用户对象来代表当前登录并且正在使用的用户,并且你也可以显示他们的用户名和头像。

 

现在,让我们把 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 关键字。

()=>'str'; // 这个表达式没有任何参数并且返回字符串'str'

 

在使用返回对象文法时需要小心。当你使用花括号 {} 时,JavaScript 会默认你想要创建一个函数主体,如 { broken: true } 。在使用隐式返回对象时,你想要避免使用花括号返回对象。

const noop = () => {foo:'bar'};
console.log(noop()); // undefined

const createFoo = () => ({ foo:'bar' });
console.log(createFoo())); // {foo:'bar'}

 

在第一个例子当中,foo 被推断是一个label 标签,而 bar 则被认为是一个没有被申明或者返回表达式。所以整个函数表达式返回 undefined

在第二个例子当中,圆括号强制把花括号解释为一个计算表达式而不是一个函数主体。

 

解构赋值

对于这个函数功能需要小心:

const createUser = ({ userName, avatar }) => ({

 

在这一行表达式当中,花括号当中代表了对象解构赋值。这个函数有一个入参(一个对象),但是从这个单一对象中解构出了2个形参,userNameavatar。这两个参数可以直接当成变量在函数主体中使用。同样,我们也可以对数组这样使用:

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 组成的数组 并把它转换成一个对象。因为我们不知道具体关键字的名字,所以我们需要计算属性名来在对象中设置 key/value 。为了达到这个目的,我们借鉴了方括号访问对象属性的方法来在对象上下文中构建对象:

{ [key]: value }

 

在完成了这一操作后,我们得到了最后的对象:

{ "foo": "bar" }

 

默认参数

JavaScript 中的函数支持设置默认参数,它带来了以下几个好处:

  1. 在满足的情况下,用户可以省略参数。
  2. 这个函数可读性更好,它本身就提供了预期入参的例子。
  3. IDE 和 静态分析工具能够使用默认参数进行类型推断。例如,如果默认参数是 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 FlowMicrosoft Typescript。目前作者在撰写文档时使用的是rtype——在函数式编程中比Typescript具有更高的可读性。

 

目前,对于类型申明还没有一个明确的胜者。JavaScript 规范中并没有推荐任何一种,因为它们或多或少都存在一些毛病。

 

类型推断是基于使用的上下文判断变量类型的一个过程。在JavaScript中,使用类型声明是一种很好的方式。

 

如果你在JavaScript函数声明中提供足够多的推断线索,你将会从类型推断中得到很多好处并且不会引人额外的开销和风险。

 

当你决定使用Typescript或者Flow的时候,你应该尽量多地利用类型推断,并且在类型推断不足的时候保存类型声明。举个例子,在JavaScript当中没有提供原生方法来申明公共接口。但是在Typescript或者rtype中是很容易且很有效的。

 

Tern.js 是JavaScript中一种很流行的类型推断工具并且在很多开发工具中都有插件。

 

Microsoft VSCode不需要Tern.js,它将Typescript的类型推断能力也带入到了常规的JavaScript当中。

 

当你在JavaScript当中定义函数的默认参数时,类型推断工具(Tern.js, Typescript和Flow)都能够提供IDE hints 来帮助你正确使用API。

 

没有默认参数的话,IDEs没有足够的提示来指出参数的具体类型(.ts文件中会默认指定any类型)。

 

带有默认参数的例子则可以推断出参数类型

 

将一个参数类型固定成一个具体类型并不总是有意义的(它会让泛型函数和高阶函数变得很困难)。当你需要的时候,默认参数会是一个最佳的完成方式,即使你使用的是Typescript和Flow。

 

工厂函数的Mixin组合

工厂函数非常擅长使用不错的API来处理对象。通常你会发现,这些对象都是你需要的,但是你会发现自己正在将类型的功能放进不同类型的对象中。这个时候,你会想要尝试抽象出这些功能放进一个功能性的Mixin当中,以便以后可以简单的重用。

 

这就是功能性Mixin 的好处所在。让我们来创建一个 withConstructor Mixin并向所有对象加入.constructor属性。

 

with-contructor.js

const withConstructor = (constructor) => (o) => ({
  // create the delegate [[Prototype]]
  __proto__: {
    // add the constructor prop to the new [[Prototype]]
    constructor,
  },
  // mix all o's props into the new object
  ...o,
});

 

现在你可以在其他Mixin当中调用它了。

import withConstructor from './with-constructor';
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
// or `import pipe from 'lodash/fp/flow';`
// Set up some functional mixins
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,
    getCapacity: () => 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.getCapacity() }
  battery status: ${ myDrone.draw(50).getCharge() }%
  battery drained: ${ myDrone.draw(75).getCharge() }% remaining
`);
console.log(`
  constructor linked: ${ myDrone.constructor === createDrone }
`);

 

上面的例子中,你可以看到withConstructor只是简单和其他Mixin一块放入管道中。withBattery()也可以使用其他类型的对象,withFlying()也是如此。

 

在代码当中,组合更多的是一种思考方式而不是一种具体的实现技术。你可以通过很多方式去实现它。函数组合只是一种最简单的构建方式,而工厂函数则是一种封装API实现细节的简单方式。

 

总结

ES6提供了一种方便的语法来创建对象和工厂函数。对你来说,大多数时候这就够用了。但是因为在JavaScript中,有另一种类似于Java实现的方式:使用class关键字。

 

在JavaScript当中,class要比工厂方式来得更加繁琐和严格,并且在重构方面也存在一些问题。但是,主流的前端框架,如ReactAngular,都包含了这种方式。对于解决一些少见的例子时,这种方式也是值得的。

 

“Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.” ~ John Carmack

有的时候,优雅的实现也仅是一个函数罢了。

 

从最简单的实现开始,仅仅在需要的时候考虑更加复杂的实现方式。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值