«JavaScript: хорошие части» - это книга Дугласа Крокфорда, которая стала популярной в 2008 году, когда мир начал понимать, что веб-разработка и, в частности, интерфейсные технологии мы здесь, чтобы остаться.

В книге Дуглас сосредотачивается на аспектах языка, которые мы можем использовать для написания чистого кода, который работает быстро: JavaScript для предприятия.

Это моя пересмотренная версия, отложенная до 2020 года, потому что за последние 12 лет многое изменилось.

Меньше значит лучше
- Людвиг Мис ван дер Роэ

Константы

Константы - это блочные переменные, ссылка на которые не может быть изменена. Если вы попытаетесь изменить их ссылку, вы получите исключение:

const name = 'John';

try {
  name = 'Luca';
}
catch (err) {
  console.log('You can NOT mutate a constant!');
  console.log(err);
}

Константы позволяют движку JavaScript выполнять критическую оптимизацию при сборке сборки из вашего исходного кода.

Щелкните здесь, чтобы прочитать хорошую статью о константах.

👉 Использовать значения по умолчанию для констант просто:

// ❌ bad way, using variables:
let foo = doSomething();
if (!foo) {
  foo = 'default value';
}

// ✅ good way, using constants:
const foo = doSomething() || 'default value';

👉 С Тернарным оператором вы получите еще больший контроль:

const result = doSomething();
const foo = (result === null)
  ? 'default value'
  : result;

👉 Учтите, что VALUE и REFERENCE - это разные вещи:

// ❌ bad: this tries to change the constant's reference:
const foo = 'xxx';
foo = 'yyy'; 

// ✅ good: "foo" reference remains unchanged:
const foo = {
  key: 'xxx'
};
foo.key = 'yyy';

Щелкните здесь, чтобы прочитать хорошую статью о« ценности и эталоне ».

Строгое равенство

Строгое равенство обеспечивает более быстрое сравнение за счет принудительной проверки типа.

// ❌ bad way, using loose equality:
//    those examples will yield "true" even if it's 
//    quite obvious that it is not what we expect:lear that 
console.log(1 == "1");
console.log(1 == true);
console.log(0 == false);
console.log(0 == "");
console.log(null == undefined);

// ✅ good way, using strict equality:
//    the same comparison will now yield a correct "false" result:
console.log(1 === "1");
console.log(1 === true);
console.log(0 === false);
console.log(0 === "");
console.log(null === undefined);

Шаблонные литералы

const name = 'John';
const surname = 'Doe';

console.log(`hello ${name} ${surname}`);

MDN: шаблонные литералы

Стрелочные функции

Стрелочные функции - это относительно недавно введенный синтаксис для создания функций. Он изначально поддерживается в NodeJS, начиная с версии 4.4.5, и всеми основными браузерами, кроме IE.

С этим синтаксисом

вы можете построить просто функцию

Не существует this, поэтому вы не можете поддаться искушению создавать конструкторы. И нет области времени выполнения, поэтому вы не можете связываться с ней, используя call, apply или bind.

👉 Синтаксис с одним аргументом

Функция, которая принимает только один аргумент, может пропустить () вокруг него:

// Single argument:
const sayHi = name => {
  console.log(`hi, ${name}`)
};
sayHi('John');

// Multiple arguments

