• JavaScript Factory Functions with ES6+
  • By Eric Elliott
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: lampui
  • Proofread by: IridescentMia, Sunui
Cubes to Smoke — MattysFlicks — (CC BY 2.0)

Note: This is part 8 of the “Writing Software” series, which focuses on learning functional programming and compositional software techniques from scratch in JavaScript ES6+. For the concept of software Composability, see Wikipedia Composability). There will be more exciting content, please look forward to! On a | < < < a > under the first article |

A factory function is a function that returns an object; it is neither a class nor a constructor. In JavaScript, any function can return an object, and if the function returns an object without the new keyword in front of it, that function is a factory function.

Factory functions have long been attractive in JavaScript because they provide the ability to easily generate object instances without delving into the complexities of classes and the new keyword.

JavaScript provides a very convenient syntax for object literals, which looks like this:

const user = {
  userName: 'echo',
  avatar: 'echo.png'
};Copy the code

Just like JSON syntax (JSON is a JavaScript based object literal syntax), (colon) is the property name on the left and the property value on the right. You can use the dot operator to access variables:

console.log(user.userName); // "echo"Copy the code

Or use square brackets and attribute names to access variables:

const key = 'avatar';
console.log( user[key] ); // "echo.png"Copy the code

If there is another variable in scope that has the same name as your property, you can use the variable directly in the object literal, eliminating the colon and property value:

const userName = 'echo';
const avatar = 'echo.png'; const user = { userName, avatar }; console.log(user); / / {"avatar": "echo.png"."userName": "echo" }Copy the code

Object literals support concise notation. We can add a.setUsername () method:

const userName = 'echo';
const avatar = 'echo.png';
const user = {
  userName,
  avatar,
  setUserName (userName) {
    this.userName = userName;
    returnthis; }}; console.log(user.setUserName('Foo').userName); // "Foo"Copy the code

In concise notation, this refers to the object calling the method. To call a method on an object, you simply access the method using the dot operator and call it with parentheses, such as game.play(), which calls.play() on the game object. To invoke a method using the dot operator, the method must be an object property. You can also apply a method to an object using the function prototype methods.call(),.apply(), or.bind().

In this case, user.setusername (‘Foo’) calls.setusername () on the user object, so this === user. In the.setUsername () method, we modify the.username value with the this reference and then return the same object instance for subsequent method chain calls.

Literals are biased toward a single object, and factory methods work with many objects

If you need to create multiple objects, you should consider using object literals in conjunction with factory functions.

Using the factory function, you can create as many user objects as you need. If you are developing a chat application, you will have one user object for the current user, and many user objects for other users who are logged in and chatting, to display their name, profile picture, and so on.

Let’s convert the User object to a createUser() factory method:

const createUser = ({ userName, avatar }) => ({
  userName,
  avatar,
  setUserName (userName) {
    this.userName = userName;
    returnthis; }}); console.log(createUser({ userName:'echo', avatar: 'echo.png'})); / * {"avatar": "echo.png"."userName": "echo"."setUserName": [Function setUserName]
}
*/Copy the code

Returns the object

The arrow function (=>) has the implicit return property: if the function body consists of a single expression, the return keyword can be omitted. ()=>’foo’ is a function that takes no arguments and returns the string “foo”.

Be careful when returning object literals. When using braces, JavaScript defaults you to create a function body, such as {broken: true}. If you need to return an explicit object literal, you need to disambiguate the object literal by enclosing it in parentheses, as shown below:

const noop = () => { foo: 'bar' };
console.log(noop()); // undefined
const createFoo = () => ({ foo: 'bar' });
console.log(createFoo()); // { foo: "bar" }Copy the code

In the first example, foo: is interpreted as a label, bar is interpreted as an expression that has not been assigned or returned, so the function returns undefined.

In the createFoo() example, the parentheses enforce the braces so that it is interpreted as an expression that requires a value, rather than a function body.

deconstruction

Pay special attention to function declarations:

