Write JavaScript code gracefully

catalogue

preface

Avoid using js dross and chicken ribs

Write concise JavaScript code

Use the new features of ES6/ES7

Babel

ESLint

Prettier

Using functional programming

Several principle examples of elegant JS code  

  Introduction to functional programming

Abstract ability

summary

preface

Almost every larger company has a project that "runs for a long time and maintenance engineers change batch after batch". If they participate in such a project, most people have only one feeling - "climb the ship mountain" "So we often say that the code written by someone is like excrement. In order to avoid becoming someone else's mouth, the code I write generally does not indicate the author's date information (it's smart because Git can manage this information well) Therefore, in the project, we should write code with good maintainability. At the same time, for engineers, improving their coding ability and writing code that is easy to read and maintain are necessary to improve development efficiency and career. During my interview, I found that many interviewers have so-called many years of work experience and have been writing repetitive code mediocrely , and never deliberate, refine and optimize, so it is impossible to improve the programming level.

So how to write maintainable and elegant code?

First of all, we should clearly realize that the code is written for ourselves and others. The code should maintain a clear structure for future generations to read and maintain. If we need to change the code one day, others and you will thank you!

Secondly, no matter the size of the company, the size of the project, or how tight the construction period is, develop good coding specifications and implement them on the ground. If the code quality is not good enough, in the case of more demand, it may lead the whole body and tilt the building. Therefore, at the beginning or end of the project   Now?   Develop good coding standards. Everyone should have their own or team coding standards!

Finally, sniff out bad small of the code, such as repeated code, nonstandard naming, excessively long functions, data mud, etc., and then continuously optimize and reconstruct without changing the external behavior of the code, so as to improve the internal structure of the subroutine.

Next, I summarized and sorted out a large set of theories and practices for you.

Avoid using js dross and chicken ribs

Over the years, with the development of HTML5 and Node.js, JavaScript has blossomed everywhere in various fields. It has changed from "the most misunderstood language in the world" to "the most popular language in the world" However, due to historical reasons, there are still some dross and weaknesses in JavaScript language design, such as global variables, automatic semicolon insertion, typeof, NaN, false values, = =, eval, etc., which cannot be removed by the language. Developers must avoid using these features. Fortunately, ESLint below can detect these features and give error prompts (if you meet an interviewer who is still testing these features, you need to consider whether they are still using these features in their projects, and you should know how to answer such questions.).

Write concise JavaScript code

The following guidelines are from Robert C. Martin's book "Clean Code" and apply to JavaScript.   Entire list   For a long time, I chose the most important part, which I used most in the project, but I still recommend you to look at the original text. This is not a style guide, but   A guide to producing readable, reusable, and reconfigurable software using JavaScript.

variable

Use meaningful and readable variable names

Bad:

var yyyymmdstr = moment().format('YYYY/MM/DD')

Good:

var yearMonthDay = moment().format('YYYY/MM/DD')

Use const of ES6 to define constants

The "constant" defined by "var" in the counterexample is variable. When declaring a constant, the constant should be immutable in the whole program.

Bad:

var FIRST_US_PRESIDENT = "George Washington"

Good:

const FIRST_US_PRESIDENT = "George Washington"

Use easy to retrieve names

We need to read much more code than we need to write, so the readability and retrievability of the code we write are very important. Using meaningless variable names will make our program difficult to understand and hurt our readers, so please use retrievable variable names. Similar   buddy.js   and   ESLint   Tools can help us find unnamed constants.

Bad:

// What the heck is 86400000 for?
setTimeout(blastOff, 86400000)

Good:

// Declare them as capitalized `const` globals.
const MILLISECONDS_IN_A_DAY = 86400000

setTimeout(blastOff, MILLISECONDS_IN_A_DAY)

Use descriptive variables (i.e. meaningful variable names)

Bad:

const address = 'One Infinite Loop, Cupertino 95014'
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/
saveCityZipCode(
  address.match(cityZipCodeRegex)[1],
  address.match(cityZipCodeRegex)[2],
)

Good:

const address = 'One Infinite Loop, Cupertino 95014'
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/
const [, city, zipCode] = address.match(cityZipCodeRegex) || []
saveCityZipCode(city, zipCode)

method

Maintain the unity of function

This is the most important rule in software engineering. When functions need to do more things, they will be more difficult to write, test, understand and combine. When you can pull out a function and complete only one action, they will be able to refactor easily and your code will be easier to read. If you strictly abide by this rule, you will be ahead of many others Hair maker.

Bad:

function emailClients(clients) {
  clients.forEach((client) => {
    const clientRecord = database.lookup(client)
    if (clientRecord.isActive()) {
      email(client)
    }
  })
}

Good:

function emailActiveClients(clients) {
  clients
    .filter(isActiveClient)
    .forEach(email)
}

function isActiveClient(client) {
  const clientRecord = database.lookup(client)
  return clientRecord.isActive()
}

The function name shall clearly indicate its function (see the meaning of the name)

Bad:

function addToDate(date, month) {
  // ...
}

const date = new Date()

// It's hard to to tell from the function name what is added
addToDate(date, 1)

Good:

function addMonthToDate(month, date) {
  // ...
}

const date = new Date()
addMonthToDate(1, date)

Use default variables to override short circuit operations or conditions

Bad:

function createMicrobrewery(name) {
  const breweryName = name || 'Hipster Brew Co.'
  // ...
}

Good:

function createMicrobrewery(breweryName = 'Hipster Brew Co.') {
  // ...
}

Function parameters (ideally no more than 2)

It is necessary to limit the number of function parameters, which makes it easier to test the function. Too many parameters will make it difficult to test each parameter of the function with effective test cases.

Functions with more than three parameters should be avoided. Usually, more than three parameters mean that the function function is too complex. In this case, you need to re optimize your function. When multiple parameters are really needed, you can consider encapsulating these parameters into an object in most cases.

Bad:

function createMenu(title, body, buttonText, cancellable) {
  // ...
}

