☕ 33 приема оптимизации JavaScript, которые вы должны знать в 2021 году

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

Хороший разработчик всегда должен учиться чему-то новому и совершенствовать навыки, которые у него уже есть. Поэтому сейчас мы будем учиться писать более чистый и понятный код!

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

В конце статьи приведен список современных возможностей языка, разделенный на версии.

Объявление переменных и присваивание значений

1. Объявление нескольких переменных

Переменные можно объявлять через запятую, при этом не требуется повторно использовать инструкцию let:

// Длинно
let test1 = 1;
let test2 = 2;

// Коротко
let test1 = 1, test2 = 2;

Такой подход особенно удобен при объявлении нескольких переменных без моментального присваивания значений:

let test1, test2, test3, test4, test5;

2. Арифметические операции

Присваивание в JavaScript легко совмещается с арифметическими операторами:

// Длинно
test1 = test1 + 1;
test2 = test2 - 1;
test3 = test3 * 20;

// Коротко
test1++;
test2--;
test3 *= 20;

3. Присваивание значений нескольким переменным

Если требуется присвоить значения сразу нескольким переменным, мы обычно делаем так:

let test1, test2, test3;
test1 = 1;
test2 = 2;
test3 = 3;

Но всю эту логику можно свести в одну строку, если использовать деструктуризацию:

let [test1, test2, test3] = [1, 2, 3];

Синтаксис деструктуризации работает не только с массивами, но и с обычными объектами:

const data = {
  test1: 1,
  test2: 2,
  test3: 3
};

// Длинно
const test1 = data.test1;
const test2 = data.test2;
const test2 = data.test3;

//shorthand
const { test1, test2, test3 } = data;

4. Присваивание со значением по умолчанию

Иногда требуется положить в переменную какое-то значение, предварительно убедившись, что это значение существует и не является falsy (null, undefined, пустая строка, 0).

Для этого можно использовать обычную конструкцию if-else:

let test2;
if (test1) {
  test2 = test1;
} else {
  test2 = 'default value';
}

Можно написать короче.

Взять, например, логический оператор ||. Если в переменной находится truthy-значение, то будет возвращено оно, иначе – дефолтное значение с правой стороны оператора.

let test 2 = test1 || 'default value';

Еще более современный подход – nullish coalescing operator??. Однако он проверяет не на все falsy-значения, а только на null и undefined. В остальном логика такая же, как у оператора ||.

const test = null ?? 'default value';
console.log(test); // 'default value'

const test1 = 0 ?? 2;
console.log(test1); // 0

Условия

5. Автоматическая проверка на truthy и falsy значения

Иногда требуется проверить, есть ли в переменной какое-либо значение. При этом важно учитывать и null, и undefined, и другие falsy-значения (NaN, пустая строка, 0).

if (test1 !== null || test1 !== undefined || test1 !== '') {
    // logic
}

К счастью, нет необходимости проводить все проверки по отдельности, тем более, что при этом легко что-нибудь упустить. Например, 0, который в ряде ситуаций тоже может расцениваться как "пустое" значение.

Можно просто положиться на JavaScript и его динамическую конверсию типов.

if (test1) {
  // logic
}

Оператор if самостоятельно приведет переменную к логическому значению и осуществит проверку.

6. Логические операторы вместо if

В пункте Присваивание с дефолтным значением мы уже видели, как логический оператор (||) может заменить конструкцию if.

Оператор && также способен на многое:

// Длинно
if (test1) {
 callMethod(); 
} 
// Коротко 
test1 && callMethod();

Если test1 является falsy-значением, то до инструкции callMethod() выполнение не дойдет.

7. Тернарный оператор вместо if

Зачастую простые конструкции if-else можно заменить еще более простым тернарным оператором:

// Длинно
let test: boolean;
if (x > 100) {
    test = true;
} else {
    test = false;
}

// Коротко
let test = (x > 10) ? true : false;

// Еще короче
let test = x > 10;

Таким образом можно преобразовать и более сложные условия, даже вложенные:

let x = 300,
test2 = (x > 100) 
        ? 'greater 100' 
        : (x < 50) 
           ? 'less 50' 
           : 'between 50 and 100';

console.log(test2); // "greater than 100"

Важно: чем сложнее условие, тем запутаннее выглядит тернарный оператор и предпочтительнее – стандартная конструкция if-else.

8. Проверка на значение из набора

