JavaScript functional programming is a topic that has been around for a long time, but it seems to be getting hotter and hotter since 2016. This could be because ES6 syntax is more friendly to functional programming, or it could be because of the popularity of functional frameworks such as RxJS (ReactiveX).

I’ve read a lot about functional programming, but most of it is theoretical, and some of it is for purely functional programming languages like Haskell. The purpose of this article is to talk about what I consider to be the actual practice of functional programming in JavaScript. The reason why THIS is “in my eyes” means that what I say is only my personal opinion, which may conflict with some strict concepts.

This article will skip a bunch of formal concepts and focus on what functional code is in JavaScript, how it differs from normal writing, what it can do for you, and what some of the most common functional models are.

I understand functional programming

In my opinion, functional programming can be understood as the programming method with functions as the main carrier, and functions are used to disassemble and abstract general expressions

What’s the advantage of doing this as opposed to imperative? The main points are as follows:

  • More semantic clarity
  • Higher reusability
  • Better maintainability
  • Limited scope and fewer side effects

Basic functional programming

The following example is a concrete functional representation

// Capitalize each word in the array





//

const arr = ['apple', 'pen', 'apple-pen'];

for(const i in arr){

const c = arr[i][0];

arr[i] = c.toUpperCase() + arr[i].slice(1);

}



console.log(arr);





// This is a function

function upperFirst(word) {

return word[0].toUpperCase() + word.slice(1);

}



function wordToUpperCase(arr) {

return arr.map(upperFirst);

}



console.log(wordToUpperCase(['apple', 'pen', 'apple-pen']));





// We can write it as a function

console.log(arr.map(['apple', 'pen', 'apple-pen'], word => word[0].toUpperCase() + word.slice(1)));

Copy the code

When things get more complicated, there are several problems with the expression:

  1. Meaning is not obvious and gradually becomes difficult to maintain
  2. Poor reusability results in more code
  3. There are a lot of intermediate variables

Functional programming is a good solution to the above problems. First, see functional notation 1, which takes advantage of function encapsulation to unpack functions (not unique in granularity) into different functions, and then use combined calls to achieve the purpose. This makes the presentation clear and easy to maintain, reuse, and extend. Second, use the higher-order function array. map instead of for… Of does array traversal, reducing intermediate variables and operations.

The main difference between functional writing 1 and functional writing 2 is that you can consider whether the function is likely to be reused later, and if not, the latter is preferable.

Chain optimization

As we can see from the above functional writing method 2, functional code in the process of writing, it is easy to cause horizontal extension, that is, to create multiple layers of nesting, let’s take an extreme example.

// Add the numbers





//

console.log(1 + 2 + 3 - 4)





// Function

function sum(a, b) {

return a + b;

}



function sub(a, b) {

return a - b;

}