Good:

function createMenu({ title, body, buttonText, cancellable }) {
  // ...
}

createMenu({
  title: 'Foo',
  body: 'Bar',
  buttonText: 'Baz',
  cancellable: true
})

Remove duplicate code

Duplicate code ranks first in bad small, so try your best to avoid duplicate code, because it means that when you need to modify some logic, there will be many places to modify.

Duplicate code is usually because two or more slightly different things share most of them, but their differences force you to use two or more independent functions to deal with most of the same things. Removing duplicate code means creating an abstract function / module / class that can deal with these differences.

Bad:

function showDeveloperList(developers) {
  developers.forEach((developer) => {
    const expectedSalary = developer.calculateExpectedSalary()
    const experience = developer.getExperience()
    const githubLink = developer.getGithubLink()
    const data = {
      expectedSalary,
      experience,
      githubLink
    }

    render(data)
  })
}

function showManagerList(managers) {
  managers.forEach((manager) => {
    const expectedSalary = manager.calculateExpectedSalary()
    const experience = manager.getExperience()
    const portfolio = manager.getMBAProjects()
    const data = {
      expectedSalary,
      experience,
      portfolio
    }

    render(data)
  })
}

Good:

function showEmployeeList(employees) {
  employees.forEach((employee) => {
    const expectedSalary = employee.calculateExpectedSalary()
    const experience = employee.getExperience()

    const data = {
      expectedSalary,
      experience
    }

    switch (employee.type) {
      case 'manager':
        data.portfolio = employee.getMBAProjects()
        break
      case 'developer':
        data.githubLink = employee.getGithubLink()
        break
    }

    render(data)
  })
}

Avoid side effects

When the function produces a result other than "accept a value and return a result" For other behaviors, the function is said to have side effects, such as writing files, modifying global variables, or transferring all your money to a stranger. In some cases, the program does need the behavior of side effects. At this time, these functions should be concentrated together. Do not modify a file with multiple functions / classes. Use and use only one service to complete this requirement.

Bad:

const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() })
}

Good:

const addItemToCart = (cart, item) => {
  return [...cart, { item, date: Date.now() }]
}

Avoid conditional judgment

This seems unlikely. Most people's first reaction when they hear this is: "how can you do other functions without if?" in many cases, the same purpose can be achieved by using polymorphism. The second question is what is the reason for this approach. The answer is what we mentioned earlier: keep the function single.

Bad:

class Airplane {
  //...
  getCruisingAltitude() {
    switch (this.type) {
      case '777':
        return getMaxAltitude() - getPassengerCount()
      case 'Air Force One':
        return getMaxAltitude()
      case 'Cessna':
        return getMaxAltitude() - getFuelExpenditure()
    }
  }
}

Good:

class Airplane {
  //...
}

class Boeing777 extends Airplane {
  //...
  getCruisingAltitude() {
    return getMaxAltitude() - getPassengerCount()
  }
}

class AirForceOne extends Airplane {
  //...
  getCruisingAltitude() {
    return getMaxAltitude()
  }
}

class Cessna extends Airplane {
  //...
  getCruisingAltitude() {
    return getMaxAltitude() - getFuelExpenditure()
  }
}

Use the new features of ES6/ES7

Arrow function

Bad:

function foo() {
  // code
}

Good:

let foo = () => {
  // code
}

Template string

Bad:

var message = 'Hello ' + name + ', it\'s ' + time + ' now'

Good:

const message = `Hello ${name}, it's ${time} now`

deconstruction

Bad:

var data = { name: 'dys', age: 1 }
var name = data.name,
    age = data.age

Good:

const data = {name:'dys', age:1} 
const {name, age} = data 

Use ES6 classes instead of ES5 functions

Typical ES5 classes (functions) have poor readability in inheritance, construction and method definition. When inheritance is required, classes are preferred.

Bad:

// The complex prototype chain inheritance will not post code

Good:

class Animal {
  constructor(age) {
    this.age = age
  }

  move() { /* ... */ }
}

class Mammal extends Animal {
  constructor(age, furColor) {
    super(age)
    this.furColor = furColor
  }

  liveBirth() { /* ... */ }
}

class Human extends Mammal {
  constructor(age, furColor, languageSpoken) {
    super(age, furColor)
    this.languageSpoken = languageSpoken
  }

  speak() { /* ... */ }
}

Async / wait is a better choice than Promise and callback

Callbacks are not neat enough and cause a lot of nesting. Promises are embedded in ES6, but async and await in ES7 are better than promises.

Promise code means "I want to perform this operation and then use its results in other operations". Await effectively reverses this meaning, making it more like "I want to get the result of this operation". I like it because it sounds simpler, so use async/await as much as possible.

Bad:

require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
  .then(function(response) {
    return require('fs-promise').writeFile('article.html', response)
  })
  .then(function() {
    console.log('File written')
  })
  .catch(function(err) {
    console.error(err)
  })

Good:

async function getCleanCodeArticle() {
  try {
    var request = await require('request-promise')
    var response = await request.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
    var fileHandle = await require('fs-promise')

    await fileHandle.writeFile('article.html', response)
    console.log('File written')
  } catch(err) {
    console.log(err)
  }
}

Babel

After the release of the ES6 standard, front-end developers have gradually learned about ES6, but it has not been widely promoted due to compatibility problems. However, the industry has also used some compromise solutions to solve compatibility and development system problems. One of the most famous is   Babel   Babel is a widely used transcoder. Its goal is to convert all ES6 new syntax using Babel to execute in the existing environment.

Use next generation JavaScript, today

Babel is not only able to convert ES6 code, but also a testing ground for ES7. For example, async/await has been supported, which makes it easier for developers to write asynchronous code. The logic and readability of the code are simply not very good. Although mainstream browsers may take some time to support this asynchronous coding method, based on Babel, developers can now use it in production environments. This is due to Babel's high consistency with the JavaScript Technical Committee, which can provide a real-world implementation of the new features of ECMAScript before standardization. So developers can   In the production environment, unpublished or unsupported language features are widely used. ECMAScript can also get real-world feedback before the specification is finalized. This positive feedback further promotes the development of JavaScript language.

