For us programmers, writing functions is all too familiar. Whatever we want to implement, we need to do it through functions. In JavaScript, functions have very high privileges, even first-class citizens, and thus support multiple programming paradigms just like Kotlin.

Today I mainly want to talk to you about some advanced techniques when writing functions, roughly as follows:

  • Pure functions
  • Higher-order functions
  • Function cache
  • Lazy function
  • Currie,
  • Function composition

Pure functions

A pure function must satisfy two conditions:

  1. Returns the same result for the same argument
  2. It doesn’t produce any side effects

Look at the following code:

function double(num){
  return num * 2 
}
Copy the code

As long as I give num the same value, it returns the same thing, and there’s no impact on the outside world, so it’s a pure function.

And:

const counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()
Copy the code

This function has a different result each time it is executed, so it is not pure.

And:

let count = 0;
function isYoung(user){
  if(user.age <= 20){
    count++;
    return true
  }
  return false
}
Copy the code

Here it gives the same result each time given the same input, but it operates on external variables, which has the side effect of not being pure.

What’s good about pure functions?

Why do we distinguish pure functions from other functions? Because pure functions can improve the quality of our code as we code.

  1. Pure functions are much cleaner and easier to understand

Each pure function performs a specific task, and we can predict the outcome from the input

  1. Optimizations can be made for pure function compilers

For example, we have the following code:

for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}
Copy the code

If fun is not a pure function, then fun(10) will be executed 1000 times, but if fun is a pure function, then since its output is determined for a given input, the above code can be optimized to:

const result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}
Copy the code
  1. Pure functions are easier to test

Tests for pure functions do not depend on external factors, and thanks to the nature of pure functions, we can simply write unit tests for pure functions by giving them an input and deciding if the output is as expected.

Using the double(num) function above, we can write unit tests like this:

const x = 1;
assert.equals(double(x),2);
Copy the code

If it weren’t pure functions, we’d have a lot of external factors to consider, such as mock data.

Higher-order functions

A higher-order function must satisfy at least one of the following conditions:

  1. Take a function as an argument
  2. Return the function as the result

For those of you who don’t know functional programming, this might sound a little weird, but a function is supposed to compute something, and it returns another function, so what does that do? Alas, this is useful. Using higher-order functions makes our code much simpler and more flexible.

Let’s take a concrete example. Let’s say we have an array, and we want to use it to create a new array, and each element of the new array is the element plus 1 of the corresponding position in the previous array.

Instead of higher-order functions, we might write something like this:

const arr1 = [1.2.3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] + 1);
}
Copy the code

But JavaScript array objects have a map method. This map method takes a callback that is applied to each element of the current array object, returning a new array.

const arr1 = [1.2.3];
const arr2 = arr1.map(function(item) {
  return item + 1;
});
console.log(arr2);
Copy the code

Does our code look cleaner? The map function is a higher-order function, and we can quickly see at a glance that this code declares the transformation of the original object, creating a new array based on the elements of the original array object. Higher order functions are more powerful than that, so let’s move on.

Function cache

Suppose we have a time-consuming pure function:

function computed(str) {    
    // Consider this a time-consuming calculation
    console.log('Executed for 10 minutes')
    // This is the calculated result
    return I figured it out.
}
Copy the code

To avoid unnecessary double counting, we can cache some results that have been calculated before. So when we do the same computation later, we can just pull the result out of the cache. What we need to do here is write a function called cached to wrap the actual function we’re calling, which takes the target function as an argument and returns a new function. In this cached function, we cache the results of previous function calls.

