So far you have learned the pieces: functions to organise logic, arrays to hold lists, objects to map keys to values, and loops to walk through data. Now it is time to assemble them into one small, real tool: a word tally.
Given a sentence, the tool tells you how often each word appears and which word shows up most. This same pattern powers search-engine indexing, reading-level analysis, and spam detection — all built on the simple idea of counting what you have seen.
The plan has three milestones:
- Split the text into lowercase words.
- Count how many times each word appears (an object is perfect for this).
- Find the most frequent word.
Each milestone is a function. The final tool is just those functions snapped together.
flowchart LR T["input text"] --> S["split into words"] S --> C["count into object"] C --> M["find most frequent"] style S fill:#e0900b,color:#fff style C fill:#e0900b,color:#fff style M fill:#e0900b,color:#fff
Milestone 1 — split into words
Raw text is messy. It has capitals, commas, exclamation marks, and extra spaces. Before you can count anything you must normalise it, or The and the will be treated as two different words and your tally will be wrong.
The recipe is three steps:
- Lowercase everything so case differences disappear.
- Split on anything that is not a letter. The regular expression
/[^a-z]+/matches runs of spaces and punctuation, sosplitcuts there. - Filter out any empty strings left at the edges.
function splitWords(text) {
return text.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 0);
}
The filter step matters. split on punctuation can leave an empty string at the start or end of the array, and an empty string is not a word. Dropping zero-length pieces keeps your data clean and prevents a ghost word from appearing in the final tally.
function splitWords(text) {
return text.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 0);
}
console.log(splitWords("Hello, world!"));
console.log(splitWords("One... two; three"));
console.log(splitWords(" spaces everywhere "));
Milestone 1. Write splitWords(text) that lowercases the text and returns an array of the words (no punctuation, no empty strings).
function splitWords(text) {
// lowercase, then split on non-letters, then drop empties
}
Use text.toLowerCase() first.
split(/[^a-z]+/) cuts on any non-letter run; filter out empty strings.
function splitWords(text) {
return text.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 0);
}
Milestone 2 — count the words
An object whose keys are words and values are counts is exactly the right shape for a frequency table. Walk the word array; for each word, bump its count by one.
The trick is the expression counts[word] = (counts[word] || 0) + 1. When a word appears for the first time, counts[word] is undefined. In JavaScript, undefined || 0 evaluates to 0, so the word starts at zero and is immediately incremented to one. On every later visit the word already has a real number, so || 0 is ignored and the count simply rises.
Without that guard, you would try to add one to undefined, which produces NaN. Once a tally becomes NaN, every subsequent addition stays NaN, and your entire frequency table is ruined. The one-liner || 0 is a safety net that turns the first visit into a valid starting count.
flowchart TD
F["counts['the']"] --> C{"defined?"}
C -->|No| Z["0"]
C -->|Yes| V["real count"]
Z --> A["+1 = 1"]
V --> B["+1 = count+1"]
style Z fill:#1e7f3d,color:#fff
style V fill:#e0900b,color:#fff
function countWords(words) {
const counts = {};
for (const word of words) {
counts[word] = (counts[word] || 0) + 1;
}
return counts;
}
console.log(countWords(["the", "cat", "the", "dog"]));
console.log(countWords(["a", "b", "a"]));
console.log(countWords([]));
Milestone 2. Write countWords(words) that takes an array of words and returns an object mapping each word to how many times it appears.
function countWords(words) {
// return { word: count, ... }
}
Start with an empty object: const counts = {}.
For each word, set counts[word] = (counts[word] || 0) + 1.
function countWords(words) {
const counts = {};
for (const word of words) {
counts[word] = (counts[word] || 0) + 1;
}
return counts;
}
Milestone 3 — find the top word
With the tally built, the last step is a linear search through the object's keys. Keep track of the best word seen so far and its count. Whenever you find a word with a strictly higher count, replace the leader.
Because JavaScript preserves insertion order for string keys, the first word to reach the maximum count wins ties. That is a useful property: if two words are equally common, the one that appeared first in the text is reported as the top word.
Notice that the helper is short because it reuses the two functions you already wrote. That is the power of decomposition: each piece is simple, and the final answer is just the pieces composed together.
Milestone 3. Write topWord(text) that returns the most frequent word. (Include splitWords and countWords from the earlier milestones so your solution is self-contained.)
function splitWords(text) {
}
function countWords(words) {
}
function topWord(text) {
// return the most frequent word
}
Call countWords(splitWords(text)) to build the tally.
Loop Object.keys(counts); keep the word with the highest count so far.
function splitWords(text) {
return text.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 0);
}
function countWords(words) {
const counts = {};
for (const word of words) {
counts[word] = (counts[word] || 0) + 1;
}
return counts;
}
function topWord(text) {
const counts = countWords(splitWords(text));
let best = null;
let bestCount = 0;
for (const word of Object.keys(counts)) {
if (counts[word] > bestCount) {
best = word;
bestCount = counts[word];
}
}
return best;
}
function splitWords(text) {
return text.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 0);
}
function countWords(words) {
const counts = {};
for (const word of words) {
counts[word] = (counts[word] || 0) + 1;
}
return counts;
}
function topWord(text) {
const counts = countWords(splitWords(text));
let best = null;
let bestCount = 0;
for (const word of Object.keys(counts)) {
if (counts[word] > bestCount) {
best = word;
bestCount = counts[word];
}
}
return best;
}
const sample = "the quick brown fox the lazy dog the";
console.log(countWords(splitWords(sample)));
console.log("Top word:", topWord(sample));
Predict it. Run the finished tool on a sentence. What does it print?
function splitWords(text) {
return text.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 0);
}
function countWords(words) {
const counts = {};
for (const word of words) {
counts[word] = (counts[word] || 0) + 1;
}
return counts;
}
function topWord(text) {
const counts = countWords(splitWords(text));
let best = null, bestCount = 0;
for (const word of Object.keys(counts)) {
if (counts[word] > bestCount) { best = word; bestCount = counts[word]; }
}
return best;
}
console.log(countWords(splitWords("to be or not to be")));
console.log(topWord("to be or not to be"));
"to" and "be" each appear twice; "or" and "not" once.
Top word is the first to reach the max — "to".
Transfer task. Write rankWords(text) that returns an array of [word, count] pairs sorted from most frequent to least frequent. Include splitWords and countWords so the solution is self-contained. Use Object.entries(counts) to get the pairs, and sort with b[1] - a[1] to order by count descending.
function splitWords(text) {
}
function countWords(words) {
}
function rankWords(text) {
// return [[word, count], ...] sorted by count descending
}
Object.entries(counts) gives [[word, count], ...].
sort((a, b) => b[1] - a[1]) orders by the count (index 1) descending.
function splitWords(text) {
return text.toLowerCase().split(/[^a-z]+/).filter((w) => w.length > 0);
}
function countWords(words) {
const counts = {};
for (const word of words) {
counts[word] = (counts[word] || 0) + 1;
}
return counts;
}
function rankWords(text) {
const counts = countWords(splitWords(text));
return Object.entries(counts).sort((a, b) => b[1] - a[1]);
}
flowchart LR C["counts object"] --> E["Object.entries"] E --> A["[[word,count],...]"] A --> S["sort by count"] S --> R["ranked report"] style E fill:#e0900b,color:#fff style S fill:#e0900b,color:#fff
Recap
- Normalise text first: lowercase, split on non-letters, and filter empties.
- An object is the natural shape for a frequency tally: keys are words, values are counts.
counts[word] = (counts[word] || 0) + 1safely starts every word at zero.- Finding the top word is a linear scan through
Object.keys. Object.entriesplussortturns a tally into a ranked report.
You now have a complete, reusable tool. The same pipeline — clean, count, analyse — appears in every field that processes text, from journalism to machine learning.