The simplest way to use Babel is as follows:

# Install Babel CLI and Babel preset es2015 plug-ins
npm install -g babel-cli
npm install --save babel-preset-es2015

Create a file. babelrc in the current directory and write:

{
  "presets": ['es2015']
}

For more functions, please refer to Official website.

ESLint

A high-quality project must include a perfect lint. If a project is still mixed with tab, two spaces and four spaces, a function can move hundreds of lines, and there are several layers of if, nesting and callback. In addition to the various JavaScript dross and chicken ribs mentioned above, a strong wind is blowing in the urban-rural fringe. How can I write code and adjust the code format every day.

How can this work? If you get paid, you have to write the code well. Therefore, lint is very necessary, especially for large projects. He can ensure that the code conforms to a certain style and has at least readability. Other people in the team can master other people's code as soon as possible. For JavaScript projects, ESLint will be a good choice at present. The installation process of ESLint will not be introduced. Please refer to Official website , let's talk about a very strict ESLint configuration, which is the best response to the section on writing concise JavaScript code above.

{
  "parser": "babel-eslint",
  "env": {
    "es6": true,
    "browser": true
  },
  "extends": ["airbnb", "prettier", "plugin:react/recommended"],
  "plugins": ["react", "prettier"],
  "rules": {
    "prettier/prettier": [
      "error",
      {
        "semi": false,
        "singleQuote": true,
        "trailingComma": "es5"
      }
    ],
    // The complexity of a function is no more than 10, and all branches, loops and callbacks add up to no more than 10 in a function
    "complexity": [2, 10],
    // The nesting of a function cannot exceed 4 layers. Multiple for loops and deep if else are the source of sin
    "max-depth": [2, 4],
    // A function can have up to three layers of callback, using async/await
    "max-nested-callbacks": [2, 3],
    // Maximum number of lines in a file
    "max-lines": ["error", {"max": 400}],
    // A function can have up to 5 parameters. A function with too many parameters means that the function is too complex. Please split it
    "max-params": [2, 5],
    // A function can have up to 10 variables. If it exceeds, please split it or simplify it
    "max-statements": [2, 10],
    // A staunch supporter of semiconlon less
    "semi": [2, "never"],

    "class-methods-use-this": 0,
    "jsx-a11y/anchor-is-valid": [
      "error",
      {
        "components": ["Link"],
        "specialLink": ["to"]
      }
    ],
    "jsx-a11y/click-events-have-key-events": 0,
    "jsx-a11y/no-static-element-interactions": 0,
    "arrow-parens": 0,
    "arrow-body-style": 0,
    "import/extensions": 0,
    "import/no-extraneous-dependencies": 0,
    "import/no-unresolved": 0,
    "react/display-name": 0,
    "react/jsx-filename-extension": [1, {"extensions": [".js", ".jsx"]}],
    "react/prop-types": 0
  }
}

Prettier

Prettier is a JavaScript formatting tool. It is inspired by refmt. It has advanced support for the language features of ES6, ES7, JSX and Flow. By parsing JavaScript into AST and beautifying and printing based on ast, prettier will lose almost all the original code style, so as to ensure the consistency of JavaScript code style. You can feel it first.

Automatically format the code. No matter what your original code format is, it will be formatted into the same. This function   Great, really great. We don't have to worry about code format anymore.

After ESLint and Prettier are confirmed, they must be added to the pre commit hook. Because people are lazy, don't expect all engineers to take the initiative to execute ESLint and Prettier, so the following. Pre commit file is created. When postinstall of scripts in package.json, soft link to. Git / hooks / pre commit, This will automatically execute the following script when pre commit. Try to join the pre commit hook at the initial stage of the project. Joining in the middle of the project may encounter opposition from the team, which is difficult to implement. This is also a place to pay attention to during the interview. We need practical means to improve efficiency and implement it.

After ESLint and Prettier are confirmed, they must be added to the pre commit hook. Because people are lazy, don't expect all engineers to take the initiative to execute ESLint and Prettier, so the following. Pre commit file is created. When postinstall of scripts in package.json, soft link to. Git / hooks / pre commit, This will automatically execute the following script when pre commit. Try to join the pre commit hook at the initial stage of the project. Joining in the middle of the project may encounter opposition from the team, which is difficult to implement. This is also a place to pay attention to during the interview. We need practical means to improve efficiency and implement them. The above operations can be solved easily through husky.

Install it along with husky:

yarn add lint-staged husky --dev

and add this config to your package.json:

{
  "scripts": {
    "precommit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,json,css,md}": ["prettier --write", "git add"]
  }
}

The above commands will perform Prettier formatting before ESLint verification during pre commit. If you want to format the code during editing, Prettier also has plug-ins for the current mainstream editors. Please refer to   here  , In addition, ESLint can be used well with Prettier. Refer to   eslint-plugin-prettier  , I have sorted out all the above configurations and files   This project   In order to let everyone write code well, I really broke my heart.

Using functional programming

Before we talk about functional programming and its advantages, let's look at the disadvantages of our common programming methods, imperative programming.

function getData(col) {
  var results = []
  for (var i = 0; i < col.length; i++) {
    if (col[i] && col[i].data) {
      results.push(col[i].data)
    }
  }
  return results
}

