29 ноября 2020

☕ 25 полезных сниппетов JavaScript для вашей коллекции

Frontend-разработчик в Foquz. https://www.cat-in-web.ru/
Подборка полезных фрагментов кода на JavaScript для решения распространенных задач программирования. Этот джентльменский набор сниппетов пригодится любому разработчику.
☕ 25 полезных сниппетов JavaScript для вашей коллекции

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

1. Проверка типа

JavaScript не имеет сильной системы типов, что многие считают серьезным недостатком. Поклонники строгости по достоинству оценят TypeScript, но и в самом JS есть некоторые полезные возможности. Например, провести простую проверку типа можно с помощью ключевого слова typeof.

Однако его возможности ограничены. С примитивами и функциями все работает как надо, но массивы от простых объектов вы отличить не сможете – они все имеют тип object. И не будем забывать про null, который тоже object, о чем часто забывают.

Давайте соберем все возможные проверки в одну полезную функцию:

isOfType.js
        const isOfType = (() => {
  // создаем пустую коллекцию без прототипа
  const type = Object.create(null);

  // проверка на null
  type.null = x => x === null;
  // проверка на undefined
  type.undefined = x => x === undefined;
  // проверка на nil - сюда относятся и null, и undefined
  type.nil = x => type.null(x) || type.undefined(x);

  // проверка на строку и литерал String: 
  // 's', "S", new String()
  type.string = x => !type.nil(x) 
       && (typeof x === 'string' || x instanceof String);

  // проверка на число и литерал числа: 12, 30.5, new Number()
  // для NaN и Infinity оператор typeof также возвращает 'number',
  // поэтому их нужно исключить
  type.number = x => !type.nil(x) && ( 
    (!isNaN(x) && isFinite(x) && typeof x === 'number')
    || x instanceof Number);

  // проверка на булево значение и литерал Boolean: 
  // true, false, new Boolean()
  type.boolean = x => !type.nil(x) && 
    (typeof x === 'boolean' || x instanceof Boolean);

  // проверка на массив
  type.array = x => !type.nil(x) && Array.isArray(x);

  // проверка на объект и литерал объекта: 
  // {}, new Object(), Object.create(null)
  type.object = x => ({}).toString.call(x) === '[object Object]';

  // проверка на конкретный тип
  type.type = (x, X) => !type.nil(x) && x instanceof X;

  // проверка на Set
  type.set = x => type.type(x, Set);
  // проверка на Map
  type.map = x => type.type(x, Map);
  // проверка на Date
  type.date = x => type.type(x, Date);

  return type;
})();
    

Код функции на GitHub

2. Проверка на пустое значение

Часто требуется определить, является ли значение пустым. Однако само понятие пустоты разное для разных типов данных. Например, в пустой строке нет символов, а в пустом объекте – ключей. Соответственно, нужно использовать разные методы:

isEmpty.js
        function isEmpty(x) {
  if (Array.isArray(x) 
     || typeof x === 'string'
     || x instanceof String
  ) {
    return x.length === 0;
  }

  if (x instanceof Map || x instanceof Set) {
    return x.size === 0;
  }

  if (({}).toString.call(x) === '[object Object]') {
    return Object.keys(x).length === 0;
  }

  return false;
}
    

Код функции на Github

3. Получение последнего элемента

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

Главное не забыть об особенностях разных типов списков:

lastIem.js
        function lastItem(list) {
  if (Array.isArray(list)) {
    return list.slice(-1)[0];
  }
  
  if (list instanceof Set) {
    return Array.from(list).slice(-1)[0];
  }

  if (list instanceof Map) {
    return Array.from(list.values()).slice(-1)[0];
  }
}
    

Код функции на Github

4. Генератор случайного числа в заданном диапазоне

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

