[javaScript Core] High-order functions

Foreword

“In JavaScript, functions are first-class citizens.” We can always see this sentence in various books and articles.

The popular explanation is: functions in JS are also objects. They can have attributes, can be assigned to a variable, can be placed in an array as elements, and can be used as attributes of other objects. It can do what ordinary objects can do, but ordinary objects cannot. It can also be done.

The so-called higher-order function is a function whose input parameters have functions or whose output is a function.

The most common higher-order functions are map(), reduce(), filter(), sort() , setTimeout, setInterval and Ajax requests, we call it a callback function because it passes a function as a parameter to another function.

Another commonly seen scenario for higher-order functions is to output another function inside a function, such as a closure, as well as currying, decurrying and partial functions to be discussed next.

Currying

Definition

  • The Little Red Book (3rd Edition): Used to create functions with one or more parameters already set. The basic approach is to use a closure to return a function. (P604)
  • Wikipedia: Currying (English: Currying) is to transform a function that accepts multiple parameters into a function that accepts a single parameter (the first parameter of the original function), and returns a new function that accepts the remaining parameters and returns the result. Technology. (original link)
  • The principle of currying is to use closures to form a private scope that is not destroyed, store all pre-processed content in this scope that is not destroyed, and return a function, which will be executed in the future.

I’m a little confused by the official explanation, so I’ll summarize it in plain English:

Currying (Currying), also known as Partial Evaluation, is to transform an original function that accepts multiple parameters into a function that accepts a single parameter (the first parameter of the original function). And returns a new function that can accept the remaining parameters and finally returns the same result as the original function.

The core idea is to split the function passed in with multiple parameters into a single (or partial) parameter function, and then internally return to call the next single (or partial) parameter function to process the remaining parameters in sequence.

Application

Currying has 3 common applications:

  • Parameter reuse – When the same function is called multiple times and most of the parameters passed are the same, then the function may be a good candidate for currying
  • Early return – multiple internal judgments are called multiple times, and the result of the first judgment can be directly returned to the external receiver.
  • Delay calculation/execution – avoid executing programs repeatedly and wait until the results are actually needed.

Universal implementation

// ES5 way
function currying(fn) {<!-- -->
  var rest1 = Array.prototype.slice.call(arguments)
  rest1.shift()
  return function() {<!-- -->
    var rest2 = Array.prototype.slice.call(arguments)
    return fn.apply(null, rest1.concat(rest2))
  }
}

// ES6 way
function currying(fn, ...rest1) {<!-- -->
  return function(...rest2) {<!-- -->
    return fn.apply(null, rest1.concat(rest2))
  }
}

Try using it to curry a sayHello function:

//Continue above
function sayHello(name, age, fruit) {<!-- -->
  console.log(`My name is ${<!-- -->name}, I am ${<!-- -->age} years old, and I like to eat ${<!-- -->fruit}`)
}

var curryingShowMsg1 = currying(sayHello, 'Xiao Ming')
curryingShowMsg1(22, 'Apple') // Output: My name is Xiao Ming, I am 22 years old, and I like to eat apples

var curryingShowMsg2 = currying(sayHello, '小肖', 20)
curryingShowMsg2('Watermelon') // Output: My name is Xiaoshuai, I am 20 years old, I like to eat watermelon

Application 1: Parameter reuse

The following function named uri receives 3 parameters. The function of the function is to return a string concatenated by the three parameters.

function uri(protocol, hostname, pathname) {<!-- -->
  return `${<!-- -->protocol}${<!-- -->hostname}${<!-- -->pathname}`;
}

// have a test
const uri1 = url('https://', 'www.fedbook.cn', '/function-curring/')
console.log(uri1)