const sayHi = (name, surname) => {
  console.log(`hi, ${name} ${surname}`)
};
sayHi('John', 'Doe);

👉 Синтаксис одной инструкции

Функция, которая делает только одно, может опускать {} и имеет неявное return statement:

const sum = (a, b) => a + b;
console.log(sum(2, 4))

👉 Возвращение предметов

Будьте осторожны со значением {}, когда хотите вернуть объект:

// ❌ bad way, here the curly brackets are interpreted as
//    the function's body:
const makeObject = (key, val) => { key: val};

// ✅ good way, wrap your object so to make it an explicit reference:
const makeObject = (key, val) => ({ key: val});

👉 Значения аргументов по умолчанию

Аргумент может определять значение по умолчанию:

const calculateAge = (dateOfBirth, today = new Date()) => {
  const diff_ms = today.getTime() - dateOfBirth.getTime();
  const age_dt = new Date(diff_ms);
  return Math.abs(age_dt.getUTCFullYear() - 1970);
};

console.log(`You are ${calculateAge(new Date("1946-06-02"))} years old`);
console.log(`You were ${calculateAge(new Date("1946-06-02"), new Date("2010"))} years old in 2010`);

👉 Состав функций

Использовать Функциональную композицию и Принцип единой ответственности очень просто:

const ensureDate = (value) => (value instanceof Date ? value : new Date(value));
const dateDiff = (d1, d2) => ensureDate(d1).getTime() - ensureDate(d2).getTime();
const dateYears = (date) => Math.abs(date.getUTCFullYear() - 1970);
const calculateAge = (dateOfBirth, today = new Date()) => dateYears(new Date(dateDiff(today, dateOfBirth)));

console.log(`You are ${calculateAge(new Date("1946-06-02"))} years old`);
console.log(`You were ${calculateAge(new Date("1946-06-02"), new Date("2010"))} years old in 2010`);

👉 Карри

А еще легко карри и заглушить:

// Generic thunk to sum numbers:
const add = a => b => a + b;

// Specialized function that increase a number:
const inc = add(1);

console.log(inc(1));
console.log(inc(10));

👉 Избегайте операторов переключения

Определенно легко избежать операторов переключения, используя Функциональную композицию и Ранний возврат:

// ❌ bad way, using conditionals:
let foo = null;
let res = doSomething();

if (res === 'a') {
  foo = 'option 1';
} else if (res === 'b') {
  foo = 'option 2';
} else {
  foo = 'no choice';
}

// ❌ bad way, using switch:
let foo = null;
swith (doSomething()) {
  case 'a':
    foo = 'option 1';
    break;
  case 'b':
    foo = 'option 2';
    break;
  default:
    foo = 'no choice';
}

// ✅ good way, using Function Composition & Return First:
const getChoice = (res) => {
  if (res === "a") return "option 1";
  if (res === "b") return "option 2";
  return "no choice";
};

const foo = getChoice(doSomething()) || 'default value';

IIFE: выражение немедленного вызова функции

Выражение немедленно вызываемой функции - это изящный синтаксис, который использует тот факт, что вы можете обернуть любой фрагмент кода с помощью(), чтобы получить ссылку на него. Хотя это бесполезно с символами и примитивами, но очень удобно с объектами:

const italianToday = (() => {
  const pad = (v) => String(v).padStart(2, 0);
  const today = new Date();
  const day = pad(today.getDate());
  const month = pad(today.getMonth() + 1);
  const year = today.getFullYear();
  return `${day}/${month}/${year}`;
})();

console.log(italianToday);

При правильном использовании IIFE предотвращает засорение области действия вспомогательными переменными и в конечном итоге позволяет писать более чистый и декларативный код.

Деструктурирующее присвоение

Назначение деструктуризации позволяет экспортировать символы в текущий блок:

const payload = {
  name: 'John',
  surname: 'Doe',
  dateOfBirth: '1946-06-02'
}

const { name, surname } = payload;

console.log(`name: ${name}`);
console.log(`surname: ${surname}`);

👉 Импорт модулей

Это особенно полезно при импорте только частей модуля:

// With classic NodeJS style:
const { useMemo, useEffect } = require('react');

// With ES6 modules:
import { useMemo, useEffect } from 'react';

👉 Выполнить открытие / закрытие

И вы можете использовать его для реализации принципа открытости-закрытости:

// ❌ bad way, using positional arguments with default values:
const doSomething = (a = 1, b = 2, c = 3) => a + b + c;
console.log(doSomething());
console.log(doSomething(5, 5, 5));

// ✅ good way, using destructuring assignment and defaults:
const doSomething = ({ a = 1, b = 2, c = 3 } = {}) => a + b + c;
console.log(doSomething());
console.log(doSomething({ a: 5, b: 5, c: 5 }));

В первой реализации используются позиционные аргументы, поэтому, если вам нужно добавить новый аргумент для расширения возможностей функции, у вас есть 2 варианта:

  1. просто добавьте новый аргумент в конец списка
  2. рефакторинг всего кода, который использует эту функцию

Есть и другие недостатки использования позиционных аргументов, например нельзя пропустить средние аргументы.

Вторая реализация использует назначение деструктуризации для создания карты key:value, которая делает возможным:

  1. вызывающий может предоставить аргументы в любом порядке
  2. вызывающий может указать только часть аргументов, остальные будут по умолчанию
  3. добавление нового аргумента (со значением по умолчанию) не нарушит существующий код

Щелкните здесь, чтобы прочитать хорошую статью о« именованных аргументах и ​​позиционных аргументах »

👉 Переименовать свойства

Вы можете переименовывать свойства при реструктуризации:

const payload = {
  first: "John",
  last: "Doe"
};

const { first: name, last: surname } = payload;

console.log(`${name} ${surname}`);

👉 Вложенная деструктуризация

Вы также можете вкладывать деструктурирующие задания:

const payload = {
  name: {
    first: "John",
    last: "Doe"
  },
  address: {
    city: "Malmö",
    country: "Sweden"
  }
};

const {
  name: { first: name },
  address: { city }
} = payload;

console.log(`${name} lives in ${city}`);

👉 С массивами

Назначение деструктуризации также работает с массивами:

const payload = ['one', 'two', 'three'];

// Destructure the first two items:
const [ first, second ] = payload;

console.log(`first: ${first}`);
console.log(`second: ${second}`);

Массив API

Списки сейчас повсеместны, а старого доброго for ... do больше нет. Вместо этого вам следует рассмотреть возможность использования Array API, который в сочетании с функциями стрелок сделает ваш код более выразительным и читаемым.

Механизмы Javascript, такие как V8, также могут серьезно оптимизировать выполнение API связанных массивов.

const jsStuff = [{
  operator: 'switch',
  isAnyGood: false
}, {
  operator: 'var',
  isAnyGood: false
}, {
  operator: 'for',
  isAnyGood: false
}, {
  operator: 'const',
  isAnyGood: true
}, {
  operator: '...',
  isAnyGood: true
}];

// ❌ bad way, using variables and FOR Loops:
let theGoodPart = [];
for (let item of jsStuff) {
  if (true === item.isAnyGood) {
    theGoodPart.push(item);
  }
}

// ✅ good way, using Array API and Function Composition:
const isGood = (item) => (true === item.isAnyGood);
const operatorOnly = (item) => item.operator;
const theGoodPart = jsStuff
  .filter(isGood)
  .map(operatorOnly);

Оператор отдыха

Оператор отдыха - прекрасный инструмент для сбора всего остального из объекта Javascript.

👉 Используйте его в сочетании с заданием «Разрушение»:

const payload = {
  name: 'John',
  surname: 'Doe',
  dateOfBirth: '1946-06-02',
  hobbies: ['paragliding', 'sailing']
};

// Extract information and collect the remaining keys into "other"
const { name, surname, ...other } = payload;

console.log(other);

👉 Используйте его для сбора аргументов функции в массив:

const foo = (a, b, ...args) => {
  console.log(`a: ${a}`);
  console.log(`b: ${b}`);

  // Log all the other arguments:
  args.forEach((arg, idx) => console.log(`${idx}: ${arg}`));
}

foo('a', 'b', 'c', 'd', 'e');

Оператор распространения

Оператор распространения позволяет быстро создавать мелкие копии объектов:

const payload = {
  name: 'John',
  surname: 'Doe',
  dateOfBirth: '1781-06-30',
  hobbies: ['paragliding', 'sailing']
};

const shallowCopy = {
  ...payload,
  dateOfBirth: '1946-06-02',
  married: true
}

👉 Вложенный оператор спреда

Вы можете вложить оператор распространения, чтобы получить глубокую копию вручную:

const payload = {
  info: {
    first: "John",
    last: "Doe"
    address: {
      city: "Malmö",
      country: "Sweden"
    }
  },
  hobbies: ['paragliding', 'sailing']
};

// This is not going to look good for deeply nested objects...
// https://www.npmjs.com/package/clone-deep
const shallowCopy = {
  ...payload,
  info: { 
    ...payload.info,
    address: { ...payload.info.address }
  },
  hobbies: [ ...payload.hobbies ],
}

Обещания

Обещания - это способы упростить то, что когда-то называлось адом обратных вызовов.

Любой асинхронный код можно обернуть и использовать как обещание:

const doSomething = () => console.log('do something...');

// ❌ bad way, using callbacks:
setTimeout(doSomething, 1000);

// ✅ good way, wrap the asynchronous code with a promise:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

// then use the Promised based asynchronous utility:
delay(1000).then(doSomething);

Для этого небольшого примера это может показаться чрезмерным усложнением ... но если вы попытаетесь сделать что-то более сложное, например, собрать несколько асинхронных функций и функций на основе обратного вызова, это будет иметь гораздо больший смысл:

// Some ludicrous but asynchronous functions:
const delayedSum = (a, b, next) => setTimeout(() => next(a + b), 10);
const delayedMulti = (a, b, next) => setTimeout(() => next(a * b), 10);
const delayedDivision = (a, b, next) => setTimeout(() => next(a / b), 10);

// ❌ bad way, callback hell:
delayedSum(1, 1, (result) => {
  delayedMulti(result, 2, (result) => {
    delayedDivision(result, 2, (result) => {
      console.log(`(1 + 1) * 2 / 2 = ${result}`);
    });
  });
});

Эта вещь с вложенными обратными вызовами может продолжаться вечно, когда вы имеете дело с несколькими поисками в базе данных, HTTP-запросами или взаимодействиями с файловой системой.

Вот небольшая утилита, которая использует Стрелочные функции, Оператор отдыха и Каррирование для преобразования любой функции, основанной на обратном вызове, в функцию, основанную на обещаниях:

const promisify = (fn) => (...args) =>
  new Promise((resolve, reject) =>
    fn(...args, (err, result) => {
      if (err) {
        reject(err);
      } else {
        resolve(result);
      }
    })
  );

С помощью этой утилиты мы можем легко преобразовать наши функции в промисы:

// ✅ good way, wrap the asynchronous code with a promise:
const delayedSumP = promisify(delayedSum);
const delayedMultiP = promisify(delayedMulti);
const delayedDivisionP = promisify(delayedDivision);

И, наконец, используйте их в красивой цепочке промисов:

// ✅ good way, use Promise.resolve() to start a Promise chain:
Promise.resolve()
  .then(() => delayedSumP(1, 1))
  .then((result) => delayedMultiP(result, 2))
  .then((result) => delayedDivisionP(result, 2))
  .then((result) => console.log(`(1 + 1) * 2 / 2 = ${result}`));

Асинхронный / Ожидание

async / await - это просто более простой синтаксис для обещаний.

Мы можем легко переписать последний пример как:

// 👉 "await" must be used inside an "async" function
const doTheJob = async () => {
  const r1 = await delayedSumP(1, 1);
  const r2 = await delayedMultiP(r1, 2);
  return delayedDivisionP(r2, 2);
}

// 👉 an "async" function always return a promise:
doTheJob()
  .then(result => console.log(`(1 + 1) * 2 / 2 = ${result}`))
  .catch(error => `Could not do the math: ${error.message}`);

👉 Вы можете использовать try / catch в сочетании с async / await:

(async () => {
  try {
    const result = await doTheJob();
    console.log(`(1 + 1) * 2 / 2 = ${result}`);
  }
  catch (error) {
    console.log(`Could not do the math: ${error.message}`);
  }
})();

В конце концов, async / await это просто синтаксический сахар вокруг обещания и позволяет вам писать код в процедурном стиле, который проще для глаз.

Обработка ошибок

Обработка ошибок - это определенно проблема Ахилла для Javascript. Такие инструменты, как VSCode, Chrome's DevTools и source-maps делают нашу жизнь менее болезненной, тем не менее, управление ошибками Javascript не так хорошо по сравнению с другими языками.

Вот несколько советов, которые помогут вашему разработчику работать с Javascript, когда дело касается ошибок и отладки.

👉 Часто сохранять:

Если вы сохраняете после одного изменения LOC и получаете сообщение об ошибке, вы точно знаете, что ошибка находится в этой единственной строке кода.

Когда вы делаете бэкэнд, вы можете использовать такие инструменты, как Nodemon, чтобы перезагружать ваш код в реальном времени каждый раз, когда вы сохраняете файл (если он становится медленным, это хороший индикатор, что вы делаете что-то не так с вашим приложением).

В современном веб-интерфейсе принято использовать live-reload и горячую замену модуля для внесения изменений кода при сохранении. Если этот процесс замедляется, еще более важно, чтобы вы выяснили, что вы делаете не так, поскольку производительность загрузки во внешнем интерфейсе имеет первостепенное значение!

👉 Используйте TDD и Jest:

Разработка через тестирование - это не просто инструмент контроля качества, это самый полезный инструмент активной разработки КОГДА-ЛИБО.

Модульное тестирование с помощью Jest невероятно быстрое. Вы можете рассматривать модульное тестирование как абстрактные программные тихие точки останова. Ваш код проходит через столько точек останова, сколько вам может понадобиться, и срабатывает сигнал тревоги, если какое-либо ожидание не оправдывается.

E2E-тестирование похоже, и вы все еще можете использовать Jest для его запуска. Но в этом случае ваши программные точки останова учитывают состояние, и вы можете проверить выполнение в реальном времени.

Для меня сейчас практически немыслимо разработать API без TDD.

👉 Выбрасывать конкретные ошибки

Одна из немногих сведений, которые выдает движок Javascript, когда дела идут плохо, - это имя ошибки:

try {
  throw new Error("foobar");
} catch (err) {
  console.log(`Error name: ${err.name}`);       // -> Error
  console.log(`Error message: ${err.message}`); // -> foobar
}

Вы можете тщательно выбрать конкретную ошибку Javascript, которая помогает представить ситуацию сбоя:

const vote = (age, value) => {
  if (age < 18) {
    throw new RangeError('You must be 18 years old.')
  }

  return callYourVotingAPI(value);
}

В этом примере мы применяем простое правило проверки на основе возраста и выдаем конкретную ошибку, которая четко сообщает, что происходит не так.

В JavaScript есть несколько специализированных ошибок:

  • Ошибка
  • EvalError
  • RangeError
  • ReferenceError
  • Ошибка синтаксиса
  • TypeError
  • URIError

👉 Выбрасывать настраиваемые ошибки

Даже лучше, чем специализированные ошибки, нестандартные ошибки!

// Define a custom error by extending an existing error type:
class RequiredMinAgeError extends RangeError {
  constructor(message = "You must be 18 years old.") {
    super(message);
    this.name = "RequiredMinAgeError";
  }
}

// Throw your custom error type
try {
  throw new RequiredMinAgeError();
} catch (err) {
  console.log(`Error name: ${err.name}`);
  console.log(`Error message: ${err.message}`);

  // The error is an istance of the custom error and its ancestors:
  console.log(err instanceof RequiredMinAgeError); // -> true
  console.log(err instanceof RangeError);          // -> true
  console.log(err instanceof Error);               // -> true

  // But it is NOT an instance of other specialized errors
  console.log(err instanceof EvalError);           // -> false
  console.log(err instanceof ReferenceError);      // -> false
  console.log(err instanceof SyntaxError);         // -> false
  console.log(err instanceof TypeError);           // -> false
  console.log(err instanceof URIError);            // -> false
}

Некоторые интересные вещи о пользовательских ошибках:

  • Вы можете указать понятные имена ошибок
  • Вы можете определить значение по умолчанию для сообщения об ошибке
  • Вы можете украсить их любыми другими свойствами, которые вам могут понадобиться.

Щелкните здесь, чтобы получить отличное руководство по обработке ошибок.

Попробуй поймать

В жизни можно быть уверенным в одном: ошибки случаются. Этот оператор помогает взять на себя ответственность за ошибки времени выполнения и обрабатывать их программно.

👉 Избегайте утомительных условных выражений

В современной веб-разработке вы можете использовать try...catch для упрощения утомительных проверок, например:

// ❌ bad way, using tedious checks:
let val = null;
if (myObject.k1 && myObject.k1.k2 && myObject.k1.k2.k3) {
  val = myObject.k1.k2.k3;
}

// ✅ good way, using try...catch:
let val = null;
try {
  val = myObject.k1.k2.k3;
} catch () {
  val = null;
}

// 😎 cool way, with const, immediate expression & arrow functions:
const val = (() => {
  try {
    return myObject.k1.k2.k3;
  } catch () {
    return null;
  }
})();

👉 Проверить ошибки HTTP

Допустим, вы хотите создать красивую функцию fetch(), которая обертывает знаменитую библиотеку «axios», и - с целью лучшей обработки ошибок - вы также хотите выдавать некоторые пользовательские ошибки на случай, если что-то пойдет не так. :

// Import AXIOS library:
// https://www.npmjs.com/package/axios
const axios = require("axios");

/**
 * Define some custom errors as explained in the earlier paragraph:
 */ 

class FetchError extends Error {
  constructor(originalError) {
    super(originalError.message);
    this.name = "FetchError";
    this.originalError = originalError;
  }
}

class RequestError extends FetchError {
  constructor(...params) {
    super(...params);
    this.name = "RequestError";
  }
}

class ResponseError extends FetchError {
  constructor(...params) {
    super(...params);
    this.name = "ResponseError";
  }
}

class NotFoundError extends ResponseError {
  constructor(...params) {
    super(...params);
    this.name = "NotFoundError";
  }
}

Фрагмент кода, в котором вы выполняете обработку ошибок, довольно просто идентифицировать:

const fetch = async (url) => {
  try {
    const { data: { title } } = await axios.get(url);
    return `Todo: ${title}`;
  } catch (err) {
    // --->
    // HANDLE ERROR HERE!
    // <--- 
  }
};

Без try...catch вам может потребоваться разрешить вложенные условные выражения, что является довольно плохим способом решения этой проблемы:

/**
 * ❌ bad way, using nested conditionals:
 */

} catch (err) {
  if (err.response) {
    if (err.response.status === 404) {
      throw new NotFoundError(err);
    }
    throw new ResponseError(err);
  }
  if (err.request) {
    throw new RequestError(err);
  }
  throw new FetchError(err);
}

