Editly

Опубліковано: 2026-05-12

Як рахувати слова в JavaScript: 5 методів порівняно

Від наївного split() до Intl.Segmenter — 5 методів підрахунку слів у JavaScript за точністю, підтримкою Unicode та крайовими випадками. Який везти в прод.

Таблиця порівняння підрахунку слів у JavaScript: split проти regex проти Intl.Segmenter для Unicode та CJK-мов

text.split(' ').length. Кожен джуніор-JS-розробник це відправляв у прод. І воно неправильне щонайменше у три різні способи.

Це повний розбір п'яти підходів до того, як рахувати слова в JavaScript — що кожен реально робить під капотом, де він ламається і який варто взяти. Спойлер: це Intl.Segmenter.


Чому рахувати слова важче, ніж здається

Текст відчувається простим. Слова ж розділені пробілами, так? Окрім випадків:

  • Подвійні пробіли між реченнями (hello world → split дає 3 токени, а не 2)
  • Нерозривні пробіли ( ) — невидимі в браузері, постійно вставляються з Word, PDF і Google Docs
  • Табуляції й переноси рядка — валідні пробільні символи, які split(' ') ігнорує
  • CJK-текст — у китайській, японській, корейській узагалі немає пробілів між словами
  • Емодзі — сімейне емодзі (👨‍👩‍👧‍👦) це 1 видимий символ, але 11 кодових одиниць UTF-16, 6 кодових точок Unicode і 1 кластер графем
  • Скорочення й дефісиdon't, state-of-the-art — це 1 слово чи 2?

Більшість багів підрахунку невидимі, поки на твій застосунок не натрапить неангломовний користувач.


П'ять методів

Метод 1: text.split(' ').length — наївний split

function countWords(text) {
  return text.split(' ').length;
}

Це перше, що люди пишуть. І воно неправильне одразу.

countWords('hello  world')  // → 3 (extra empty string token)
countWords('hello\tworld')  // → 1 (tab not counted as separator)
countWords('')              // → 1 (empty string gives [''], not [])

Вердикт: не відправляй це в прод. Ніколи.


Метод 2: text.trim().split(/\s+/).filter(Boolean).length — залатаний split

function countWords(text) {
  return text.trim().split(/\s+/).filter(Boolean).length;
}

Значно краще. /\s+/ матчить будь-яку послідовність пробільних символів — пробіли, табуляції, переноси рядка, повернення каретки. trim() опрацьовує пробіли на початку й у кінці. filter(Boolean) відкидає порожні рядки.

countWords('hello  world')     // → 2 ✓
countWords('hello\tworld')     // → 2 ✓
countWords('')                 // → 0 ✓
countWords('héllo wörld')      // → 2 ✓ (accent characters preserved)

Де ламається: CJK-текст. '你好世界'.trim().split(/\s+/) повертає ['你好世界'] — один токен, а не чотири слова. Також рахує токени лише з пунктуації: якщо на вході -- --, отримаєш 2 фантомні «слова».

Вердикт: годиться для суто англійських інструментів. Зламано для глобальної аудиторії.


Метод 3: (text.match(/\b\w+\b/g) || []).length — класична регулярка

function countWords(text) {
  return (text.match(/\b\w+\b/g) || []).length;
}

Ти бачитимеш це всюди на Stack Overflow. Проблема в \w.

У JavaScript \w — це [A-Za-z0-9_]. Оце весь набір. Кожен символ поза ASCII — кирилиця (привет), арабська (مرحبا), грецька (γεια), корейська (안녕) — невидимий для цієї регулярки. Запасний || [] тебе видає: без нього .match() повертає null за відсутності збігів, а це був би весь рядок для нелатинського тексту.

countWords('hello world')   // → 2 ✓
countWords('привет мир')    // → 0 ✗ (Cyrillic not matched)
countWords('héllo')         // → 1, but counts 'h llo' internally → actually still 1 but accented chars may be excluded