This code is very simple. It filters an incoming array, takes out the data field of each element, and then inserts a new array to return. I believe many people will write similar code. It has many problems:

  • We are telling the computer how to do something step by step. We introduce a loop and use an insignificant local variable i to control the loop (or iterator). In fact, i don't need to care about how this variable starts, ends and grows, which has nothing to do with the problem i want to solve.
  • We introduce a state results and constantly change this state. Its value changes each time the loop.
  • When our problem changes slightly, for example, if I want to add a function to return an array of data length, we need to carefully study the existing code, figure out the whole logic, and then write a new function (in most cases, engineers will enable the "copy paste modify" method.
  • The readability of such code is very poor. Once there are more than 10 internal states and they are interdependent, it is not easy to understand its logic.
  • Such code cannot be easily reused.

If it was functional programming, you would probably write this:

function extract(filterFn, mapFn, col) {
  return col.filter(filterFn).map(mapFn)
}

Do you think the world is clean? This code is very concise and clear. If you know filter / map, it is almost difficult to write wrong. This is almost a general solution, a machine. With it, you can solve the problem of filtering and mapping any data set. Of course, you can also be so abstract:

function extract(filterFn, mapFn) {
  return function process(col) {
    return col.filter(filterFn).map(mapFn)
  }
}

Note that although the abstract results of the two are similar, the application scope is different. The latter is more like a machine producing a machine (function return function), which further decouples the problem. This decoupling makes the code not only generalization It also divides the execution of the code into two stages. It is also decoupled on the timing and interface. So you can call extract in context A and call process in context B to generate real results. Context A and context B are irrelevant. A only provides filterFn and mapFn (for example, system initialization). , the context of B only needs to provide the specific data set col (for example, when the web request arrives). This temporal decoupling greatly enhances the power of the code. The decoupling on the interface, like the universal socket used in tourism, allows your function to connect the system in context a on one end and the system in context B on the other.

Here we can see some features of functional programming:

  • Advocate composition, which is the king.
  • Each function performs a single function as much as possible.
  • Mask the details and tell the computer what I want to do instead of how to do it. We look at filter / map. They do not expose their own details. The implementation of a filter function may be a loop on a single core CPU and a dispatcher and aggregator on a multi-core CPU, but we can ignore its implementation details for the time being and only need to understand its functions.
  • Introduce as few or no states as possible.

If these features are used properly, they can bring to the software:

  • Better design and implementation.
  • Clearer and readable code. Since the state is greatly reduced, the code is easier to maintain and more stable.
  • It has better performance in distributed system. Functional programming is generally abstracted at a higher level, and map / filter / reduce is its basic instruction. If these instructions are optimized for distribution, the system can improve performance without any change.
  • It makes lazy operation possible. In imperative programming, because you clearly tell the CPU how to operate step by step, the CPU can only obey orders, and the optimization space has been squeezed; in functional programming, each function just encapsulates the operation, and a set of data goes through a series of operations from input to output. If no one handles these outputs, the operation will not be truly processed Execution.

Several principle examples of elegant JS code  

1, Conditional statement

        1. Use Array.includes to handle multiple | conditions          

// -----General------

    if (fruit == 'apple' || fruit == 'strawberry' || fruit == 'banana'  ) {
          console.log('red');
    }

//-------Elegant------

    // Extract conditions into an array
    const redFruits = ['apple', 'strawberry', 'banana', 'cranberries']; 
  
    if (redFruits.includes(fruit)) { 
        console.log('red'); 
    }

    2. Write less nesting and return invalid conditions as soon as possible

/_ Return as soon as possible when an invalid condition is found _/
function test(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  if (!fruit) thrownewError('No fruit!'); // Condition 1: throw an error early
  if (!redFruits.includes(fruit)) return; // Condition 2: when the fruit is not red, it returns directly
  console.log('red');

  // Condition 3: there must be a large number
  if (quantity > 10) {
    console.log('big quantity');
  }
}

  3. Use function default parameters and deconstruction

// -------General default parameters-------

function test(fruit, quantity) {
    if (!fruit) return;
    const q = quantity || 1; // If quantity is not provided, it defaults to 1
    console.log(`We have ${q}${fruit}!`);
}

// -------Default parameters are elegant-------

function test(fruit, quantity = 1) { // If quantity is not provided, it defaults to 1
    if (!fruit) return;
    console.log(`We have ${quantity}${fruit}!`);
}

// -------General deconstruction-------

 function test(fruit) { 
    // If there is a value, print it out
    if (fruit && fruit.name)  {
      console.log (fruit.name);
    } else {
      console.log('unknown');
    }
}

// -------Deconstruction elegance-------

  // Deconstruction -- only the name attribute is obtained
  // The default parameter is null object {}
  function test({name} = {}) {
    console.log (name || 'unknown');
  }

  4. Map / Object may be a better choice than switch  

//------switch general---------

function test(color) {
  // Use the switch case to find the fruit with the corresponding color
  switch (color) {
    case 'red':
      return ['apple', 'strawberry'];
    case 'yellow':
      return ['banana', 'pineapple'];
    case 'purple':
      return ['grape', 'plum'];
    default:
      return [];
  }
}

// -----Object elegance-----

    // Use the object literal to find the fruit of the corresponding color
    const fruitColor = {
        red: ['apple', 'strawberry'],
        yellow: ['banana', 'pineapple'],
        purple: ['grape', 'plum']
     };
    function test(color) {
      return fruitColor[color] || [];
    }

// -----Map elegance-----

  // Use Map to find the fruit with the corresponding color
  const fruitColor = newMap()
      .set('red', ['apple', 'strawberry'])
      .set('yellow', ['banana', 'pineapple'])
      .set('purple', ['grape', 'plum']);
  function test(color) {
    return fruitColor.get(color) || [];
  }

// -----filter elegant-----

const fruits = [
    { name: 'apple', color: 'red' }, 
    { name: 'strawberry', color: 'red' }, 
    { name: 'banana', color: 'yellow' }, 
    { name: 'pineapple', color: 'yellow' }, 
    { name: 'grape', color: 'purple' }, 
    { name: 'plum', color: 'purple' }
];
function test(color) {
  // Use the Array filter to find the fruit with the corresponding color
  return fruits.filter(f => f.color == color);
}

5. Use Array.every and Array.some to process all / part of the conditions

// -------Direct elegance--------
const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
  ];
