In recent years, functional programming in JavaScript has become increasingly popular. While some of its often-published principles (such as invariance) require runtime workarounds, the language's first-class processing of functions proves its support for composable code driven by this basic primitive. Before introducing how to dynamically combine functions from other functions, let's briefly review it.
Key Points
What is a function?
In fact, a function is a procedure which allows execution of a set of imperative steps to perform side effects or return values. For example:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
When this function is called with an object with the firstName and lastName properties, getFullName returns a string containing the two corresponding values:
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
It is worth noting that since ES2015, JavaScript now supports arrow function syntax:
const getFullName = (person) => { return `${person.firstName} ${person.surname}`; };
Given that our getFullName function has a 1 (i.e. a single parameter) and only has one return statement, we can simplify this expression:
const getFullName = person => `${person.firstName} ${person.surname}`;
Although the methods of these three expressions are different, they all achieve the same purpose in the end:
Combining function through return value
In addition to assigning function return values to declarations (e.g. const person = getPerson();), we can also use them to fill the parameters of other functions, or generally provide values where JavaScript allows them. Suppose we have functions that perform logging and sessionStorage side effects:
const log = arg => { console.log(arg); return arg; }; const store = arg => { sessionStorage.setItem('state', JSON.stringify(arg)); return arg; }; const getPerson = id => id === 'homer' ? ({ firstName: 'Homer', surname: 'Simpson' }) : {};
We can do these operations on the return value of getPerson using nested calls:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
Given that the required parameters need to be provided to the function according to the way it is called, the innermost function will be called first. So, in the above example, the return value of getPerson will be passed to the log, and the return value of log will be forwarded to the store. By combining function calls to build statements, we can eventually build complex algorithms from atomic building blocks, but nesting these calls can become difficult to handle; what would it look like if we wanted to combine 10 functions?
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
Luckily, we can use an elegant general implementation: simplify the array of functions into higher order functions.
Cumulative array using Array.prototype.reduce
The reduce method of theArray prototype takes an array instance and accumulates it as a single value. If we want to sum a number array, we can use the following method:
const getFullName = (person) => { return `${person.firstName} ${person.surname}`; };
Combining function using Array.prototype.reduce
Now that we understand how to simplify arrays into single values, we can use this method to combine existing functions into new functions:
const getFullName = person => `${person.firstName} ${person.surname}`;
lazy evaluation.
So how do we combine other functions into a single higher order function?
const log = arg => { console.log(arg); return arg; }; const store = arg => { sessionStorage.setItem('state', JSON.stringify(arg)); return arg; }; const getPerson = id => id === 'homer' ? ({ firstName: 'Homer', surname: 'Simpson' }) : {};
The importance of call order
Ability to combine any number of functions using composing utilities to make our code more concise and abstract. However, we can highlight one point by revisiting the inline call:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
People may find it natural to copy this with our compose function:
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
In this case, why does fNested(4) === fComposed(4) resolve to false? You may remember that I emphasized how to interpret the internal call in the first place, so compose(g, h, i) is actually equivalent to x => i(h(g(x))), so fNested returns 10 and fComposed returns 9. We can simply reverse the call order of nested or combined variants of f, but given the specificity of compose to mirror nested calls, we need a way to reduce the function in right-to-left order; JavaScript is lucky to use Array.prototype.reduceRight provides this:
const getFullName = (person) => { return `${person.firstName} ${person.surname}`; };
With this implementation, both fNested(4) and fComposed(4) resolve to 10. However, our getPersonWithSideEffects function is now incorrectly defined; although we can reverse the order of the internal functions, in some cases, reading from left to right can facilitate the psychological analysis of program steps. It turns out that our previous methods have been quite common, but are often called pipe:
const getFullName = person => `${person.firstName} ${person.surname}`;
By using our pipe function, we will keep the left-to-right order required by getPersonWithSideEffects. Pipelines have become a major component of RxJS for reasons outlined above; in this order, it may be more intuitive to think about data streams in combination streams operated by operators.
Function combinations as alternatives to inheritance
We have seen in the previous example how to combine infinite multiple functions into larger, reusable, target-oriented units. Another benefit of function combination is to free yourself from the rigidity of inheritance graphs. Suppose we want to reuse logging and storage behavior based on the class hierarchy; it can be expressed like this:
const log = arg => { console.log(arg); return arg; }; const store = arg => { sessionStorage.setItem('state', JSON.stringify(arg)); return arg; }; const getPerson = id => id === 'homer' ? ({ firstName: 'Homer', surname: 'Simpson' }) : {};
Apart from the length, the direct problem with this code is that we abuse inheritance to implement reuse; if another class extends Loggable, it is also inherently a subclass of the Storage, even if we don't need this logic. A potentially more serious problem is naming conflicts:
function getFullName(person) { return `${person.firstName} ${person.surname}`; }
If we want to instantiate MyState and call its store method, we will not call the Storeable store method unless we add a call to super.store() in MyState.prototype.store, but this will be in State Creates a tight, fragile coupling between Storable. This can be mitigated using entity systems or policy patterns, as I have introduced elsewhere, but despite the advantages of inheriting the broader taxonomy of expression systems, function combinations provide a flat, concise way to share without dependency on Code for method name.
Summary
JavaScript handles functions as values and generates their expressions, making it easy to combine larger, context-specific jobs. Thinking of this task as a cumulative of function arrays eliminates the need for imperative nested calls, and using higher-order functions results in separation of their definitions and calls. Furthermore, we can get rid of the strict hierarchical constraints imposed by object-oriented programming.
FAQs about function combinations in JavaScript
Function combination is a basic concept in JavaScript and large functional programming. It allows developers to create complex functions by combining simpler functions, thereby improving the reusability and modularity of their code. This approach makes the code easier to understand, debug and test. It also encourages the principle of "don't repeat yourself" (DRY) to reduce redundancy in the code base.
High-order functions are a key part of function combinations in JavaScript. A higher-order function is a function that can take one or more functions as parameters, return a function as a result, or perform both operations at the same time. In function combinations, we often use higher-order functions to create new functions by combining existing functions.
Of course, let's consider a simple example. Suppose we have two functions, double and increment. double takes a number and multiplies it by 2, and increment adds 1 to its input. We can combine these two functions to create a new function that doubles a number and increments the result.
const character = { firstName: 'Homer', surname: 'Simpson', }; const fullName = getFullName(character); console.log(fullName); // => 'Homer Simpson'
Function combination and function chain are two different ways to combine functions in JavaScript. Function combinations involve passing the output of one function as input to another. On the other hand, a function chain involves calling multiple functions in order, where each function calls the result of the previous function. Although both techniques can achieve similar results, function combinations are more in line with the principles of functional programming.
Function combinations facilitate the creation of small pure functions that do only one thing and do well. These functions are easier to test and debug than large monolithic functions. Since each function is independent, you can test it individually without worrying about the rest of the code. This makes it easier to find and fix bugs in the code.
Yes, function combinations can be used with asynchronous functions in JavaScript. However, this requires more attention, as you need to make sure that the output of an asynchronous function is correctly passed to the next function as input. This usually involves using promise or async/await syntax.
Although there are many benefits to function combinations, it may also introduce complexity if used improperly. For example, deeply nested function calls can be difficult to read and understand. Furthermore, if the combined functions are not pure functions (i.e. they have side effects), it may lead to unexpected results. Therefore, it is important to use function combinations with caution and combine them with good coding practices.
Curriization is a technique in JavaScript where a function with multiple parameters is converted into a series of functions, each function has only one parameter. Currying can be used with functions combinations to create more flexible and reusable functions. In fact, some utility libraries such as lodash and Ramda provide both currying and combinatorial functions.
Yes, function combinations can be used with JavaScript frameworks such as React or Vue. In fact, in React, combining components to build complex user interfaces is a common pattern. Similarly, Vue's mixin system can be regarded as a form of function combinations.
Yes, there are several libraries that provide utilities for function combinations in JavaScript. Some popular libraries include lodash, Ramda, and Redux (for state management). These libraries provide functions such as compose or pipe, making combining functions easier and more efficient.
The above is the detailed content of Function Composition in JavaScript with Array.prototype.reduceRight. For more information, please follow other related articles on the PHP Chinese website!