Если требуется выполнить какие-то действия, когда x равен одному из нескольких значений, первым порывом может быть использование обычных сравнений:

if (x === 'abc' || x === 'def' || x === 'ghi' || x ==='jkl') {
    //logic
}

Но чем больше таких "подходящих" значений, тем сильнее разрастается условие и тем проще сделать ошибку. Проще поместить все эти значения в массив и использовать метод Array.prototype.includes для проверки:

if (['abc', 'def', 'ghi', 'jkl'].includes(x)) {
   //logic
}

Важно: метод includes использует строгое сравнение с учетом типа аргумента.

9. Коллекция вместо switch

Вместо того, чтобы рассматривать каждый случай внутри инструкции switch, можно добавить их все в объект:

// Длинно
switch (data) {
  case 1:
    test1();
  break;

  case 2:
    test2();
  break;

  case 3:
    test();

  break;
  // ...
}

// Коротко
const collection = {
  1: test1,
  2: test2,
  3: test
};

collection[data] && collection[data]();

Циклы

10. Короткий синтаксис for

Всем знакомый старый-добрый цикл for имеет довольно громоздкую структуру:

for (var i = 0; i < testData.length; i++) {
  // i - индекс элемента
  // доступ к значению элемента i
  var item = testData[i];
}

К счастью, в последних версиях языка появилось несколько более удобных альтернатив:

for (let i in testData) {
  // i - индекс элемента
  // доступ к элементу i
  var item = testData[i];
}

for (let i of testData) {
  // i - значение элемента
}

Кроме того, есть замечательный функциональный метод forEach:

function testData(element, index, array) {
  console.log('test[' + index + '] = ' + element);
}

[11, 24, 32].forEach(testData);
// logs: test[0] = 11, test[1] = 24, test[2] = 32

Такой код выглядит очень понятно, но у него есть некоторые ограничения. Например, перебор массива методом forEach нельзя прервать с помощью инструкции break.

Числа

11. Конверсия строки в число

Одиночный оператор сложения (+) неявно приводит тип полученного аргумента к числу.

// Длинно
let test1 = parseInt('123'); 
let test2 = parseFloat('12.3'); 

// Коротко
let test1 = +'123'; 
let test2 = +'12.3';

12. Экспоненциальная запись

Избавиться от большого количества нулей поможет экспоненциальная запись:

// Длинно
for (var i = 0; i < 10000; i++) { ... }

// Коротко
for (var i = 0; i < 1e4; i++) { ... }

13. Возведение в степень

Если вы вдруг до сих пор не знали, то пришло время узнать – в JavaScript есть специальный оператор для возведения в степень:

// Длинно
Math.pow(2,3); 

// Коротко
2**3 

14. Округление

Двойное побитовое отрицание для 32-битных целых чисел дает такой же эффект, как Math.floor (округляет вниз):

// Длинно
Math.floor(1.9) === 1 // true

// Коротко
~~1.9 === 1 // true

Строки

15. Получение символа из строки

Строка – это по сути массив символов, поэтому к каждому из них можно обратиться по индексу:

let str = 'abc';

// Длинно
str.charAt(2); // c

// Коротко
str[2]; // c

16. Повторение строки

Чтобы соединить несколько одинаковых строк в одну, существует несколько подходов. Самый очевидный – и самый громоздкий – использовать цикл:

let test = ''; 
for(let i = 0; i < 5; i ++) { 
  test += 'test '; 
} 

Но есть и более логичный – встроенный метод String.prototype.repeat():

let test = 'test '.repeat(5);

Бонус для любителей нестандартных решений:

let test = Array(6).join('test ');

17. Конкатенация

Составление одной строки из нескольких фрагментов – ужасная головная боль. Нас спасут шаблонные литералы:

// Длинно
const welcome = 'Hi ' + test1 + ' ' + test2 + '.'

// Коротко
const welcome = `Hi ${test1} ${test2}.`;

Они не только позволяют избежать многочисленных конкатенаций, но и поддерживают многострочность.

// Длинно
const data = 'abc abc abc abc abc abc\n\t'
    + 'test test,test test test test\n\t'

// Коротко
const data = `abc abc abc abc abc abc
         test test,test test test test`

Массивы

18. Наличие элемента

Обычно чтобы проверить, присутствует ли элемент в массиве, мы используем метод indexOf и сравниваем найденный индекс с -1.

if (arr.indexOf(item) > -1) { 
  // элемент в массиве есть
}

if(arr.indexOf(item) === -1) { 
  // элемента в массиве нет
}