The disadvantage of the above writing method is that when we have many URLs, it will lead to a lot of repeated parameters (for example, https:// is a repeated parameter, and we don’t need to enter the URL in the browser. Enter http or https).

The idea of using currying to achieve parameter reuse:

  • The original function (called function A) only sets one parameter (the parameter of the receiving protocol);
  • Create another function (called function B) inside the function. Function B fills in the remaining two parameters and returns the url in string form;
  • Function A returns function B.
function uri_curring(protocol) {<!-- -->
  return function(hostname, pathname) {<!-- -->
    return `${<!-- -->protocol}${<!-- -->hostname}${<!-- -->pathname}`;
  }
}

// have a test
const uri_https = uri_curring('https://');

const uri1 = uri_https('www.fedbook.cn', '/frontend-knowledge/javascript/function-currying/');
const uri2 = uri_https('www.fedbook.cn', '/handwritten/javascript/10-implement bind method/');
const uri3 = uri_https('www.myblog.com', '/');

console.log(uri1);
console.log(uri2);
console.log(uri3);

Application 2: Compatibility Check

::: warning
For the convenience of writing, the following code will use ES6 syntax. If you want to use the compatibility detection function in the actual production environment, you need to convert it to ES5 syntax.
:::

Due to the development of browsers and various reasons, some functions and methods are not supported by some browsers. At this time, it is necessary to make a judgment in advance to determine whether the user’s browser supports the corresponding method.

Taking event listening as an example, IE (before IE9) supports the attachEvent method, and other mainstream browsers support the addEventListener method. We need to create a new function to do so. Judgment of both.

const addEvent = function(element, type, listener, useCapture) {<!-- -->
  if(window.addEventListener) {<!-- -->
    console.log('Judged to be another browser')
    // Same function as native addEventListener
    // element: element to which event monitoring needs to be added
    // type: What type of event to add to the element
    // listener: callback function executed
    // useCapture: Selection of event bubbling or event capture
    element.addEventListener(type, function(e) {<!-- -->
      // In order to avoid this pointing problem, use call to bind this
      listener.call(element, e);
    }, useCapture);
  } else if(window.attachEvent) {<!-- -->
    console.log('Judged to be a browser below IE9')
    //Native attachEvent function
    // The fourth parameter is not needed because IE supports event bubbling.
    // Splice one more on, so that you can use the event type in a unified writing form
    element.attachEvent('on' + type, function(e) {<!-- -->
      listener.call(element, e);
    });
  }
}

// have a test
let div = document.querySelector('div');
let p = document.querySelector('p');
let span = document.querySelector('span');

addEvent(div, 'click', (e) => {<!-- -->console.log('clicked div');}, true);
addEvent(p, 'click', (e) => {<!-- -->console.log('clicked p');}, true);
addEvent(span, 'click', (e) => {<!-- -->console.log('span was clicked');}, true);

The disadvantage of the above encapsulation is that every time the addEvent function is called when writing a listening event, a compatibility judgment of if...else... will be performed. In fact, you only need to perform a compatibility judgment once in the code, and dynamically generate a new function based on the result of one judgment, so there is no need to recalculate it in the future.

So how to use function currying to optimize this encapsulated function?

//Use the immediate execution function. When we put this function at the head of the file, we can first perform execution judgment.
const addEvent = (function() {<!-- -->
  if(window.addEventListener) {<!-- -->
    console.log('Judged to be another browser')
    return function(element, type, listener, useCapture) {<!-- -->
      element.addEventListener(type, function(e) {<!-- -->
        listener.call(element, e);
      }, useCapture);
    }
  } else if(window.attachEvent) {<!-- -->
    console.log('Judged to be a browser below IE9')
    return function(element, type, handler) {<!-- -->
      element.attachEvent('on' + type, function(e) {<!-- -->
        handler.call(element, e);
      });
    }
  }
}) ();

// have a test
let div = document.querySelector('div');
let p = document.querySelector('p');
let span = document.querySelector('span');

addEvent(div, 'click', (e) => {<!-- -->console.log('clicked div');}, true);
addEvent(p, 'click', (e) => {<!-- -->console.log('clicked p');}, true);
addEvent(span, 'click', (e) => {<!-- -->console.log('span was clicked');}, true);

Because the above encapsulation executes the function immediately, triggering multiple events will still trigger only one if conditional judgment.

Two features of function currying are used here: early return and delayed execution.

Application 3: Implement an add function

This is a classic interview question, which requires us to implement an add function that can achieve the following calculation results:

add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

This question can explain the delayed execution of currying, and go directly to the code:

function add() {<!-- -->
  // Convert the incoming variable parameters into an array object
  let args = Array.prototype.slice.call(arguments);

  // Recursion: Call yourself inside the internal function
  // When the add function is called continuously, add the parameter of the N + 1th bracket to the parameter of the Nth bracket
  let inner = function() {<!-- -->
    args.push(...arguments);
    return inner;
  }
  
  inner.toString = function() {<!-- -->
    //The values in args are continuously accumulated
    return args.reduce(function(prev, cur) {<!-- -->
      return prev + cur;
    });
  };

  return inner;
}

// have a test
let result = add(1)(2)(3)(4);
console.log(result);

Explain a few key points:

1) Indefinite parameters arguments need to be converted into array objects:

Because arguments is not a real array, but an array-like object, Array.prototype.slice.call(arguments) can convert an object with length The attribute object is converted into an array.