Но вы можете разумно использовать try...catch и SRP и создать функцию, которая определяет, какую ошибку выдавать более читаемым способом:

/**
 * ✅ good way, using try...catch and SRP:
 */

// Specialized function that identifies the error thrown by Axios:
const getFetchError = (err) => {
  // Test for a Request error:
  try {
    if (err.response.status === 404) {
      return new NotFoundError(err);
    }
    return new ResponseError(err);
  } catch (err) {}

  // Test for a Response error:
  try {
    if (err.request) {
      return new RequestError(err);
    }
  } catch (err) {}

  // Fallback on a generic error:
  return new FetchError(err);
};

// Here goes the entire `fetch()` implementation:
const fetch = async (url) => {
  try {
    const { data: { title } } = await axios.get(url);
    return `Todo: ${title}`;
  } catch (err) {
    throw getFetchError(err);
  }
};

Даже если для этого упрощенного примера «хороший способ» означает написание большего количества кода, мы все равно добьемся:

  1. лучшая читаемость
  2. лучшее модульное тестирование
    (поскольку мы можем тестировать функцию getFetchError() в полной изоляции.)
  3. выполнение СРП

Выводы

JavaScript - хороший язык.
У него слишком много наследия, и вам действительно стоит избегать его 😉.

Первоначально опубликовано на https://marcopeg.com.

👋 Присоединяйтесь к FAUN сегодня и получайте похожие истории каждую неделю на свой почтовый ящик! Получите еженедельную дозу обязательных к прочтению технических статей, новостей и руководств.

Подписывайтесь на нас в Twitter 🐦 и Facebook 👥 и Instagram 📷 и присоединяйтесь к нашим Facebook и Linkedin Группы 💬

Если этот пост был полезен, пожалуйста, нажмите несколько раз кнопку хлопка 👏 ниже, чтобы выразить поддержку автору! ⬇