Skip to content
← К сниппетам
JavaScript

Утилиты на JavaScript: строки, массивы, объекты

Пятнадцать небольших JavaScript-хелперов, которые всплывают в каждом проекте: дедупликация, глубокое копирование, слаги, UUID, компактные числа и склонение, безопасный разбор JSON и валидация email.

Это те самые функции, которые оседают в utils.js любого проекта, каждый раз переписываются чуть по-разному и почти никогда не стоят отдельной зависимости. Каждая ниже достаточно короткая, чтобы прочитать её целиком перед вставкой, — в этом и смысл: пятнадцать строк, которые вы понимаете, лучше транзитивной зависимости, которую не читал никто.

Те, у кого появился нативный ответ

Часть классики уже вошла в язык, и старый обходной путь теперь просто хуже. structuredClone копирует вглубь, включая Map, Set, Date и циклические ссылки, — при JSON.parse(JSON.stringify(x)) обычные Map и Set теряют содержимое, Date становится строкой, а циклическая ссылка вызывает ошибку. crypto.randomUUID() выдаёт UUID v4 по спецификации во всех актуальных браузерах и в Node. Intl.NumberFormat с notation: 'compact' превращает 1234 в «1,2 тыс.» на языке пользователя, а Intl.PluralRules выбирает правильную форму для языков, где форм больше двух, — то есть для большинства, включая русский с его тремя.

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

Те, где вся суть в краевом случае

JSON.parse бросает исключение на битом входе, поэтому любой разбор данных, которые пришли не от вас, должен быть обёрнут в функцию с запасным значением. Дедупликация массива — однострочник через Set, ровно до момента, когда элементы становятся объектами: сравнение по ссылке тихо перестаёт работать. Обрезка текста по символам режет слова пополам, а обрезка по словам требует решить, что считать словом. Приведение "false", "0" и "" к булеву через Boolean() даёт true для первых двух — редко то, чего ждут от разбора конфига.

Валидация — это выбор способа ошибаться

Правильного регулярного выражения для email не существует: грамматика из RFC 5322 допускает адреса, которые не примет ни один почтовый провайдер, а любой достаточно короткий шаблон отвергает валидные. Практичная позиция — проверить правдоподобность формы, отсечь заведомо сломанное, а остальное оставить письму с подтверждением. Сниппет здесь занимает именно эту позицию, а не притворяется исчерпывающим.

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

JavaScriptJavaScript

Заглавная первая буква строки

Короткий и правильный способ сделать заглавной только первую букву — плюс вариант, который делает заглавной первую букву каждого слова.

Как это работает: capitalize делает заглавной только первый символ, остальное не трогает; capitalizeWords использует регулярное выражение, находящее первую букву каждой границы слова (\b\w), чтобы сделать заглавной первую букву в каждом слове предложения.

function capitalize(str) {
  if (!str) return str
  return str.charAt(0).toUpperCase() + str.slice(1)
}

function capitalizeWords(str) {
  return str.replace(/\b\w/g, (char) => char.toUpperCase())
}

// Example usage
capitalize('hello world')      // "Hello world"
capitalizeWords('hello world') // "Hello World"
JavaScriptJavaScript

Косинусное сходство между двумя векторами

Измеряет, насколько похожи два вектора эмбеддинга — базовая операция для семантического поиска и RAG-извлечения.

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

function cosineSimilarity(a, b) {
  let dot = 0
  let normA = 0
  let normB = 0
  const len = Math.min(a.length, b.length)

  for (let i = 0; i < len; i++) {
    dot += a[i] * b[i]
    normA += a[i] * a[i]
    normB += b[i] * b[i]
  }

  const magnitude = Math.sqrt(normA) * Math.sqrt(normB)
  return magnitude === 0 ? 0 : dot / magnitude
}

// Example usage — compare a query embedding against a stored chunk
const similarity = cosineSimilarity(queryEmbedding, chunkEmbedding)
// closer to 1 = more similar

Примечание: если ваш провайдер эмбеддингов (например OpenAI) гарантирует уже нормализованные векторы единичной длины, то простое скалярное произведение само по себе уже равно косинусному сходству и дешевле в вычислении при масштабе — эта версия обрабатывает общий случай, когда векторы не нормализованы заранее.

JavaScriptJavaScript

Управление уникальным отсортированным списком тегов