console.log(sub(sum(sum(1, 2), 3), 4);

Copy the code

This example shows only the extreme case of horizontal scaling, where as the number of nesting layers of functions increases, code becomes much less readable and error-prone.

In this case, we can consider a variety of optimization approaches, such as the following chain optimization.

// Optimized writing (yes, you read that right, this is the chain writing of Lodash)

const utils = {

chain(a) {

this._temp = a;

return this;

},

sum(b) {

this._temp += b;

return this;

},

sub(b) {

this._temp -= b;

return this;

},

value() {

const _temp = this._temp;

this._temp = undefined;

return _temp;

}

};



console.log(utils.chain(1).sum(2).sum(3).sub(4).value());

Copy the code

After this rewriting, the structure becomes clearer as a whole, and it is easy to show what each link of the chain is doing. Another good example of nested versus chained functions is the callback function versus the Promise pattern.

// Request both interfaces sequentially





// The callback function

import $ from 'jquery';

$.post('a/url/to/target', (rs) => {

if(rs){

$.post('a/url/to/another/target', (rs2) => {

if(rs2){

$.post('a/url/to/third/target');

}

});

}

});





// Promise

import request from 'catta'; Catta is a lightweight request tool that supports FETCH, JSONP, Ajax, and no dependencies

request('a/url/to/target')

.then(rs => rs ? $.post('a/url/to/another/target') : Promise.reject())

.then(rs2 => rs2 ? $.post('a/url/to/third/target') : Promise.reject());

Copy the code

As the nested levels and single-layer complexity of the callback function increased, it became bloated and difficult to maintain, whereas Promise’s chained structure, at high complexity, was vertically scalable and hierarchically isolated.

A common functional programming model

Closure

A block of code that can keep local variables from being released is called a closure

The concept of closures is quite abstract, and I’m sure you all know and use this feature to some extent

So what exactly do closures do for us?

Let’s see how to create a closure:

// Create a closure

function makeCounter() {

let k = 0;



return function() {

return ++k;

};

}



const counter = makeCounter();



console.log(counter()); / / 1

console.log(counter()); / / 2

Copy the code

The code block of makeCounter refers to the local variable K in the returned function, so that the local variable cannot be recycled by the system after the function is executed, resulting in the closure. The purpose of this closure is to “preserve” the local variable so that it can be reused when the inner function is called; Unlike global variables, this variable can only be referenced inside a function.

In other words, closures simply create “persistent variables” that are private to the function.

So from this example, we can conclude that closure creation conditions are:

  1. There are inner and outer functions
  2. The inner function refers to local variables of the outer function

The purpose of closures

The main purpose of closures is to define scoped persistence variables that can be used for caching, intermediate calculations, and so on.

// A simple cache tool

// The anonymous function creates a closure

const cache = (function() {

const store = {};



return {

get(key) {

return store[key];

},

set(key, val) {

store[key] = val;

}

}

} ());



cache.set('a', 1);

cache.get('a'); / / 1

Copy the code

The example above is an implementation of a simple caching tool. Anonymous functions create a closure so that store objects can always be referenced without being recycled.

Disadvantages of closures

Persistent variables will not be released normally and will continue to occupy memory space, which can easily cause memory waste, so some additional manual cleanup mechanism is usually required.

Higher-order functions

A function that takes or returns a function is called a higher-order function

It sounds like a very cold word, but actually we use it a lot, we just don’t know their names. The JavaScript language natively supports higher-order functions because JavaScript functions are first-class citizens that can be used either as arguments or as the return value of another function.

We often see many native higher-order functions in JavaScript, such as array. map, array. reduce, and array. filter

Let’s take map as an example and see how it is used

Map (map)

Mapping is for sets, that is, to do the same transformation on every term of a set, to produce a new set

Map is a higher-order function that takes a function argument as the logic of the mapping

// Add one to each item in the array to form a new array





//

Const arr = [1, 2, 3];

const rs = [];

for(const n of arr){

rs.push(++n);

}

console.log(rs)





/ / map

Const arr = [1, 2, 3];

const rs = arr.map(n => ++n);

Copy the code

For… Iterating through the array in an of loop creates additional operations and risks changing the array

The map function encapsulates the necessary operations so that we only need to worry about the function implementation of the mapping logic, reducing the amount of code and reducing the risk of side effects.

Currying

Given some arguments to a function, generate a new function that takes other arguments

You may not hear this term very often, but anyone who has used undescore or Lodash has seen it.

There is an amazing _. Partial function that is an implementation of Currization

// Get the relative path of the target file to the base path





//

const BASE = '/path/to/base';

const relativePath = path.relative(BASE, '/some/path');





/ / _. Parical rewrite

const BASE = '/path/to/base';

const relativeFromBase = _.partial(path.relative, BASE);



const relativePath = relativeFromBase('/some/path');

Copy the code

With _. Partial, we get a new function relativeFromBase, which acts as a call to path.relative and passes the first argument to BASE by default, followed by subsequent arguments.

In this case, what we really want to do is get a path relative to BASE every time, not relative to any path. Currization allows us to care only about some parameters of a function, making the function’s purpose clearer and calling easier.

Composing (Composing)

Create a new function by combining the capabilities of multiple functions

Again, you’ll probably first see it in LoDash, the compose method (now called flow).

// Capitalize each word in the array, do Base64





// The general way to write (one of them)

const arr = ['pen', 'apple', 'applypen'];

const rs = [];

for(const w of arr){

rs.push(btoa(w.toUpperCase()));

}

console.log(rs);





/ / _. Flow to rewrite

const arr = ['pen', 'apple', 'applypen'];

const upperAndBase64 = _.partialRight(_.map, _.flow(_.upperCase, btoa));

console.log(upperAndBase64(arr));

Copy the code

_.flow merges the ability of uppercase and Base64 functions to generate a new function. Easy to reuse as a parameter function or later.

Own point of view

My understanding of JavaScript functional programming may differ from many traditional concepts. I don’t just consider higher-order functions to be functional programming. I consider everything else, such as ordinary function combination calls and chain structures, to be functional programming, as long as they take functions as the main carrier.

I don’t think functional programming is necessary, nor should it be a mandatory requirement. Like object orientation or any other idea, it is one of them. More often than not, we should be a combination of several, rather than limited to concepts.

The resources