☕ 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
- String.prototype.replaceAll: заменяет в строке все совпадения с шаблоном.
- Promise.any: принимает коллекцию промисов и ожидает выполнения любого из них.
- WeakRef: содержит слабую ссылку на другой объект, которая не препятствует сборке мусора.
- FinalizationRegistry: вызывает коллбэк при уничтожении объекта сборщиком мусора.
- Приватные поля классов.
- Разделитель разрядов для чисел.
- Intl.ListFormat: форматирование списков.
- Intl.DateTimeFormat: форматирование даты и времени.
ES2020/ES11
- BigInt: способ представления больших чисел.
- import(): динамический импорт модулей.
- Nullish coalescing: удобная проверка на null и undefined.
- globalThis: глобальный объект.
- Promise.allSettled: принимает коллекцию промисов и ожидает их выполнения, возвращает массив с результатами.
- Опциональные последовательности: позволяет обращаться к свойству на любом уровне вложенности без проверки на наличие каждого свойства в цепочке.
- String.prototype.matchAll: поиск совпадений в строке.
- Именованный экспорт из модулей.
- import.meta: мета-данные модулей.
ES2019/ES10
- Array.flat: объединение массивов с указанием глубины.
- Array.flatmap: создание массива с коллбэком для каждого элемента.
- Object.fromEntries: преобразует список пар ключ-значение в объект.
- String.prototype.trimStart, String.prototype.trimEnd: удаление пробелов из строки.
- Function.prototype.toString: преобразование функций к строке.
- Symbol.prototype.description: возможность добавить описание для символов.
ES2018/ES9
- Асинхронные циклы.
- Promise.finally: вызывается при любом исходе промиса.
- Rest/Spread-синтаксис.
- Именованные группы захвата в регулярных выражениях.
- Флаг
s
в регулярных выражениях: режим dotall – точка может соответствовать символу переноса строки.
ES2017/ES8
- Object.entries: возвращает пары ключей и значений объекта.
- Object.values: возвращает массив значений объекта.
- String.prototype.padStart, String.prototype.padEnd: заполнение строки пробелами до нужной длины.
- Object.getOwnPropertyDescriptors: возвращает все собственные дескрипторы свойств объекта.
- Асинхронные функции.
ES2016/ES7
- Array.prototype.includes: проверка наличия значения в массиве.
- Возведение в степень.