Операции добавления/удаления для массива тегов, который всегда остаётся уникальным и отсортированным по алфавиту — удобно для тег-пикеров и фильтров-чипов.

Как это работает: addTag проходит через Set для устранения дублей, removeTag просто фильтрует — обе функции пересортировывают результат перед возвратом, поэтому массив всегда в предсказуемом порядке для рендера в виде чипов.

function addTag(tags, tag) {
  const set = new Set(tags)
  set.add(tag)
  return Array.from(set).sort()
}

function removeTag(tags, tag) {
  return tags.filter((t) => t !== tag).sort()
}

// Example usage
let tags = ['vue', 'css']
tags = addTag(tags, 'javascript') // ['css', 'javascript', 'vue']
tags = removeTag(tags, 'css')     // ['javascript', 'vue']
JavaScriptJavaScript

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

Создаёт полную независимую копию объекта без ссылок — использует нативный structuredClone там, где он доступен, и падает обратно на JSON-round-trip для старых сред.

Как это работает: structuredClone (Node 17+, все современные браузеры) корректно копирует Date, Map, Set, ArrayBuffer и циклические ссылки. JSON-fallback проще, но теряет undefined, Date, функции и специальные типы.

function deepClone(value) {
  if (typeof structuredClone === 'function') {
    return structuredClone(value)
  }
  // Fallback для старых сред (теряет Date, undefined, функции)
  return JSON.parse(JSON.stringify(value))
}

// Example usage
const original = { user: { name: 'Alice', tags: ['admin'] }, createdAt: new Date() }
const clone = deepClone(original)

clone.user.tags.push('editor')
console.log(original.user.tags) // ['admin'] — оригинал не тронут
JavaScriptJavaScript

Компактное форматирование чисел (1.2K, 3M)

Превращает большие числа в короткие читаемые обозначения — 600000 становится 600K.

Как это работает: функция проверяет порядок величины числа (миллиарды, миллионы, тысячи) и делит соответствующим образом, обрезая лишний ".0", чтобы "2.0M" читалось как "2M".

function formatCompactNumber(n) {
  const abs = Math.abs(n)
  if (abs >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1).replace(/\.0$/, '')}B`
  if (abs >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
  if (abs >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, '')}K`
  return `${n}`
}

// Example usage
formatCompactNumber(600000)   // "600K"
formatCompactNumber(2000000)  // "2M"
formatCompactNumber(1200)     // "1.2K"
JavaScriptJavaScript

Генерация slug из строки

Преобразует произвольную строку (в том числе с кириллицей) в URL-friendly slug — строчные буквы, без пробелов и спецсимволов, дефисы вместо пробелов.

Как это работает: сначала транслитерируется кириллица в латиницу, затем строка приводится к нижнему регистру, убираются все символы кроме букв, цифр и дефисов, а пробелы заменяются дефисами.

const CYRILLIC_MAP = {
  а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ё:'yo',ж:'zh',з:'z',и:'i',
  й:'j',к:'k',л:'l',м:'m',н:'n',о:'o',п:'p',р:'r',с:'s',т:'t',
  у:'u',ф:'f',х:'kh',ц:'ts',ч:'ch',ш:'sh',щ:'shch',ъ:'',ы:'y',ь:'',
  э:'e',ю:'yu',я:'ya',
}

function transliterate(str) {
  return str
    .split('')
    .map((ch) => CYRILLIC_MAP[ch.toLowerCase()] ?? ch)
    .join('')
}

function generateSlug(str) {
  return transliterate(str)
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_]+/g, '-')
    .replace(/^-+|-+$/g, '')
}

// Example usage
generateSlug('Привет, мир!')      // 'privet-mir'
generateSlug('Hello World 2026') // 'hello-world-2026'
generateSlug('  --foo bar--  ')  // 'foo-bar'
JavaScriptJavaScript

Генерация UUID

Создаёт случайный UUID (v4) по спецификации, используя встроенный Web Crypto API — без внешних библиотек.

Как это работает: crypto.randomUUID() (Node 14.17+, все современные браузеры) криптографически случаен и соответствует RFC 4122 v4; фолбэк воспроизводит тот же формат для старых окружений, но НЕ является криптографически безопасным.

function generateUuid() {
  if (typeof crypto !== 'undefined' && crypto.randomUUID) {
    return crypto.randomUUID()
  }
  // Fallback for environments without crypto.randomUUID (older Node/browsers)
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0
    const v = c === 'x' ? r : (r & 0x3) | 0x8
    return v.toString(16)
  })
}