Но можно использовать другой подход, без сравнения индексов – побитовый оператор ~.

if(~arr.indexOf(item)) { 
  // элемент в массиве есть
}

if(!~arr.indexOf(item)) { 
  // элемента в массиве нет
}

Оператор ~ возвращает 0 только для значения -1. Для всех других значений будет возвращено число, отличное от нуля, то есть truthy-значение.

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

Не стоит забывать и про удобнейший метод Array.prototype.includes:

if (arr.includes(item)) { 
  // элемент в массиве есть
}

19. Поиск элемента

Метод Array.prototype.find – это удобная функциональная замена простому перебору элементов массива с помощью цикла for:

// Длинно
function findElementByName(arr, name) {
    for (let i = 0; i < arr.length; ++i) {
        if (arr[i].name === name) {
            return arr[i];
        }
    }
}

// Коротко
function findElementByName(arr, name) {
  return arr.find(el => el.name === name);
}

const data = [
  { name: 'John' },
  { name: 'Jane' },
]
findElementByName(data, 'Jane'); // { name: 'Jane' }

20. Spread-синтаксис

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

Например, для конкатенации массивов:

// Стандартный подход
const data = [1, 2, 3];
const test = [4, 5, 6].concat(data);

// Spread-синтаксис
const data = [1, 2, 3];
const test = [4, 5, 6, ...data];

console.log(test); // [ 4, 5, 6, 1, 2, 3]

Или клонирования массива:

// Стандартный подход
const test1 = [1, 2, 3];
const test2 = test1.slice()

// Spread-синтаксис
const test1 = [1, 2, 3];
const test2 = [...test1];

21. Минимальное и максимальное значение

Методы Math.max и Math.min могут принимать любое количество аргументов. Чтобы передать им массив, можно использовать метод Function.prototype.apply:

const arr = [1,2,3];
Math.max.apply(null, arr); // 3
Math.min.apply(null, arr); // 1

А можно воспользоваться деструктуризацией:

Math.max(…arr); // 3
Math.min(…arr); // 1

Объекты

22. Присваивание значений

Если имя свойства совпадает с именем переменной, в которой хранится значение, дублировать его необязательно:

let test1 = 'a'; 
let test2 = 'b';

// Длинно
let obj = {test1: test1, test2: test2}; 

// Коротко
let obj = {test1, test2};

23. Перебор ключей и значений

В современном JavaScript есть сразу 3 метода для перебора объектов: Object.keys(), Object.values() и Object.entries(). Каждый из них возвращает массив – ключей, значений или сразу и того, и другого.

Трансформировав структуру данных таким образом, мы можем легко проитерировать как обычный массив любым удобным способом.

const data = { test1: 'abc', test2: 'cde', test3: 'efg' };

Object.keys(data); // ['test1', 'test2', 'test3'];
Object.values(data); // ['abc', 'cde', 'efg'];
Object.entries(data); // [['test1','abc'],['test2','cde'],['test3','efg']]

Функции

24. Параметры по умолчанию

Современный стандарт JavaScript позволяет задать дефолтные значения параметров прямо в сигнатуре функции. Теперь не нужно проверять это отдельно:

// Длинно
function add(test1, test2) {
  if (test1 === undefined)
    test1 = 1;
  if (test2 === undefined)
    test2 = 2;
  return test1 + test2;
}

// Коротко
function add(test1 = 1, test2 = 2) {
  return test1 + test2;
}


add(5, 10); // 15
add(5); // 7
add(); // 3

add(5, null); // 5

Важно: дефолтное значение устанавливается только в том случае, если указанный параметр не передан или равен undefined. На null это не распространяется.

25. Операции в return

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

// Длинно
function check(test) {
  if (!test) {
    return 'default value';
  } else {
    return test;
  }
}

// Коротко 
function check(test) {
  return test || 'default value';
}

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

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

// Длинно
[1, 2, 3].map(function(i) { 
  return i * 2;
});

// Коротко
[1, 2, 3].map(i => i * 2);

Стрелочные функции позволяют возвращать значение неявно, без использования оператора return:

// Длинно
function calculate(diameter) {
  return Math.PI * diameter
}

// Коротко
calculate = diameter => Math.PI * diameter;

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

***

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

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

***

Современные возможности JavaScript

ES2021/ES12

ES2020/ES11

ES2019/ES10

ES2018/ES9

ES2017/ES8

ES2016/ES7

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