const createUser = ({ userName, avatar }) => ({Copy the code

In this line, curly braces ({,}) denote object deconstruction. This function takes one parameter (that is, an object), but from that parameter, two parameters, userName and Avatar, are deconstructed. These parameters can be used as variables inside a function. Deconstruction can also be used for arrays:

const swap = ([first, second]) => [second, first]; console.log( swap([1, 2]) ); / / (2, 1)Copy the code

You can use extended syntax (… VarName) gets the remaining values of the array (or argument list) and passes them back to a single element:

const rotate = ([first, ...rest]) => [...rest, first]; console.log( rotate([1, 2, 3]) ); / / [2, 3, 1)Copy the code

Calculate attribute values

We used square brackets to dynamically access the value of an object’s property:

const key = 'avatar';
console.log( user[key] ); // "echo.png"Copy the code

We can also evaluate property values to assign values:

const arrToObj = ([key, value]) => ({ [key]: value });
console.log( arrToObj([ 'foo'.'bar' ]) ); // { "foo": "bar" }Copy the code

In this case, arrToObj takes an array of key-value pairs, also known as tuples, and converts it into an object. Since we do not know the property name, we need to calculate the property name to set the property value on the object. To do this, we use square bracket notation to set the property name and place it in the context of the object literal to create the object:

{ [key]: value }Copy the code

After the assignment, we get an object like this:

{ "foo": "bar" }Copy the code

The default parameters

JavaScript functions support default parameter values, giving us the following advantages:

  1. Users can omit parameters with appropriate defaults.
  2. The function is more self-descriptive because the default value provides an example of the expected input.
  3. Ides and static analysis tools can use default values to infer the types of parameters. For example, a default value1Indicates that the data type that the parameter can accept isNumber.

Using default parameters, we can describe the expected interface for our createUser factory function, and we can automatically fill in some details if the user does not provide the information:

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()
);Copy the code

The last part of the function signature might look a bit funny:

} = {}) => ({Copy the code

The = {} at the end of the argument declaration indicates that an empty object will be used as the default argument if the argument passed in does not meet the requirements. When you try to deconstruct an assignment from an empty object, the default value of the attribute is automatically filled in, because that’s what defaults do: replace undefined with a predefined value.

If there is no default value = {} and no valid argument is passed to createUser(), an error will be thrown because you cannot access the property from undefined.

Type judgment

At the time of this writing, JavaScript doesn’t have built-in type annotations, but a number of formatting tools or frameworks have emerged in recent years to fill in the gaps, These include JSDoc (which is declining due to better options), Facebook’s Flow, and Microsoft’s TypeScript. I personally use Rtype because I find it more readable than TypeScript for functional programming.

As of this writing, the types of annotation schemes are pretty much the same. None has taken refuge in the JavaScript specification, and each has its own obvious shortcomings.

Type inference is the process of inferring the type of a variable based on its context, and is a good alternative to type annotations in JavaScript.

If you provide enough clues to extrapolate in standard JavaScript functions, you can get most of the benefits of type annotations without worrying about any additional costs or risks.

Even if you do decide to use a tool like TypeScript or Flow, you should always take advantage of the benefits of type inference and save type annotations when type inference blows up. For example, native JavaScript does not support defining shared interfaces. However, interfaces can be defined easily and efficiently using TypeScript or RType.

Tern.js is a popular JavaScript type inference tool that has plug-ins for many code editors or ides.

Microsoft’s Visual Studio Code doesn’t need Tern because it builds TypeScript’s type inference capabilities into JavaScript Code.

When you specify default parameter values in JavaScript functions, many type inference tools such as tern.js, TypeScript, and Flow provide hints in the IDE to help developers use the API properly.

Without default values, ides (and, more often, ourselves) don’t have enough information to determine the type of arguments a function expects.

There is no default value, and the type of ‘userName’ is unknown.

With default values, the IDE (and, more often, ourselves) can infer types from the code.

With defaults, the IDE can prompt that the type of ‘userName’ should be a string.

Limiting arguments to fixed types (which makes general-purpose functions and higher-order functions even more limited) is not very sensible. But if it ever makes sense, using default arguments is usually the case, even if you’re already using TypeScript or Flow for type inference.

Factory functions for Mixin structures

Factory functions are good at creating objects using a good API. In general, they satisfy basic needs, but soon you’ll run into situations where similar functions are built into different types of objects, so you’ll need to abstract these functions into mixin functions for easy reuse.

Mixin’s factory functions are about to come into their own. Let’s build a mixin function withConstructor and add the.constructor attribute to all object instances.

with-constructor.js:

const withConstructor = constructor => o => {
  const proto = Object.assign({},
    Object.getPrototypeOf(o),
    { constructor }
  );
  return Object.assign(Object.create(proto), o);
};Copy the code

Now you can import and use other mixins:

import withConstructor from `./with-constructor'; const pipe = (... fns) => x => fns.reduce((y, f) => f(y), x); // 'import pipe from'lodash/fp/flow'; Const withFlying = o => {let isFlying = false; // set some 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, 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 } `);Copy the code

As you can see, the reusable withConstructor() mixin is simply put into the pipeline along with other mixins. The withBattery() can be used by other types of objects, such as robots, electric skateboards, or portable device chargers. WithFlying () can be used to model flying cars, rockets, or balloons.

Object composition is more a way of thinking than a specific technique of writing code. You can use it in many ways. Function composition is just the simplest way to build your way of thinking from scratch, and factory functions are a simple way to wrap the implementation details of object composition into a friendly API.

conclusion

ES6 provides a convenient syntax for object creation and factory functions that is sufficient most of the time, but because this is JavaScript, there is a more convenient and Java-like syntax: the class keyword.

In JavaScript, analogy factories are more verbose and limited, making them more problematic when refactoring code, but they are also adopted by mainstream front-end frameworks like React and Angular, and there are rare use cases that make classes more relevant.

“Sometimes, the most elegant implementation is just a function. Not methods, not classes, not frameworks. It’s just a function.” ~ John Carmack

Finally, remember not to make things complicated. Factory functions are not necessary. Your solution to a problem should be:

Pure functions > factory functions > functional Mixin > classes

Next: Why Composition is Harder with Classes >

The following

Want to learn more about JavaScript object composition?

Learn Javacript with Eric Elliott, now or never!

Eric Elliott is the author of “Write JavaScript Apps” (O ‘Reilly) and “Learn JavaScript with Eric Elliott.” He has contributed to many companies and organizations, such as Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN and BBC, and is a top artist for many organizations, Including but not limited to Usher, Frank Ocean and Metallica.

He spent most of his time in the San Francisco Bay Area with the most beautiful women in the world.

Thank JS_Cheerleader.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, React, front-end, back-end, product, design and other fields. If you want to see more high-quality translation, please continue to pay attention to the Project, official Weibo, Zhihu column.