// Example usage
generateUuid() // "3f9a1c2e-8b3d-4c1a-9f2e-6d8b1a4c7e9f"
JavaScriptJavaScript

Выбор и исключение ключей объекта

Два небольших хелпера для создания нового объекта только с указанными ключами (или без них) — удобно для обрезки payload перед отправкой в API.

Как это работает: pickKeys оставляет только запрошенные ключи, omitKeys оставляет всё, кроме них — оба создают новый объект, не изменяя исходный.

function pickKeys(obj, keys) {
  if (!obj) return {}
  return keys.reduce((acc, key) => {
    if (key in obj) acc[key] = obj[key]
    return acc
  }, {})
}

function omitKeys(obj, keys) {
  if (!obj) return {}
  return Object.entries(obj).reduce((acc, [key, value]) => {
    if (!keys.includes(key)) acc[key] = value
    return acc
  }, {})
}

// Example usage
const user = { id: 1, name: 'Alex', password: 'secret', email: '[email protected]' }
pickKeys(user, ['id', 'name']) // { id: 1, name: 'Alex' }
omitKeys(user, ['password'])   // { id: 1, name: 'Alex', email: '[email protected]' }
JavaScriptJavaScript

Склонение слов через Intl.PluralRules

Корректно склоняет существительные для любой локали — включая языки вроде русского с несколькими формами множественного числа — через встроенный Intl API.

Как это работает: Intl.PluralRules относит число к категории склонения ('one', 'few', 'many', 'other'...) по правилам локали; остаётся сопоставить каждой категории нужную форму слова, вместо того чтобы жёстко прописывать правило "добавить s", которое работает только для английского.

function pluralize(count, singular, options = {}) {
  const { locale = 'en-US', plural, forms = {} } = options
  const category = new Intl.PluralRules(locale).select(count)

  const word = forms[category] || (category === 'one' ? singular : plural || `${singular}s`)
  return `${count} ${word}`
}

// Example usage
pluralize(1, 'apple') // "1 apple"
pluralize(5, 'apple') // "5 apples"

// Russian needs multiple forms (one/few/many)
pluralize(3, 'яблоко', {
  locale: 'ru-RU',
  forms: { one: 'яблоко', few: 'яблока', many: 'яблок' }
}) // "3 яблока"
JavaScriptJavaScript

Удаление дублей из массива

Способ в одну строку убрать дубли из массива примитивов через Set, плюс паттерн для удаления дублей массива объектов по ключу.

Как это работает: Set хранит только уникальные значения, поэтому распаковка его обратно в массив убирает дубли примитивов в одну строку; для объектов (которые никогда не равны друг другу по ссылке) нужно вручную отслеживать увиденные значения ключа при фильтрации.

// Primitives — numbers, strings, etc.
const unique = [...new Set([1, 2, 2, 3, 3, 3])] // [1, 2, 3]

// Objects — dedupe by a specific key
function uniqueByKey(items, key) {
  const seen = new Set()
  return items.filter((item) => {
    if (seen.has(item[key])) return false
    seen.add(item[key])
    return true
  })
}

// Example usage
uniqueByKey(
  [{ id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 1, name: 'A (dup)' }],
  'id'
) // [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
JavaScriptJavaScript

JSON.parse, который никогда не бросает исключение

Оборачивает JSON.parse так, чтобы при некорректных данных возвращался null вместо падения — идеально для десериализации значений из Redis, localStorage или пользовательского ввода.

Как это работает: null/undefined/пустая строка на входе трактуются как "нет значения" и сразу возвращают null; всё остальное проходит через try/catch, так что некорректный JSON возвращает null вместо исключения.

function safeJsonParse(raw) {
  if (raw === null || raw === undefined || raw === '') return null

  try {
    return JSON.parse(raw)
  } catch {
    return null
  }
}

// Example usage — instead of a try/catch at every call site
const session = safeJsonParse(await redis.get('session:123'))
if (!session) {
  // treat as "no session" rather than crashing
}
JavaScriptJavaScript

Преобразование произвольных значений в boolean

Превращает строки вроде "yes"/"on"/"1" (и числа) в настоящий boolean — удобно для парсинга env-переменных, query-параметров или CSV-импорта.

