Strings
Strings are immutable in JavaScript, which changes how you build them and why the naive concat-in-a-loop is a trap interviewers watch for.
By the end: Manipulate strings without accidentally writing O(n squared) code.
A string is a sequence of characters, and in JavaScript it is immutable: you can never change one. Every operation that looks like it edits a string actually builds a new one.
let s = 'hello';
// In strict mode, which is every module and every panel here, this throws.
// In an old sloppy-mode script it was ignored in silence, which was worse.
try {
s[0] = 'H';
} catch (err) {
console.log(err.name + ':', err.message);
}
console.log('s is unchanged:', s);
s = 'H' + s.slice(1); // a brand new string
console.log('after rebuilding:', s);
console.log('length:', s.length, '| char at 1:', s[1], '| charAt:', s.charAt(1));
Output
function buildBad(n) {
let out = '';
let charsCopied = 0;
for (let i = 0; i < n; i++) {
charsCopied += out.length; // the whole string is copied each time
out += 'x';
}
return { length: out.length, charsCopied };
}
function buildGood(n) {
const parts = [];
for (let i = 0; i < n; i++) parts.push('x');
return { length: parts.join('').length, charsCopied: n };
}
for (const n of [100, 400, 800]) {
console.log('n =', String(n).padStart(3),
'| concat copies:', String(buildBad(n).charsCopied).padStart(7),
'| join copies:', buildGood(n).charsCopied);
}
Output
The operations you will actually use
const text = 'the quick brown fox';
console.log('slice(4, 9): ', text.slice(4, 9));
console.log('slice(-3): ', text.slice(-3));
console.log('includes: ', text.includes('brown'));
console.log('indexOf: ', text.indexOf('brown'));
console.log('startsWith: ', text.startsWith('the'));
console.log('split(" "): ', text.split(' '));
console.log('join back: ', text.split(' ').join('-'));
console.log('toUpperCase: ', text.toUpperCase());
console.log('trim: ', ' padded '.trim() + '|');
console.log('repeat: ', 'ab'.repeat(3));
console.log('padStart: ', '7'.padStart(3, '0'));
console.log('replaceAll: ', text.replaceAll('o', '0'));
Output
Characters and codes
Interview problems frequently need "is this a letter", "what is the 0-based position of this letter in the alphabet", or a 26-slot counting array. All of that runs through character codes.
console.log("'a' code:", 'a'.charCodeAt(0), "| 'z' code:", 'z'.charCodeAt(0));
console.log("'A' code:", 'A'.charCodeAt(0), "| '0' code:", '0'.charCodeAt(0));
// The alphabet index trick: 0 for 'a', 25 for 'z'.
const idx = (ch) => ch.charCodeAt(0) - 97;
console.log('index of c:', idx('c'), '| index of z:', idx('z'));
// And back again.
console.log('code 97 is:', String.fromCharCode(97), '| 65 is:', String.fromCharCode(65));
// A 26-slot counter, the standard tool for anagram questions.
function letterCounts(word) {
const counts = new Array(26).fill(0);
for (const ch of word) counts[idx(ch)]++;
return counts;
}
console.log('counts for "banana":', letterCounts('banana').filter(c => c > 0), '(a=3, b=1, n=2)');
Output
const isDigit = (ch) => ch >= '0' && ch <= '9';
const isLower = (ch) => ch >= 'a' && ch <= 'z';
const isUpper = (ch) => ch >= 'A' && ch <= 'Z';
const isLetter = (ch) => isLower(ch) || isUpper(ch);
const isAlnum = (ch) => isLetter(ch) || isDigit(ch);
for (const ch of ['7', 'k', 'K', '!', ' ']) {
console.log(JSON.stringify(ch), '→ digit:', isDigit(ch), 'letter:', isLetter(ch), 'alnum:', isAlnum(ch));
}
Output
Two patterns that cover most string questions
Normalise, then compare. Most "is this a palindrome" or "are these anagrams" questions are really about cleaning the input first.
// The readable way: normalise, reverse, compare. O(n) time, O(n) space.
function isPalindromeSimple(s) {
const clean = s.toLowerCase().replace(/[^a-z0-9]/g, '');
return clean === [...clean].reverse().join('');
}
// The interview way: two pointers, no extra string. O(n) time, O(1) space.
function isPalindromeTwoPointer(s) {
const ok = (ch) => /[a-z0-9]/.test(ch);
let i = 0, j = s.length - 1;
const low = s.toLowerCase();
while (i < j) {
while (i < j && !ok(low[i])) i++;
while (i < j && !ok(low[j])) j--;
if (low[i] !== low[j]) return false;
i++; j--;
}
return true;
}
for (const s of ['A man, a plan, a canal: Panama', 'race a car', '', '.,']) {
console.log(JSON.stringify(s.slice(0, 22)).padEnd(26),
'simple:', isPalindromeSimple(s), '| two-pointer:', isPalindromeTwoPointer(s));
}
Output
Use a canonical form as a key. If two strings should be treated as equal under some rule, compute a form that is identical for both and use it as a map key.
function groupAnagrams(words) {
const groups = new Map();
for (const word of words) {
// Sorted letters is the canonical form: 'eat' and 'tea' both become 'aet'.
const key = [...word].sort().join('');
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(word);
}
return [...groups.values()];
}
console.log(groupAnagrams(['eat', 'tea', 'tan', 'ate', 'nat', 'bat']));
// A counting key avoids the sort: O(n * k) instead of O(n * k log k).
function groupAnagramsCounting(words) {
const groups = new Map();
for (const word of words) {
const counts = new Array(26).fill(0);
for (const ch of word) counts[ch.charCodeAt(0) - 97]++;
const key = counts.join(',');
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(word);
}
return [...groups.values()];
}
console.log(groupAnagramsCounting(['eat', 'tea', 'tan', 'ate', 'nat', 'bat']));
Output
Unicode, briefly
Almost every interview problem is plain ASCII, so you can ignore this. But you should know it exists, because being able to name the issue is a small credibility win.
const emoji = 'a\u{1F600}b'; // a, a grinning face, b
console.log('length says:', emoji.length, 'but there are 3 characters');
console.log('emoji[1] is half a character:', JSON.stringify(emoji[1]));
console.log('spread splits correctly:', [...emoji], '→', [...emoji].length);
console.log('so reverse via spread is safe:', [...emoji].reverse().join(''));
console.log('codePointAt:', emoji.codePointAt(1).toString(16));
Output
Your turn: longest common prefix
Write longestCommonPrefix(words) returning the longest string that starts every word in the array. ['flower','flow','flight'] gives 'fl'; ['dog','racecar','car'] gives ''. Handle an empty array.
Aim for O(total characters) time and O(1) extra space beyond the answer. The simplest correct approach walks character position 0, 1, 2... and stops as soon as any word disagrees or runs out.
Output
Recap
- Strings are immutable: every apparent edit builds a new string.
- Never build a string with
+=in a loop; push pieces into an array andjoin('')once. charCodeAt(0) - 97gives the 0-based alphabet index, which powers 26-slot counting arrays.- Comparison is by character code, so
'Z' < 'a'and'10' < '9'are both true. - Two patterns solve most string questions: normalise then compare, and use a canonical form as a map key.