Утилиты на 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 допускает адреса, которые не примет ни один почтовый провайдер, а любой достаточно короткий шаблон отвергает валидные. Практичная позиция — проверить правдоподобность формы, отсечь заведомо сломанное, а остальное оставить письму с подтверждением. Сниппет здесь занимает именно эту позицию, а не притворяется исчерпывающим.
У генерации слага похожая природа: транслитерация, снятие диакритики и схлопывание разделителей делаются просто, но что делать с нелатинскими алфавитами — решение продуктовое, а не техническое.
Заглавная первая буква строки
Короткий и правильный способ сделать заглавной только первую букву — плюс вариант, который делает заглавной первую букву каждого слова.
Как это работает: 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"
Косинусное сходство между двумя векторами
Измеряет, насколько похожи два вектора эмбеддинга — базовая операция для семантического поиска и 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) гарантирует уже нормализованные векторы единичной длины, то простое скалярное произведение само по себе уже равно косинусному сходству и дешевле в вычислении при масштабе — эта версия обрабатывает общий случай, когда векторы не нормализованы заранее.
Управление уникальным отсортированным списком тегов
Операции добавления/удаления для массива тегов, который всегда остаётся уникальным и отсортированным по алфавиту — удобно для тег-пикеров и фильтров-чипов.
Как это работает: 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']
Глубокое клонирование объекта
Создаёт полную независимую копию объекта без ссылок — использует нативный 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'] — оригинал не тронут
Компактное форматирование чисел (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"
Генерация 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'
Генерация 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"
Выбор и исключение ключей объекта
Два небольших хелпера для создания нового объекта только с указанными ключами (или без них) — удобно для обрезки 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]' }
Склонение слов через 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 яблока"
Удаление дублей из массива
Способ в одну строку убрать дубли из массива примитивов через 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' }]
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
}
Преобразование произвольных значений в 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
Обрезать текст по количеству слов
Урезает строку до заданного числа слов и добавляет суффикс («…» по умолчанию). В отличие от обрезки по символам, не разрывает слова посередине.
Как это работает: строка разбивается по пробелам, считаются слова. Если слов меньше лимита — возвращается оригинал без изменений. Иначе берётся нужное количество слов и прибавляется суффикс.
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…'
Валидация 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
Валидация медиа-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
Другие подборки сниппетов
Контактная информация
Оставьте заявку или напишите напрямую в телеграм. Я отвечу в течение 24 часов.