Function cached(fn){const cache = object.create (null); Return function cachedFn (STR) {return function cachedFn (STR) {// If there is no cache, we will execute the target function if (! cache[str] ) { let result = fn(str); Cache [STR] = result; } return cache[str] } }Copy the code

And we can see that when we put in the same parameters we can get the result straight away.

Lazy function

Inside the function body contains a variety of conditional statements, sometimes these conditional statements need to be performed only once, for example when we write a singleton determine whether an object is empty, we will create an object, if is empty that we actually know the follow-up as long as the program is still running, the object is not empty, But every time we use it, we’re still going to see if it’s null, we’re going to perform our condition. We can improve the performance a little bit by removing these criteria after the first execution, so that they can be used without being null, which is a lazy function.

Let’s put the above description into simple code:

let instance = null;
function user() {
    if( instance ! =null) {
      return instance;
    } else {
      instance = new User()
      returninstance; }}Copy the code

The above code performs a condition judgment every time it is executed. However, if our condition judgment is very complex, this can be a significant performance impact. In this case, we can use the lazy function trick to optimize the code:

var user = function() {
    var instance = new User();
    user = function() {
        return instance;
    };
    return user();
}
Copy the code

So after the first execution, we rewrite the previous function with a new function, and every subsequent execution of the function returns a fixed value, which will definitely improve the performance of our code. So if we encounter a conditional statement that is executed only once, we can optimize it with lazy functions and remove the conditional statement by overwriting the original function with a new function.

The function is curialized

Currie, is simply a accept multiple parameters of the function into a series of function takes a single argument, so it might be a little around, is actually a one-time accept a heap of parameter function, converted to accept the first parameter is the function of a accept the second parameter, the function returns a accept return to a third parameter for the fourth parameter function, And so on.

Maybe a lot of you are seeing it for the first time and you don’t know what it’s for, why do you want to do all this fancy stuff when you can do it all at once?

  1. Currying allows us to avoid passing the same value repeatedly
  2. This is essentially creating a higher-order function that we can use to process the data

Let’s look at a simple summation function that takes three numbers as arguments and returns their sum.

function sum(a,b,c){
 return a + b + c;
}
Copy the code

It can be successfully called with a few more or fewer arguments:

The sum (1, 2, 3) -- -- > 6 sum (1, 2) -- - > NaN sum (1, 2, 3, 4) -- - > 6 / / the extra parameter is ignoredCopy the code

So how can we turn this into a Curryized version?

function curry(fn) {
    if (fn.length <= 1) return fn;
    const generator = (. args) = > {
        if (fn.length === args.length) {

            returnfn(... args) }else {
            return (. args2) = > {

                returngenerator(... args, ... args2) } } }return generator
}
Copy the code

Here’s an example:

We can get the same result as we passed all the parameters in the previous shuttle, and we can also cache the results of the previous calculation in any step. For example, if we pass (1,2,3,6) this time, we can avoid double-counting the first three parameters.

Function composition

Suppose we wanted to implement a function that multiplied a given number by 10 and output it as a string, we would need to do two things:

  • Given a number multiplied by 10
  • Number to string

When we got it, it would read something like this:

const multi10 = function(x) { return x * 10; };
const toStr = function(x) { return `${x}`; };
const compute = function(x){
    return toStr(multi10(x));
};
Copy the code

There are only two steps here, so it doesn’t look complicated, but the reality is that if there are more operations, the layer nesting is ugly and error prone, something like this: fn3(fn2(fn1(fn0(x)))). To avoid this and flatline the call hierarchy, we can write a compose function for composing function calls together:

const compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};
Copy the code

Then we can write our compute function like this:

let `compute` = compose(toStr, multi10);
compute(8);
Copy the code

By using the compose function, we can compose two functions into one. This makes the code more intuitive by executing from right to left, rather than calculating the result of one function as an argument to another. For example, compose supports only two parameters, so we can write a version that supports any parameters:

function compose(... funs){ return (x)=>funs.reduce((acc, fun) => fun(acc), x) }Copy the code

Our compose function now has no limit on the number of parameters:

Through the combination function, we can be able to specify the declarative function, the connection between the readability of the code is also greatly improved, also facilitate our subsequent to extension and reconstruction of the code, and in the React when we have more high order component change, with a very ugly, a set of we can in a similar way to make our high order component hierarchy flattening.

Well, that’s it for today. There are a lot of tricks we can use to make our code more elegant