2) Features of toString invisible conversion:

For add(1)(2)(3)(4), the inner function is returned when executing each bracket, and it calls itself continuously. Each time the inner function returns are all internal functions.

If you print the final return result of function execution, you can find that a string is returned (the original function is converted to a string and returned). This is an implicit conversion that occurs, and the implicit conversion occurs because the internal toString method.

Knowing this, we can use this feature to customize the returned content: override the toString method of the inner function, and implement the execution code for parameter addition inside it.

It is worth mentioning that this kind of processing can return the correct cumulative result, but the returned result is a function type (function). This is because we are using a recursive return function and the internal function is hidden. Before the expression is converted into a string, it is a function.

Anti-currying

Definition

Currying fixes some parameters and returns a function that accepts the remaining parameters. It is also called a partial calculation function. The purpose is to narrow the scope of application and create a more targeted function. The core idea is to split the function passed in with multiple parameters into a single-parameter (or partial) function, and then internally return to call the next single-parameter (or partial) function to process the remaining parameters in sequence.

And anti-currying, literally speaking, the meaning and usage are exactly the opposite of function currying. It expands the scope of application and creates a function with a wider range of applications. Expand methods that are only applicable to specific objects to more objects.

Universal implementation

// ES5 way
Function.prototype.unCurrying = function() {<!-- -->
  var self = this
  return function() {<!-- -->
    var rest = Array.prototype.slice.call(arguments)
    return Function.prototype.call.apply(self, rest)
  }
}

// ES6 way
Function.prototype.unCurrying = function() {<!-- -->
  const self = this
  return function(...rest) {<!-- -->
    return Function.prototype.call.apply(self, rest)
  }
}

If you feel that it is not good to put the function on the prototype of Function, you can also do this:

// ES5 way
function unCurrying(fn) {<!-- -->
  return function (tar) {<!-- -->
    var rest = Array.prototype.slice.call(arguments)
    rest.shift()
    return fn.apply(tar, rest)
  }
}

// ES6 way
function unCurrying(fn) {<!-- -->
  return function(tar, ...argu) {<!-- -->
    return fn.apply(tar, argu)
  }
}

Let’s briefly try out the general implementation of anti-currying. We borrow the push method on Array to add an element to an array like arguments:

//Continue above
var push = unCurrying(Array.prototype.push)

function execPush() {<!-- -->
  push(arguments, 4)
  console.log(arguments)
}

execPush(1, 2, 3) // Output: [1, 2, 3, 4]

Difference

Simply put, function currying is the process of reducing the order of high-order functions, narrowing the scope of application, and creating a more targeted function.

function(arg1, arg2) // => function(arg1)(arg2)
function(arg1, arg2, arg3) // => function(arg1)(arg2)(arg3)
function(arg1, arg2, arg3, arg4) // => function(arg1)(arg2)(arg3)(arg4)
function(arg1, arg2, ..., argn) // => function(arg1)(arg2)…(argn)

Anti-currying is the opposite, increasing the scope of application and making the method usage scenarios larger. Using decurrying, you can lend out native methods so that any object has the methods of the native object.

obj.func(arg1, arg2) // => func(obj, arg1, arg2)

The difference between currying and anti-currying can be understood this way:

  • Currying is to pass parameters in advance before operation, and multiple parameters can be passed.
  • Anti-currying is delayed parameter passing. During operation, the originally fixed parameters or this context are delayed as parameters until the future.

Partial function

A partial function is to create a function that calls another part (a function with pre-prepared parameters or variables). The function can generate a function that is actually executed based on the parameters passed in. It does not include the logic code we really need. It just returns other functions based on the parameters passed in. Only the returned functions have real processing logic, such as:

var isType = function(type) {<!-- -->
  return function(obj) {<!-- -->
    return Object.prototype.toString.call(obj) === `[object ${<!-- -->type}]`
  }
}

var isString = isType('String')
var isFunction = isType('Function')

In this way, a set of methods for determining object types is quickly created using partial functions.

The difference between partial functions and currying:

  • Currying is to change a function that accepts n parameters from passing all parameters at once and executing them to accepting parameters and executing them multiple times, for example: add = ( x, y, z) => x + y + z→curryAdd = x => y => z => x + y + z.
  • Partial function fixes a certain part of the function and returns a new function to accept the remaining parameters through the passed parameters or methods. The number may be one or multiple

When a curried function only accepts two parameters, such as curry()(), the concept of the curried function is similar to that of a partial function. The partial function can be considered to be a curried function. Degraded version.

(over)