Вердикт: прийнятно для дев-інструментів, що обробляють лише ASCII. Тихий провал для всього іншого.


Метод 4: (text.match(/\p{L}+/gu) || []).length — Unicode Property Escapes

function countWords(text) {
  return (text.match(/\p{L}+/gu) || []).length;
}

\p{L} — це Unicode Property Escape, що означає «будь-яка літера Unicode». Прапор u обов'язковий — без нього V8 кидає SyntaxError, бо \p невалідний у не-Unicode режимі. Прапор g знаходить усі збіги глобально.

countWords('hello world')      // → 2 ✓
countWords('привет мир')       // → 2 ✓ (Cyrillic works)
countWords('héllo wörld')      // → 2 ✓ (accented chars work)
countWords('你好 世界')         // → 2 ✓ (space-separated CJK)
countWords('你好世界')          // → 1 ✗ (no spaces, counts as one match)

Числа й окрема пунктуація виключаються автоматично, що зазвичай і потрібно.

Вердикт: чудово для латиниці, кирилиці, арабської, грецької, єврейської та CJK із пробілами. Усе ще не вміє сегментувати CJK без пробілів.


Метод 5: Intl.Segmenter — правильна відповідь

function countWords(text) {
  const segmenter = new Intl.Segmenter('und', { granularity: 'word' });
  let count = 0;
  for (const { isWordLike } of segmenter.segment(text)) {
    if (isWordLike) count++;
  }
  return count;
}

Intl.Segmenter — це API інтернаціоналізації W3C, доступний у всіх сучасних JavaScript-рантаймах (Baseline 2023). Передавай 'und' як локаль для незалежної від локалі сегментації або свою конкретну локаль ('zh', 'ja') для правил із урахуванням мови.

Прапорець isWordLike — ось ключ: він true для справжніх слів і false для пробілів, пунктуації й роздільників. Жодної фільтрації не треба.

countWords('hello world')      // → 2 ✓
countWords('привет мир')       // → 2 ✓
countWords('你好世界')          // → 2 ✓ (你好 = hello, 世界 = world — dictionary segmentation)
countWords("don't stop")       // → 2 ✓ (contraction = 1 word)
countWords('state-of-the-art') // → 4 ✓ (hyphenated = 4 words, matches editorial convention)
countWords('')                 // → 0 ✓

Вердикт: використовуй це. Саме це браузери застосовують усередині для перевірки орфографії й виділення тексту.

Примітка щодо Node.js: на Node.js 16+ зі стандартним складанням full-icu Intl.Segmenter працює з коробки. Якщо ти на старішій версії чи на складанні small-icu (поширене в деяких Docker-образах), можеш отримати TypeError: Intl.Segmenter is not a constructor. Полагодь це, встановивши пакет full-icu і передавши --icu-data-dir під час старту — або просто оновись до Node 18+, де повні дані ICU вшито за замовчуванням.


Таблиця порівняння точності

МетодАнглійськаДіакритикаКирилиця/АрабськаCJK (без пробілів)Порожній рядокСкорочення
split(' ')✗ (подвійні пробіли)✗ (повертає 1)
trim().split(/\s+/)
/\b\w+\b/g
/\p{L}+/gu
Intl.Segmenter

Рядок Intl.Segmenter — єдиний, де всі галочки.

Те саме речення, відрендерене латиницею, кирилицею, арабською, CJK та емодзі — зелена галочка над Intl.Segmenter, червоний хрестик над \w-регуляркою


Міркування щодо продуктивності

Для документа на 1000 слів усі п'ять методів мізерні — менш ніж 1 мс на будь-якій сучасній машині. Різниця проявляється на масштабі.

На 100 000 слів (повний рукопис роману):

  • Регулярні методи (/\p{L}+/gu) виконуються за ~20–40 мс — досить швидко для підрахунку в реальному часі на подіях input
  • Intl.Segmenter виконується за ~80–120 мс — усе ще менше за 100 мс, але вже близько до порога для плавного UI на 60fps