randomNumber.js
        function randomNumber(max = 1, min = 0) {
  if (min >= max) { return max; }
  return Math.floor(Math.random() * (max - min) + min);
{
  
    

Код функции на GitHub

5. Генератор случайного id

Если вам просто нужен уникальный идентификатор (а не что-то сложное вроде UUID), то вы вполне можете обойтись без специальных библиотек. Используйте текущее время, инкремент или символы алфавита:

unique-id.js
        // использование текущего времени в миллисекундах в качестве основы
// увеличение при каждом запросе с помощью функции-генератора
const uniqueId = (() => {
  const id = (function*() {
    let mil = new Date().getTime();
    while (true) 
      yield mil += 1;
  })();
  
  return () => id.next().value;
})();

// создание уникального id заданной длины
// на основе полученного значения (или нуля)
// увеличение при каждом запросе
const uniqueIncrementingId = ((lastId = 0) => {
  const id = (function*() {
    let numb = lastId;
    while (true)
      yield numb += 1;
  })();

  return (length = 12) => 
      `${id.next().value}`.padStart(length, '0');
})();

// создание уникального id из букв и цифр
const uniqueAlphaNumericId = (() => {
  const heyStack = '0123456789abcdefghijklmnopqrstuvwxyz';
  const randomInt = () => 
    Math.floor(Math.random() * Math.floor(heyStack.length));
  return (length = 24) => 
    Array.from({ length }, () => heyStack[randomInt()]).join('');
})();
    

Код функции на Github

6. Создание диапазона чисел

В JavaScript очень не хватает функции для генерации последовательности чисел – вроде range в Python. Она была бы очень полезна для циклов for…of и других ситуаций, когда нужен определенный диапазон значений.

range.js
        function range(maxOrStart, end = null, step = null) {
  if (!end) {
    return Array.from({length: maxOrStart}, (_, i) => i);
  }

  if (end <= maxOrStart) {
    return [];
  }

  if (step !== null) {
    return Array.from(
      {length: Math.ceil(((end - maxOrStart) / step))},
      (_, i) => (i * step) + maxOrStart
    );
  }

  return Array.from(
    {length: Math.ceil((end - maxOrStart))},
    (_, i) => i + maxOrStart
  );
}
    

Код функции на Github

7. Форматирование JSON и сериализация данных

Наверняка во время разработки вы активно пользуетесь консолью для тестирования и вывода данных. Для этого удобно применять метод JSON.stringify. Его третий параметр определяет количество пробелов для форматирования отступов. Кроме того есть второй параметр, который может быть полезен для обработки непримитивных типов данных – функций, символов и т.п., которые обычно игнорируются при сериализации.

stringifier.js
        const obj = {
  name: 'John Doe',
  family: [
    {
      name: 'Jane Doe',
    }
  ],
  something: [12, 3, 45],
  method() {
    return 'i am ignored';
  },
  set: new Set([1, 4, 5]),
  map: new Map([1, 4], [5, 10]]),
  symb: Symbol('test')
};

const replacer = (key, val) => {
  if (typeof val === 'symbol') {
    return val.toString();
  }

  if (val instanceof Set) {
    return Array.from(val);
  }

  if (val instanceof Map) {
    return Array.from(val.entries());
  }

  if (typeof val === 'function') {
    return val.toString();
  }

  return val;
};

console.log(JSON.stringify(obj));
console.log(JSON.stringify(obj, null, 4));
console.log(JSON.stringify(obj, replacer));
    
☕ 25 полезных сниппетов JavaScript для вашей коллекции

Код функции на Github

8. Последовательное выполнение промисов

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

async-sequentializer.js
        const asyncSequentializer = (() => {
  const toPromise = (x) => {
    // если это промис, просто вернуть его
    if (x instanceof Promise) { 
      return x;
    }

    if (typeof x === 'function') {
      // если не асинхронная функция
      // то преобразовать в асинхронную,
      // выполнить и вернуть промис
      return (async () => await x())();
    }

    return Promise.resolve(x);
  }

  // принимает список функций, асинхронных функций или промисов
  return (list) => {
    const results = [];

    return list
      .reduce((lastPromise, currentPromise) => {
        return lastPromise.then(res => {
          results.push(res); // собрать результаты
          return toPromise(currentPromise);
        });
      }, toPromise(list.shift()))
      // собрать результаты последнего промиса
      // вернуть массив результатов
      .then(res => Promise.resolve([...results, res]));
  }
})();
    

Код функции на GitHub

9. Постоянный опрос (polling)

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

polling.js
        async function poll(fn, validate, interval = 2500) {
  const resolver = async (resolve, reject) => {
    // отлов ошибок, выбрасываемых функцией fn
    try { 
      const result = await fn(); // fn необязательно должна возвращть промис
      // вызов валидатора для проверки полученного результата
      const valid = validate(result);

      if (valid === true) {
        resolve(result);
      } else if (valid === false) {
        setTimeout(resolver, interval, resolve, reject);
      } 
      // если валидатор возвращает что-то кроме true/false,
      // опрос прекращается
    } catch (e) {
      reject(e);
    }
  };

  return new Promise(resolver);
}
    

Код функции на GitHub

10. Работа с наборами промисов

Раньше не существовало нативных функций для работы с наборами промисов, поэтому приходилось изобретать собственные решения. Но Promise API постоянно развивается, и теперь у нас есть замечательные методы allSettled, race and any, с которыми стоит познакомиться каждому разработчику.

Promise.all

Ожидает, когда выполнятся (resolve) все промисы в списке или хотя бы один из них будет отклонен (reject). Если не было ошибок, метод вернет массив с результатами выполнения промисов, сохранив их исходный порядок.

Promise.allSettled

Ожидает завершения всех промисов, независимо от их статуса выполнения. Возвращает массив с результатами. Каждый объект в этом массиве имеет поле status, которое может быть равно fullfilled или rejected. Если промис выполнился, то результат будет в поле value. Если же он был отклонен, то причина будет в поле reason.

Promise.any

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

Promise.rase

Ожидает самого первого завершения любого промиса из списка независимо от его статуса.

        const prom1 = Promise.reject(12);
const prom2 = Promise.resolve(24);
const prom3 = Promise.resolve(48);
const prom4 = Promise.resolve('error');

Promise.all([prom1, prom2, prom3, prom4])
  .then(res => console.log('all', res))
  .catch(err => console.log('all failed', err));

Promise.allSettled([prom1, prom2, prom3, prom4])
  .then(res => console.log('allSettled', res))
  .catch(err => console.log('allSettled failed, err));

Promise.any([prom1, prom2, prom3, prom4])
  .then(res => console.log('any', res))
  .catch(err => console.log('any failed, err));

Promise.race([prom1, prom2, prom3, prom4])
  .then(res => console.log('race', res))
  .catch(err => console.log('race failed, err));
    

11. Перестановка элементов массива

Не то чтобы это была очень трудная задача, однако полезно знать, что в ES6 существует удобный способ ее решения.

swap.js
        const array = [12, 24, 48];

// старый способ
const swapOldWay = (arr, i, j) => {
  const arrayCopy = [...arr];
  let temp = arrayCopy[i];
  arrayCopy[i] = arrayCopy[j];
  arrayCopy[j] = temp;
  return arrayCopy;
};

// новый способ
const swapNewWay = (arr, i, j) => {
  const arrayCopy = [...arr];
  [arrayCopy[0], arrayCopy[2]] = [arrayCopy[2], arrayCopy[0]];
  return arrayCopy;
}

console.log(swapOldWay(array, 0, 2));
// [48, 24, 12]
console.log(swapNewWay(array, 0, 2));
// [48, 24, 12]
    

12. Добавление ключа в объект по условию

Этот трюк очень полезен при работе с состояниями в React. Ключ добавляется (или не добавляется) в объект в зависимости от некоторого условия с помощью spread-оператора.

Условие задается с применением тернарного оператора. Если оно выполняется, вернется объект с ключами, которые нужно добавить в главный объект. Если не выполняется – пустой объект.

        let condition = true;

const man = {
  someProperty: 'some value',
  // условие
  ...(condition === true ? { newProperty: 'value' } : {})
}
  
  
    

13. Использование переменных в качестве ключей объекта

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

        let property = 'newValidProp';

const man = {
  someProperty: 'some value',

  [`${property}`]: 'value',
}
    

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

        let property = 'newValidProp';

const man = {
  someProperty: 'some value',

  [property]: 'value',
}
    

14. Проверка наличия ключа в объекте

Обычно для такой проверки нужен оператор in. Однако он учитывает не только собственные свойства объекта, но также и свойства всех его прототипов (как и цикл for...in). Это обычно не является желательным поведением. Лучше всего использовать проверку методом hasOwnProperty.

        const sample = {
    prop: 'value'
}

console.log('prop' in sample); // true
console.log('toString' in sample); // true

console.log(sample.hasOwnProperty('prop')); // true
console.log(sample.hasOwnProperty('toString')); // false
    

15. Удаление дублирующихся элементов массива

Избавиться от повторяющихся элементов массива очень просто – потребуется структура Set. Она отлично работает с разными типами данных и использует специфический алгоритм сравнения значений.

        const numberArrays = [
  undefined, Infinity,
  12, NaN, false, 5, 7, 
  null, 12, false, 5,
  undefined, 89, 9, null,
  Infinity, 5, NaN];

console.log(Array.from(new Set(numberArrays));
// [undefined, Infinity, 12, NaN, false, 5, 7, null, 89, 9]



    

С объектами это, конечно, работать не будет, так как каждый объект уникален. Поэтому нужно вводить дополнительный фильтр:

        const objArrays = [{id: 1}, {id: 4}, {id: 1}, {id: 5}, {id: 4}];

// Set воспринимает каждый объект как уникальное значение
console.log(Array.from(new Set(objArrays));
// [{id: 1}, {id: 4}, {id: 1}, {id: 5}, {id: 4}]

// фильтрация по id
const idSet = new Set();
console.log(
  objArrays.filter(obj => {
    const existingId = idSet.has(obj.id); 
    idSet.add(obj.id);

    return !existingId;
  })
);
// [{id: 1}, {id: 4}, {id: 5}]


    

16. break и continue в цикле forEach

Метод Array.forEach – очень удобная функциональная альтернатива обычному циклу for. Однако у него есть недостатки, например, отсутствие возможности прервать цикл в любой момент – по аналогии с оператором break.

Чтобы эмулировать это поведение, сохранив все преимущества, можно вызвать другой метод – Array.some. Он прекращает перебор элементов, если функция возвращает true. Используем этот трюк вместо break:

        const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];

for (const number of numbers) {
  if (number % 2 === 0) {
    continue; // переход к следующему элементу
  }
  if (number > 5) {
    break;
  }

  console.log(number);
}
// 1, 3, 5

numbers.some(number => {
  if (number % 2 === 0) {
    return false; // переход к следующему элементу
  } 

  if (number > 5) {
    return true; // остановка перебора
  }

  console.log(number);
});
    

17. Деструктуризация с псевдонимами и дефолтными значениями

Деструктуризация – одна из самых мощных и удобных функций JavaScript, но синтаксис у нее довольно запутанный и сложный для запоминания. Поэтому повторяем еще раз, как задавать псевдонимы для названий полей и устанавливать значения по умолчанию:

        function demo1({ dt: data }) {
  // переименование dt в data
  console.log(data);  
}

function demo2({ dt: {name, id = 10}}) {
  // глубокая деструктуризация поля dt
  // для параметра id установлено значение по умолчанию - 10
  console.log(name, id);  
}

demo1({ dt: { name: 'sample', id: 50 } });
//{ name: 'sample', id: 50 }

demo2({ dt: { name: 'sample' } });
// 'sample', 10
    

18. Опциональные последовательности и нулевое слияние

Еще две новых фичи JavaScript, которые существенно облегчают труд разработчика – optional chaining и nullish coalescing.

Нулевое слияние

Очень полезно при работе с неопределенными значениями. Оператор ?? возвращает правое значение только в том случае, если левое равно null или undefined. То есть он сохраняет все остальные falsy-значения, например, 0 или пустую строку.

        const obj = {
  data: {
    container: {
      name: {
        value: 'sample'
      },
      int: {
        value: 0
      }
    }
  }
};

// #1
console.log(obj.data.container.int.value || 'no int value');
// no int value

// #2
console.log(obj.data.container.int.value ?? 'no int value');
// будет выведено: 0
    

В первом примере указанное поле существует, но его значение равно 0, что интерпретируется как falsy. Поэтому оператор || вернет правую часть – дефолтную строку.

Опциональные последовательности

Очень удобный прием для работы со структурами с большим уровнем вложенности.

        const obj = {
  data: {}
};

console.log(obj.data.wrapper.name.value);
// Cannot read property 'name' of undefined
    

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

        console.log(
  (obj && obj.data && obj.data.wrapper && obj.data.wrapper.name) || 'no name'
);
// 'no name'
    

Чем глубже структура объекта, тем сложнее становится конструкция и тем проще в ней ошибиться.

Оператор опциональных последовательностей ? позволяет существенно сократить это выражение:

        console.log((obj?.data?.wrapper?.name) || 'no name');
// no name
    

19. Классы – это синтаксический сахар

Используя классы JavaScript, нельзя забывать, что это лишь синтаксический сахар над прототипами и функциями-конструкторами. Один из способов убедиться в этом – унаследовать класс от обычной функции-конструктора.

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

        function Parent() {
  const privateProp = 12;
  const privateMethod = () => privateProp + 10;

  this.publicMethod = (x = 0) => privateMethod() + x;
  this.publicProp = 10;
}

class Child extends Parent {
  myProp = 20;
}

const child = new Child();

console.log(
  child.myProp, // 20
  child.publicProp, // 10
  child.publicMethod(40), // 62
  child.privateProp, // undefined
  child.privateMethod(), // ошибка "child.privateMethod is not a function"
);
    

20. Сложный конструктор

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

Всю эту логику можно поместить прямо в функцию-конструктор. Для расширения родительских классов используйте методы apply или call.

        function Employee() {
  this.profession = 'Software Engineer';
  this.salary = '$150000';
}

function DeveloperFreelancer() {
  this.programmingLanguages = ['JavaScript', 'Python', 'Swift'];
  this.avgPerHour = '$100';
}

function Engineer(name) {
  this.name = name;
  this.freelancer = {};

  Employee.apply(this);
  DeveloperFreelancer.apply(this.freelancer);
}

const john = new Enginerr('John Doe');

console.log(
  john.name, // 'John Doe'
  john.profession, // 'Software Engineer'
  john.salary, // '$150000'
  john.freelancer, 
  // { programmingLanguages: ['JavaScript', 'Python', 'Swift'], avgPerHour: '$100' }
);
    

21. Итерирование всего

Для перебора элементов разных коллекций (Object, Array, Set, String и т. д.) приходится использовать разные методы. Было бы удобно объединить всю эту функциональность в один абстрактный метод.

Второй аргумент – callback – это функция для обработки элементов. Если она вернет true, цикл будет прерван (по аналогии с оператором break).

iterables.js
        function forEach(list, callback) {
  const entries = Object.entries(list);
  let i = 0;
  const len = entries.length;
  
  for(;i < len; i++) {
    const res = callback(entries[i][1], entries[i][0], list);
    
    if(res === true) break;
  }
}

forEach([1, 2, 3], console.log);
forEach(new Set([1, 2, 3]), console.log);
forEach(new Map([[1, 1], [2, 2], [3, 3]]), console.log);
forEach('123', console.log);
forEach({ a: 1, b: 2, c: 3 }, console.log);
    

Код функции на Github

22. Обязательные аргументы функций

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

required-arguments.js
        function required(argName = 'param') {
  throw new Error(`"${argName}" is required`)
}

function iHaveRequiredOptions(arg1 = required('arg1'), arg2 = 10) {
  console.log(arg1, arg2)
}

iHaveRequiredOptions(); // "arg1" is required
iHaveRequiredOptions(12); // 12, 10
iHaveRequiredOptions(12, 24); // 12, 24
iHaveRequiredOptions(undefined, 24); // "arg1" is required
    

Код функции на Github

23. Создание модулей и синглтонов

Иногда вам требуется какой-то глобальный для всей программу объект – синглтон. Можно долго спорить о том, насколько эта практика хороша, но полезно знать, как сделать это на JavaScript.

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

singletone.js
        class Service {
  name = 'service'
}

const service = (function(S) {
  // здесь может быть подготовка данных
  // или любые другие действия для инициализации сервиса
  const service = new S();
  return () => service;
})(Service);

const element = (function(S) {
  const element = document.createElement('DIV');
  // здесь могут быть любые действия с DOM
  // например, инициализация js-плагинов
  return () => element;
})();
    

24. Глубокое копирование объектов

Для многоуровневого копирования объектов часто подключаются сторонние библиотеки (например, lodash). Однако эту задачу легко решить в JavaScript с помощью несложной рекурсивной функции. На каждом уровне она клонирует объекты с помощью их конструктора и копирует все поля.

deep-clone.js
        const deepClone = obj => {
  let clone = obj;
  if (obj && typeof obj === "object") {
    clone = new obj.constructor();
    
    Object.getOwnPropertyNames(obj).forEach(
      prop => (clone[prop] = deepClone(obj[prop]))
    );
  }

  return clone;
};
    

Код функции на Github

25. Глубокая заморозка объектов

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

deep-freeze.js
        const deepFreeze = obj => {
  if (obj && typeof obj === "object") {
    if(!Object.isFrozen(obj)) {
      Object.freeze(obj);
    }
    
    Object.getOwnPropertyNames(obj).forEach(prop => deepFreeze(obj[prop]));
  }

  return obj;
};
    

Код функции на Github

Репозиторий со всеми сниппетами

Источники

МЕРОПРИЯТИЯ

Делитесь своими любимыми сниппетами в комментариях

ВАКАНСИИ

Добавить вакансию

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