Как это работает: строковые значения приводятся к нижнему регистру и обрезаются, затем сверяются со настраиваемыми списками "истинных"/"ложных" слов; числа считаются истинными, если они не 0 и не NaN; всё остальное обрабатывается через обычный Boolean(value).

function toBoolean(value, options = {}) {
  const {
    truthyValues = ['true', 'yes', 'on', 'y', '1'],
    falsyValues = ['false', 'no', 'off', 'n', '0', '']
  } = options

  if (typeof value === 'string') {
    const normalized = value.toLowerCase().trim()
    if (truthyValues.includes(normalized)) return true
    if (falsyValues.includes(normalized)) return false
  }

  if (typeof value === 'number') return value !== 0 && !Number.isNaN(value)

  return Boolean(value)
}

// Example usage
toBoolean('yes') // true
toBoolean('0')   // false
toBoolean(process.env.FEATURE_FLAG) // works great for env vars
JavaScriptJavaScript

Обрезать текст по количеству слов

Урезает строку до заданного числа слов и добавляет суффикс («…» по умолчанию). В отличие от обрезки по символам, не разрывает слова посередине.

Как это работает: строка разбивается по пробелам, считаются слова. Если слов меньше лимита — возвращается оригинал без изменений. Иначе берётся нужное количество слов и прибавляется суффикс.

function truncateWords(text, maxWords, suffix = '…') {
  const words = text.trim().split(/\s+/)
  if (words.length <= maxWords) return text
  return words.slice(0, maxWords).join(' ') + suffix
}

function truncateChars(text, maxChars, suffix = '…') {
  if (text.length <= maxChars) return text
  return text.slice(0, maxChars).trimEnd() + suffix
}

// Example usage
truncateWords('Привет это длинный текст для теста', 3) // 'Привет это длинный…'
truncateWords('Short', 10)                             // 'Short'
truncateChars('Hello World', 7)                        // 'Hello W…'
JavaScriptJavaScript

Валидация email-адреса

Базовая проверка формы email для интерфейса. Не покрывает весь допустимый синтаксис и не подтверждает существование адреса.

Regex проверяет привычную форму адреса с @ и точкой в домене. Это эвристика: она может отвергнуть допустимый редкий адрес и пропустить некорректный. Лимит в примере считается в символах JavaScript и не заменяет проверку ограничений SMTP в байтах. Для проверки владения адресом отправляйте письмо подтверждения. ASCII-вариант ниже также не является полной проверкой стандарта.

function isValidEmail(email) {
  if (!email || typeof email !== 'string') return false
  const trimmed = email.trim()
  if (trimmed.length > 254) return false
  return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(trimmed)
}

// Упрощённый ASCII-вариант; IDN возможен в записи Punycode
function isValidEmailStrict(email) {
  const re = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/
  return re.test((email ?? '').trim().toLowerCase())
}

// Example usage
isValidEmail('[email protected]')    // true
isValidEmail('[email protected]')  // true
isValidEmail('notanemail')          // false
isValidEmail('missing@tld')         // false
JavaScriptJavaScript

Валидация медиа-URL

Проверяет, что URL использует HTTPS и указывает на распознаваемое расширение изображения/видео/аудио — частая проверка перед принятием ссылки на медиа от пользователя.

Как это работает: isValidHttpsUrl использует то, что конструктор URL бросает исключение на некорректном вводе, вместо ручного регулярного выражения; isMediaUrl сверяет расширение со списком известных, также допуская query-строку после него (photo.jpg?size=large).

function isValidHttpsUrl(url) {
  if (!url) return false
  try {
    return new URL(url).protocol === 'https:'
  } catch {
    return false
  }
}

function isMediaUrl(url) {
  if (!url) return false
  const extensions = ['.jpg', '.jpeg', '.png', '.gif', '.mp4', '.mp3', '.ogg', '.wav']
  const lower = url.toLowerCase()
  return extensions.some((ext) => lower.endsWith(ext) || lower.includes(`${ext}?`))
}

// Example usage
isValidHttpsUrl('https://example.com/photo.jpg') && isMediaUrl('https://example.com/photo.jpg')
// true — safe to treat as a media attachment
// CONNECT

Контактная информация

Оставьте заявку или напишите напрямую в телеграм. Я отвечу в течение 24 часов.

Укажите email или Telegram, чтобы я мог вам ответить.