Емпіричне правило: для вхідних даних понад 50 000 слів запускай лічильник у Web Worker. Передай текст через postMessage, запусти сегментатор у контексті воркера й відправ результат назад. Головний потік лишається незаблокованим.

// word-count.worker.js
self.onmessage = ({ data: text }) => {
  const segmenter = new Intl.Segmenter('und', { granularity: 'word' });
  let count = 0;
  for (const { isWordLike } of segmenter.segment(text)) {
    if (isWordLike) count++;
  }
  self.postMessage(count);
};

Якщо хочеш звірити свою реалізацію з еталонною, встав текст у наш Лічильник слів — працює на 100% у твоєму браузері, нуль даних на жоден сервер — і порівняй лічбу зі своєї функції з тим, що показує він.


Коли який метод використовувати

СценарійРекомендований метод
Швидкий скрипт лише для англійськоїtrim().split(/\s+/)
Продакшен-застосунок, багатомовний/\p{L}+/gu
Продакшен-застосунок + підтримка CJKIntl.Segmenter
Node.js CLI, будь-яка моваIntl.Segmenter (Node ≥16)
Легасі-браузери (IE, старий Safari)trim().split(/\s+/) + примітка про поліфіл

Крайові випадки з реального світу для тестів

Перш ніж відправляти лічильник слів у прод, прожени його на цих входах. Якщо бодай якийсь дає неочікуваний результат — у твоєму методі є баг:

// 1. Multiple whitespace types
"hello\t\nworld"         // expect: 2

// 2. Non-breaking space (pasted from Word)
"hello world"       // expect: 2

// 3. Zero-width space (pasted from web)
"hello​world"       // expect: 1 or 2 (debatable, document your choice)

// 4. Pure punctuation
"... --- ???"            // expect: 0

// 5. Numbers only
"123 456"                // expect: 0 (if counting "words" = letters only)
                         // expect: 2 (if counting tokens)

// 6. Mixed script
"hello мир"              // expect: 2

// 7. Emoji in text
"Great job 🎉"           // expect: 2 (emoji is not a word)

// 8. Hyphenated compound
"state-of-the-art design" // Intl.Segmenter → 5; /\p{L}+/gu → 5; split → 2

Випадок із дефісом — той, що збиває людей найбільше. Універсальної «правильної» відповіді немає — англійські стайлгайди не згодні між собою. Обери поведінку, задокументуй її й будь послідовним.


Що використовує Лічильник слів на цьому сайті

Лічильник слів на editlyapp.com використовує Intl.Segmenter із /\p{L}+/gu як запасний варіант для середовищ, де API ще недоступний. Сегментатор працює на головному потоці для документів до 50 000 слів і перемикається на Web Worker для більших входів — утримуючи відгук UI під 16 мс незалежно від довжини рукопису.

Це той самий підхід, що описаний у статті Оцінка читабельності простими словами, де сегментація речень використовує Intl.Segmenter із granularity: 'sentence', щоб точно живити формулу Flesch-Kincaid.

Якщо ти будуєш текстовий інструмент на регулярках і треба протестувати патерни на реальному вмісті, інструмент Пошук і заміна на цьому сайті підтримує повні регулярки з прапором u — зручно для валідації твоїх патернів /\p{L}+/gu, перш ніж заводити їх у застосунок.


Остаточна реалізація

Ось готова до продакшену версія, що опрацьовує кожен випадок вище:

/**
 * Count words in any language using Intl.Segmenter.
 * Falls back to Unicode regex for environments without Segmenter support.
 */