function test() {
  // Conditions: (short form) all fruits must be red
  const isAllRed = fruits.every(f => f.color == 'red');
  
  // Condition: at least one fruit is red
  const isAnyRed = fruits.some(f => f.color == 'red');
  console.log(isAllRed); // false
}

  Introduction to functional programming

What is functional programming and what are its advantages?

Before we talk about functional programming and its advantages, let's look at the disadvantages of our common programming methods, imperative programming.

function getData(col) {
    var results = [];
    for (var i=0; i < col.length; i++) {
        if (col[i] && col[i].data) {
            results.push(col[i].data);
        }
    }
    return results;
}
This code is very simple. It filters an incoming array, takes out the data field of each element, and then inserts a new array to return. I believe many people will write similar code. It has many problems:
  • We're telling the computer how to do something step by step. We introduce loops that use an insignificant local variable i to control the loop (or iterator). In fact, i don't need to care about how this variable starts, ends and grows, which has nothing to do with the problem i want to solve.

  • We introduce a state results and constantly change this state. Its value changes each time the loop.

  • When our problem changes slightly, for example, I want to add a function to return the relevant information   data   An array of length, then we need to carefully study the existing code, figure out the whole logic, and then write a new function (in most cases, engineers will enable the "copy paste modify" method.

  • It's not easy to write such code correctly.

  • The readability of such code is very poor. Once there are more than 10 internal states and they are interdependent, it is not easy to understand its logic.

  • Such code cannot be easily reused.

If it was functional programming, you would probably write this:

function getData(col) {
    return col
        .filter(item => item && item.data)
        .map(item => item.data);
}

I first filter the array to be processed, and then map to get the result. This code is concise and clear. If you know filter / map, it is almost difficult to write wrong.

And you can easily refactor it to make it more generic:

function extract(filterFn, mapFn, col) {
    return col => col.filter(filterFn).map(mapFn);
}

const validData = item => item && item.data;
const getData = extract.bind(this, validData, item => item.data);
const getDataLength = extract.bind(this, validData, item => item.data.length);

Compared with the previous code, the structure is clearer, easier to expand, and more in line with the open close principle.

Here we can see some features of functional programming:

  • Advocate composition

  • Each function performs a single function as much as possible

  • Mask the details and tell the computer what I want to do instead of how to do it. We look at filter / map. They do not expose their own details. The implementation of a filter function may be a loop on a single core CPU and a dispatcher and aggregator on a multi-core CPU, but we can ignore its implementation details for the time being and only need to understand its functions.

  • Introduce as few or no states as possible.

If these features are used properly, they can bring to the software:

  • Better design and Implementation

  • Clearer and readable code. Since the state is greatly reduced, the code is easier to maintain and more stable.

  • It has better performance in distributed system. Functional programming is generally abstracted at a higher level, and map / filter / reduce is its basic instruction. If these instructions are optimized for distribution, the system can improve performance without any change.

  • It makes lazy operation possible. In imperative programming, because you clearly tell the CPU how to operate step by step, the CPU can only obey orders, and the optimization space has been squeezed; in functional programming, each function just encapsulates the operation, and a set of data goes through a series of operations from input to output. If no one handles these outputs, the operation will not be truly processed Execution.

Let's take an example:

lazy(bigCollection)
    .filter(validItem)
    .map(processItem)
    .skip(2)
    .take(3)

For the above code, regardless of   bigCollection   No matter how large, the loop will execute only a limited number of times.

Let's take another example:

const Stream = Rx.Observable;
Stream.from(urls)
    .flatMap(url => 
        new Stream.create(stream => {
            request(url, (error, response, body) => {
                if (error) return stream.onError(error);
                stream.onNext({ url, body });
            });
        })
        .retry(3)
        .catch(error => Stream.Just({ url, body: null }))
    )

This code uses the concept of Observable. Let's put aside the concept of Observable first. Interested students can see FRP (functional reactive programming) Here, we think Observable is a stream. We have a list of URLs and need to get the response body corresponding to each url. Each request can be retried at most three times. If it fails three times, it will return null.

If you write the same code in the traditional way, the logic and context will not be so clear.

With the above examples, we have a preliminary understanding of functional programming. Now let's go back to the source and talk about what function is.

When we were in junior high school, we knew that a function has a scope and a value range   f(x) = x * x, if its scope is all integers, then the value range of the function is all positive integers. Here, integers and positive integers are the input and output types of the function.

The function we are talking about here is almost equivalent to the function in mathematics, which is to map the value (definition field) of one field to the value (value field) of another field through transformation. The biggest feature of mathematical function is that if the value field of one function f(x) is the definition field of another function g(x), the two functions can be combined:

g(f(x)) = (g f)(x)

h(j(k(x, y, ...)))
   = h((j k)(x, y, ...))
   = (h j k)(x, y, ...)
   = (h j) (k(x, y, ...))

Functional programming also incorporates composition The word "combination" sounds familiar to everyone, right? In object-oriented programming, one of the best practices is:. What a strange best practice. Inheritance is the core function of object-oriented, but we have to try our best to use this core function as little as possible? This is because inheritance is reducing the reuse of code. If we want to DRY (Don't Repeat Yourself), either use Mixin for functional integration, or move the repeated code to the base class, but there will be problems. You may not be able to rewrite the base class, or even if you can rewrite the base class, rewriting the base class for an upper function violates the open close principle, which is a dilemma. Therefore, we advocate composition.

Combination is a powerful tool. The above-mentioned h, j and k are three basic functions. Through combination, we can derive a series of new functions: (j, k), (h, j), (h, j, k). Just like building Lego blocks, they greatly expand the functions.

Many functional programming languages provide specialized syntax, such as   compose (clojure), < < < (haskell et al.

We have used combination in the above example. Let's take another example:

const getComponentPath = (name, basePath) => path.join(basePath, name);
const getModelPath = getComponentPath.bind(null, 'models');
const getConfigPath = getComponentPath.bind(null, 'config');

const getConfigFile = (p, name) => path.join(getConfigPath(p), name);

const readTemplate = filename => fs.readFileAsync(filename, ENCODING);

const processTemplate = params =>
    promise => promise.then(content => mustache.render(content, params));

const writeFile = filename => 
    promise => promise.then(
        content => fs.writeFileAsync(filename, content, ENCODING)
    );

const processConfigTemplate = R.pipe(
      getConfigFile,
      readTemplate,
      processTemplate(PARAMS),
      writeFile(getConfigFile(topDir, 'config.yml'))
);

processConfigTemplate(topDir, 'config.mustache')
    .then(() => console.log('done!'))
    .catch(err => console.log(err));

The function of this code is very simple. Read the data in the config directory of the project   config.mustache   Files, generating   config.yml. When many people write this function, they must write it as a function, but here I write nine functions, six of which are directly related to the main function   processConfigTemplate   It is a direct combination of several functions. Instead of using compose, another concept pipe is used. It is also used to combine functions, but its combination direction is just opposite to compose, which is more convenient to write. You can understand pipe here. A group of inputs successively pass through each function in the pipe, and the input of the previous function is the output of the next function Continue to execute, and finally get an output.

Such code is very readable, almost without comments, and a javascript programmer who has just taken over the code can understand it; Moreover, it is highly reusable. Any part of it can be used in other places. If the requirements change, for example, we don't use the mustache template and use the handlebar instead. In the whole logic, we only need to replace or add a new one   processTemplate   Function. Due to the single function, the testability of each function is very strong, and it is easy to write test case. If you maintain such code, it is the happiest thing in the world.

Some students may say, how to debug such code? Let's look back at this code and think about it. Does this code need debugging? As long as the compilation (sorry, javascript has no compilation stage) passes, you can almost guarantee that the code will work. I wrote the code while writing this manuscript. For the sake of brevity, I did not provide some statements for library loading, but I believe there is no big problem in writing this code. Many novice engineers rely heavily on the one-step tracking function of IDE. I can responsibly tell you that as you grow, you must reduce or eliminate the dependence on one-step tracking. Future programs cannot be debugged by one-step tracking. If your system runs in a distributed environment with hundreds of machines and thousands of core s, how do you step in / step out? unrealistic. I think:

Excellent engineers conceive the context of the code in their head (or on paper), and write the code line by line at one go; Poor engineers need to rely on the one-step tracking function provided by the IDE to call out the code line by line. One is to write and the other is to set a high profile.

The reason why you need one-step tracking to adjust the program is that there are too many intermediate states in the program. You can't track the values of these intermediate states, so you need to use the power of the IDE. But functional programming can effectively help you control or even eliminate intermediate states. Like the above example, you don't have intermediate states. What else do you track?

Let's look at some concepts in this program to facilitate the combination of functions:

  1. curry

  2. closure

Kuri's picture

Look at curry first. I'm sorry, I'm off the topic. Curry here is not the curry of warriors, but the curry in functional programming. In Wikipedia, currying is introduced as follows:

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

Generally speaking, it is to convert a function with multiple parameters into a series of functions with only one parameter. In javascript, you can use   bind   Coritization.

For example, this sentence:

const getConfigPath = getComponentPath.bind(null, 'config');

In functional programming, the position of function parameters is very particular, which needs to be carefully arranged. Auxiliary and Coriolis parameters should be put in front to facilitate binding.

Another Coriolis method, or more orthodox method, is through higher-order functions. Higher order function means that a function can accept another function as a parameter or return a function as a result. Let's look at this sentence:

const processTemplate = params =>
    promise => promise.then(content => mustache.render(content, params));

stay   writeFile   In this function, we accept   params   As a parameter, returns an accept   Promise   As a parameter and returns   Promise   Function of. High order functions are very important in functional programming. In fact, if you use javascript to develop, especially nodejs, you will deal with high-order functions almost every day.

When A function returns another function, we find that it is used in the function body of the returned function   processTemplate   Parameters passed in. This is another very important concept in functional programming: closure. Closure means that the scope of A variable is always valid in the entire legal scope. For example, function A returns function B, and function B can access A's local variables, including parameters, at any time. This is closure. Closure is A common pattern in functional programming. It can help you delay the calculation and postpone the calculation until necessary.

What is deferred computing? Let's take an example:

const authMiddleware = config =>
    (req, res, next) => {
        if (config.auth.strategy === 'jwt') {
            const token = getToken(req.headers);
            jwt.verify(token, secret, ...);
        }
    }

People who have used expressjs can probably see that this function is used to generate a middleware of expressjs. Normally, when you write middleware, you have all the context. You will write it directly as follows:

// config is define in this module somewhere
app.use((req, res, next) => {
    if (config.auth.strategy === 'jwt') {
        ...
    }
});

However, if you are writing a framework and the user has not created an app or generated a config object when writing the middleware, the only thing you can do is assume that the caller will pass you a legal config object and return a middleware for her to use. This is to delay the calculation to the time needed.

After we have a clear understanding of coriolism and closures, we will look at their contribution to composition. stay   processConfigTemplate   Inside:

const processConfigTemplate = R.pipe(
      getConfigFile,
      readTemplate,
      processTemplate(PARAMS),
      writeFile(getConfigFile(topDir, 'config.yml'))
);

We need to calculate the file name of config, obtain its content, use the parameter processing template, and write a new file. If we want to combine them, we must adapt the input and output of each other. To fit perfectly, we need the concept of coritization / closure. In this way, processTemplate and writeFile   Such a function does not lose its universality and can also be used in other occasions. With these basic functions, I can easily provide   processJadeTemplate,processMustacheTemplate,writeDb   Wait, combine all kinds of functional logic.

Let's stop and think, what would you do if it was OOP? How do you design interface s, how to define various types, and class behaviors, how to abstract, what design patterns to use, adapter, chain of responsibility, etc. Then you will remind yourself not to over design; When you need to extend later, you have to destroy the open and close principle, or abstract or refactor. In short, in order to achieve the same goal, OOP is like Catholicism before the religious reform, full of red tape, and doing anything is like holding a ceremony; FP is like Martin Luther's Protestantism, simple and clear.

Functional programming has some very interesting features. For example, if the input parameters and output parameters of your function have the same type, this is a very special function called monoid. For example, in javascript   Promise, it accepts one promise and returns another promise, so it is a monoid, and it satisfies other laws. It is also a Monad, or further, Either Monad. Promise   Encapsulates a value, which can have two states of success (data) and failure (throw error) at some time in the future (a bit like Schrodinger's cat). That's why you normally write code like this:

fs.openFile(filename, encoding, (err1, data1) => {
    if (err1) return console.log(err1);
    processTemplate(data1, (err2, data2) => {
        if (err2) return console.log(err2);
        writeFile(filename, data2, encoding, (err3, data3) => {
            if (err3) return console.log(err3);
            console.log('Done!');
        });
    });
});

And use   Promise, the structure of the code can be simplified to:

fs.openFileAsync(filename, encoding)
    .then(content => processTemplate(content))
    .then(content => writeFile(filename, content, encoding))
    .then(() => console.log('done!'))
    .catch(err => console.log(err));

What I want to say here is not just   Promise   It can be used to solve problems such as callback shell, but: promise   Unified the handling of the mistakes we have been headache all the time! You can combine your code in one go and handle errors in a unified place. This is Monad's power! Without the power of functional programming, you can't enjoy such simple code (compare the two kinds of code carefully). In daily work, there are many similar structures, not only callback shell, but also if else shell.

Functional programming has many concepts, such as Monad, monoid, function, applied, curry and so on, which will make you confused. But the essence of all these concepts is for composition. In the world of functional programming, composition is the king and everywhere.

Abstract ability

One of the signs of human intelligence from young to mature is the ability to recognize and use numbers. When we were three or four years old, although we could skillfully recite the numbers within 100 at will, the numbers were only numbers for us as children. Even if we had just finished counting 12 grapes in front of our table and ate one, we had to count it again to determine it was 11 (don't ask me why, it's all clear). At this age, numbers leave concrete things and no longer have any meaning to us.

With the growth of age, the development of the brain and the continuous training in primary school, we began to be able to use numbers at will. At the same time, we can't even feel that it is an abstraction of real life. We can skillfully take out six yuan for each kilogram of cabbage for eighty cents, celery for one yuan and a bottle of soy sauce for two yuan and four yuan, Leave the market happily.

By junior and senior high school, abstraction has started from numbers and progressed like a higher level. Plane geometry and analytic geometry replace us from numbers to figures, while Algebra (from j concrete numbers to abstract letters) leads us to the level of functions. The problem in primary school: "my father's age is three times that of me. In ten years, his age will be twice that of me. Ask me how old I am now?" at this time, it became an introductory topic of the equation. If my age is x and my father's age is y, you can write the equation as follows:

y = 3x
y + 10 = 2(x + 10)

However, there are thousands of such equations, and we need to calculate a solution for each column. Next, we further normalize and abstract the problem:

y = a1x + b1
y = a2x + b2

So we have a solution for x: (b2 - b1) / (a1 - a2). It is like a machine. For any similar problem, we only need to apply it to produce a solution. This is the formula (or theorem). Formulas (or theorems) and their proof process run through our middle school era.

At the University, the degree of abstraction has risen to a huge level. We abstract relationships from numbers. Calculus studies and analyzes the relationship between the change of things and things themselves; Probability theory studies the possibility of random events; Discrete mathematics studies mathematical logic, set, graph theory and so on. These are studies of relationships. The difficult and miscellaneous diseases in middle school and in college are all Pediatrics who have been killed without discussion.

...

However, not everyone can adapt to this upgrading of abstraction. Sometimes you are trapped at a certain level (for example, the program King's College Mathematics doesn't toss around, skipping classes will ruin your life) and can't break through. At this time, we can only solidify this thinking through continuous practice, and then find its meaning in the solidification. It's like Zhang Wuji on ice fire island is forced by Xie Xun to recite martial arts secrets, or you are trying to get familiar with every number back and forth at the age of three. They are meaningless at that moment. When the day when they need to be meaningful comes, you will have an epiphany.

Similarly, writing code requires abstraction, which is absolutely necessary. If you don't want to be a primary coder all your life, if you want to write some code that you feel satisfied with, and if you don't want to be replaced by more advanced coding tools in the future, you need to learn abstraction.

The first priority of abstraction is to abstract the specific problem into a function (or class) and solve it by program. It is believed that every programmer claiming to be a software engineer can achieve this level of abstraction: once he has mastered the grammar of a language, he can map the problem to idioms and write qualified code. This is somewhat like pupils' understanding of numbers: they can apply them at will without confusion.

The second level of abstraction is to write functions that can solve multiple problems. Just like the general solution (b2 - b1) / (a1 - a2) of the binary primary equation mentioned above, you create a machine that can handle all the problems it can solve. Code like this:

function getData(col) {
    var results = [];
    for (var i=0; i < col.length; i++) {
        if (col[i] && col[i].data) {
            results.push(col[i].data);
        }
    }
    return results;
}

When we look at it independently, it seems to be very concise and the mapping of specific problems to be solved is very accurate. However, when I abstract the loop, the filtering in the loop and the processing of filtering results, the following code can be generated:

function extract(filterFn, mapFn, col) {
    return col.filter(filterFn).map(mapFn);
}

This is a general solution, a machine. With it, you can solve any data set filtering and mapping problem. Of course, you can also be so abstract:

function extract(filterFn, mapFn) {
    return function process(col) {
        return col.filter(filterFn).map(mapFn);
    }
}

Note that although the abstracted results are similar, the application scope is different. The latter is more like a machine that produces a machine (return function), which further decouples the problem. This decoupling makes the code not only generalize, but also divide the execution process of the code into two stages, and understand the coupling in timing and interface. So you can call extract in context A and call process in context B to produce real results. Context a and context B can be irrelevant. The context of a only needs to provide filterFn and mapFn (for example, system initialization), and the context of B only needs to provide the specific data set col (for example, when the web request arrives). This timing decoupling greatly enhances the power of the code. Decoupling on the interface, just like the universal socket used in tourism, enables your function to connect one end to the system in context a and the other end to the system in context B.

(of course, for languages that support currying, the two are actually equivalent. I just look at the difference from the perspective of thinking.).

The second level of abstraction is not difficult to grasp. Various pattern s in OOP and high-order functions in FP are powerful weapons to help us carry out the second level of abstraction.

The third level of abstraction is the establishment of the basic model. If we look for commonalities in the problems to be solved in the previous abstraction, we look for commonalities and connections in the solutions. For example, build a contract in your program world. In software development, the most basic contract is the type system. Strongly typed languages such as java and haskell contain such contracts. What about a weakly typed language like javascript? You can build this contract yourself:

function array(a) {
    if (a instanceof Array) return a;
    throw new TypeError('Must be an array!');
}

function str(s) {
    if (typeof(s) === 'string' || s instanceof String) return s;
    throw new TypeError('Must be a string!');
}

function arrayOf(type) {
    return function(a) {
        return array(a).map(str);
    }
}

const arrayOfStr = arrayOf(str);
// arrayOfStr([1,2]) TypeError: Must be a string!
// arrayOfStr(['1', '2']) [ '1', '2' ]

Another example. In the world of programs, no one's perfect. When you make a query from the database, there are two possibilities (ignore the exception first): the result contains a value or is empty. This binary result is almost what we need to deal with every day. If each operation of your program produces a binary result, your code will form a pyramid on the structure of if...else:

function process(x) {
    const a = step1(x);
    if (a) {
        const b = step2(a);
        if (b) {
            const c = step3(b);
            if (c) {
                const d = step4(c):
                if (d) {
                    ...
                } else {
                    ...
                }
            } else {
                ...
            }
        } else {
            ...
        }
    } else {
        ...
    }
}

It's hard to read and write. Can the data of this binary structure be abstracted into a data structure? Yes. You can create a type: May, which contains two possibilities: Some and None, and then formulate such contracts: stepx() to accept may and return may. If the accepted may is a None, it will be returned directly, otherwise it will be processed. The new code logic is as follows:

function stepx(fn) {
    return function(maybe) {
        if (maybe isinstanceof None) return maybe;
        return fn(maybe);
    }
}
const step1x = stepx(step1);
const step2x = stepx(step2);
const step3x = stepx(step3);
const step4x = stepx(step4);

const process = R.pipe(step1, step2, step3, step4);

As for how to write this may type, I won't discuss it first.

(Note: Haskell has the type of May)

The fourth level of abstraction is to formulate rules and establish a world to solve the whole problem space, which is metaprogramming. When it comes to metaprogramming, we first think of lisp, clojure and other languages that "program is data, data is program" that can operate syntax numbers at run time. It's not. Clojure programmers who don't have strong abstract ability can't write better meta programming programs than javascript programmers who have strong abstract ability. It's like Lv Bu's Halberd of square sky painting can only be used for self photographing in your and my hands.

The ability of metaprogramming is divided into many stages. Let's talk about the entry-level ability without language support: the ability to abstract actual problems into rules. In other words, before creating a company (solving problems), you first create a company law (establishing the rules of problem space), and then create a company according to the company law (using rules to solve problems).

For example, you need to write a program to parse various feed s with different specifications, process them into a data format and store them in the database. The source data may be XML or json. The definitions of their field names are very inconsistent. Even if they are XML, some data with the same meaning are in attribute and some in child node. What should we do?

Of course, we can write a handler (or class) for each feed and reuse the common parts as much as possible. However, this means that for each new feed support, you have to write a new part of the code.

A better way is to define a language that maps source data to target data. In this way, once the language is defined, you only need to write a Parser, once and for all, and then add a feed. Just write a description using the language. The following is a description of a json feed:

{
    "video_name": "name",
    "description": "longDescription",
    "video_source_path": ["renditions.#max_renditions.url", "FLVURL"],
    "thumbnail_source_path": "videoStillURL",
    "duration": "length.#ms2s",
    "video_language": "",
    "video_type_id": "",
    "published_date": "publishedDate",
    "breaks": "",
    "director": "",
    "cast": "",
    "height": "renditions.#max_renditions.frameHeight",
    "width": "renditions.#max_renditions.frameWidth"
}

This is the description of an XML feed:

{
    "video_name": "title.0",
    "description": "description.0",
    "video_source_path": "media:group.0.media:content.0.$.url",
    "thumbnail_source_path": "media:thumbnail.0.$.url",
    "duration": "media:group.0.media:content.0.$.duration",
    "video_language": "",
    "video_type_id": "",
    "published_date": "",
    "breaks": "",
    "director": "",
    "cast": "",
    "playlist": "mcp:program.0",
    "height": "media:group.0.media:content.0.$.height",
    "width": "media:group.0.media:content.0.$.width"
}

I won't expand the details of this description language, but by defining a language, or defining rules, we have successfully changed the nature of the problem: from a data processing program to a Parser; Input has changed from one feed to a source file of a description language. As long as the Parser is written, the problem can be solved once and for all. You can even write a UI for the language so that non engineers in the team can easily support new feeds.

summary

Software engineering has developed for more than 50 years and is still moving forward. Now, take these principles as the touchstone and apply them to the actual development of the team, and try to use them as one of the standards for assessing the code quality of the team.

One more thing: these guidelines will not make you an excellent engineer immediately, and long-term adherence to them does not mean that you can rest assured. A thousand mile trip begins with one step. We need to conduct code reviews with our peers from time to time, constantly optimize our code, and don't be afraid of the efforts needed to improve code quality. In the long run, you will not only understand the code you wrote six months ago, but also get the praise of your peers, and your program will go further!

Keywords: Javascript Front-end Functional Programming

Added by suncore on Tue, 28 Sep 2021 03:29:34 +0300