function countWords(text) {
  if (!text || !text.trim()) return 0;

  if (typeof Intl !== 'undefined' && Intl.Segmenter) {
    const segmenter = new Intl.Segmenter('und', { granularity: 'word' });
    let count = 0;
    for (const { isWordLike } of segmenter.segment(text)) {
      if (isWordLike) count++;
    }
    return count;
  }

  // Fallback: Unicode Property Escapes (all modern browsers, no IE)
  return (text.match(/\p{L}+/gu) || []).length;
}

Зверни увагу на дві речі. По-перше, ранній return на порожньому вході чи лише з пробілів — Intl.Segmenter на порожньому рядку повертає нуль сегментів, але гард тут явний. По-друге, перевірка наявності фічі Intl.Segmenter замість try/catch — чистіше й дешевше.


FAQ

Який найточніший спосіб рахувати слова в JavaScript?

Intl.Segmenter із granularity: 'word' — найточніший. Це стандартний API W3C, вбудований у V8, який коректно опрацьовує CJK, арабську, тайську (без меж за пробілами), кластери емодзі, слова через дефіс і скорочення. Для більшості суто англійських випадків /\p{L}+/gu із прапором u — надійна, простіша альтернатива.

Чому text.split(' ').length повертає неправильну лічбу?

Три причини. По-перше, він рахує порожні рядки, коли в тексті є кілька пробілів поспіль — 'hello world'.split(' ') повертає ['hello', '', 'world'], довжина 3, а не 2. По-друге, він не бачить табуляцій (\t), переносів рядка (\n) і нерозривних пробілів (U+00A0). По-третє, він рахує пробіли на початку/в кінці як фантомні слова, поки ти не зробиш trim().

Чи працює /\b\w+\b/g для неанглійського тексту?

Ні. \w — це [A-Za-z0-9_]. Кожен кириличний, арабський, грецький, єврейський, корейський, китайський і японський символ дає нуль збігів. Якщо ти будуєш бодай щось для неамериканської аудиторії, ця регулярка мовчки видає неправильну лічбу. Натомість використовуй /\p{L}+/gu із прапором u.

Що таке Intl.Segmenter і чи безпечно його використовувати в проді?

Intl.Segmenter — це API інтернаціоналізації W3C, вбудований у V8 (Chrome/Node.js), SpiderMonkey (Firefox) і JavaScriptCore (Safari). Він досяг статусу Baseline 2023 — усі три головні браузерні рушії його підтримують. Для Node.js доступний починаючи з v16.0.0. Можеш використовувати без поліфілу в будь-якому сучасному середовищі.

Як рахувати слова в Nuxt чи React застосунку з великими текстами?

Для текстів до ~50 000 слів будь-який метод працює досить швидко на головному потоці. Для більших входів — рукописів, вставлених книжок, масової обробки — винеси у Web Worker, щоб не фризити UI. Передавай сирий рядок через postMessage і запускай сегментатор у контексті воркера.

Скорочення рахуються як одне слово чи два?

В англійській прозі скорочення (don't, it's, you're) мають рахуватися як одне слово — це збігається з тим, як рахують редактори, вчителі й видавці. Intl.Segmenter із granularity: 'word' коректно повертає don't як один сегмент-слово.

Чому мій лічильник не збігається з Google Docs чи Microsoft Word?

Google Docs і Microsoft Word використовують пропрієтарні алгоритми токенізації, що публічно не задокументовані. Google Docs зазвичай виключає виноски й рахує складні слова через дефіс як одне слово. Word за замовчуванням враховує виноски й може ділити слова через дефіс інакше залежно від встановленого мовного пакета. Intl.Segmenter із isWordLike дає найближче наближення до галузевого редакторського підрахунку — і, на відміну від обох платформ, його поведінка повністю передбачувана, бо специфікація W3C публічна.

Як рахувати слова, не рахуючи числа чи токени лише з пунктуації?

З Intl.Segmenter перевіряй segment.isWordLike === true — API автоматично позначає пунктуацію й пробіли як несловесні сегменти. З /\p{L}+/gu за визначенням матчаться лише послідовності літер, тож числа й окрема пунктуація виключаються.


Глибший погляд на те, як патерни регулярок поводяться на реальному тексті, — у Гайді з регулярних виразів: пошук і заміна, що покриває групи захоплення, квантифікатори й lookahead — усе, що треба, щоб будувати надійні патерни обробки тексту поза підрахунком слів.

Часті запитання

Який найточніший спосіб рахувати слова в JavaScript?

Intl.Segmenter з granularity: 'word' — найточніший метод. Це стандартний API W3C, вбудований у V8, який коректно опрацьовує CJK, арабську, тайську (де немає меж за пробілами), а також кластери емодзі, слова через дефіс і скорочення. Для більшості суто англійських сценаріїв /\p{L}+/gu з прапором u — надійна й простіша альтернатива.

Чому text.split(' ').length повертає неправильну лічбу?

Три причини. По-перше, він рахує порожні рядки, коли в тексті є кілька пробілів поспіль — 'hello world'.split(' ') повертає ['hello', '', 'world'], довжина 3, а не 2. По-друге, він не бачить табуляції (\t), переносів рядка (\n) і нерозривних пробілів (U+00A0) як роздільників слів. По-третє, він залишає пунктуацію, причеплену до слів, частиною токена-слова, що спотворює метрики унікальних слів.

Чи працює /\b\w+\b/g для неанглійського тексту?

Ні. У JavaScript \w — це скорочення для [A-Za-z0-9_]; воно матчить лише ASCII-літери, цифри й підкреслення. Кожен кириличний, арабський, грецький, єврейський, корейський, китайський і японський символ дає нуль збігів. Якщо ти будуєш бодай щось для неамериканської аудиторії, ця регулярка мовчки видає неправильну лічбу. Натомість використовуй /\p{L}+/gu з прапором u.

Що таке Intl.Segmenter і чи безпечно його використовувати в проді?

Intl.Segmenter — це API інтернаціоналізації W3C, вбудований у V8 (Chrome/Node.js), SpiderMonkey (Firefox) і JavaScriptCore (Safari). Він досяг статусу Baseline 2023 — тобто його підтримують усі три головні браузерні рушії. Для Node.js він доступний починаючи з v16.0.0. Можеш використовувати його без поліфілу в будь-якому сучасному середовищі.

Як рахувати слова в Nuxt чи React застосунку з великими текстами?

Для текстів до ~50 000 слів будь-який із методів працює досить швидко на головному потоці. Для більших вхідних даних — рукописів, вставлених книжок, масової обробки — винеси це у Web Worker, щоб не фризити UI. API Intl.Segmenter не можна передати у воркер напряму, тож передавай сирий рядок через postMessage і запускай сегментатор у контексті воркера.

Скорочення рахуються як одне слово чи два?

В англійській прозі скорочення (don't, it's, you're) мають рахуватися як одне слово — це збігається з тим, як рахують редактори, вчителі й видавці. Методи 1–3 (на основі split і \w-регулярки) роблять це правильно випадково, бо апостроф розділяє їх на два токени лише якщо трактується як пробіл, а він ним не є. Intl.Segmenter з granularity: 'word' коректно повертає don't як один сегмент-слово.

Як рахувати слова, не рахуючи числа чи токени лише з пунктуації?

Відфільтруй результати свого сегментатора чи регулярки так, щоб лишилися тільки сегменти з принаймні однією літерою Unicode. З Intl.Segmenter перевіряй segment.isWordLike === true — API автоматично позначає пунктуацію й пробіли як несловесні сегменти. З /\p{L}+/gu за визначенням матчаться лише послідовності літер, тож числа й окрема пунктуація виключаються.

Спробуйте наш безкоштовний лічильник слів

Миттєво рахуйте слова, перевіряйте читабельність та аналізуйте текст.

Відкрити лічильник слів