JavaScript String Interview Questions
Short answer
Strings in JavaScript are immutable primitives: every method returns a new string and the original never changes. Interviews test that fact, the difference between slice and substring, and three coding problems — reverse, palindrome and anagram — where the Unicode edge cases separate good answers from memorised ones.
| slice(a, b) | substring(a, b) | substr(a, len) | |
|---|---|---|---|
| Negative indices | Counts from the end | Treated as 0 | Start counts from the end |
| a > b | Returns empty string | Swaps the arguments | N/A |
| Second argument means | End index | End index | Length |
| Status | Preferred | Fine, quirky | Deprecated |
Are strings mutable in JavaScript?
No. Strings are primitives and immutable — every method that looks like it changes a string actually returns a new one, and the original is untouched. Assigning to an index silently does nothing in sloppy mode and throws in strict mode, which surprises people coming from languages with mutable character arrays.
1let s = "hello";
2
3s.toUpperCase();
4console.log(s);
5// → hello the result was thrown away
6
7s = s.toUpperCase();
8console.log(s);
9// → HELLO reassignment, not mutation
10
11const t = "hello";
12t[0] = "H";
13console.log(t);
14// → hello silently ignored (TypeError in strict mode)
15
16console.log(t.replace("h", "H"), t);
17// → Hello hello a new string, and the originalThe practical consequence is that building a string in a loop with `+=` creates a new string on every iteration. Modern engines optimise this heavily so it rarely matters in practice, but for very large concatenations pushing into an array and calling `join("")` once is the answer interviewers are looking for.
What are template literals and tagged templates?
Template literals are backtick strings with interpolation and real multi-line support, which removes the escaped-newline concatenation that used to litter JavaScript. The part fewer candidates know is tagged templates: put a function name before the backticks and it receives the literal string pieces and the interpolated values separately, letting it process them.
1const name = "Arun", count = 3;
2console.log(`Hi ${name}, you have ${count} item${count === 1 ? "" : "s"}`);
3// → Hi Arun, you have 3 items
4
5// Tagged: the function gets the pieces and the values apart
6function safe(strings, ...values) {
7 return strings.reduce((out, str, i) =>
8 out + str + (values[i] ? String(values[i]).replace(/</g, "<") : ""), "");
9}
10
11const evil = "<script>";
12console.log(safe`User said: ${evil}`);
13// → User said: <script>
14
15console.log(String.raw`C:\new\table`);
16// → C:\new\table escapes left aloneTagged templates are worth a sentence even if you never write one, because they explain a library people use daily. When you write a styled-component, the CSS you type between backticks arrives at a function as an array of static chunks plus the interpolated values, which is how it can hash the result and inject a class. Being able to say that turns a syntax question into evidence you understand the ecosystem.
What is the difference between slice, substring and substr?
All three extract part of a string and none of them mutate it. `slice` is the one to use: it accepts negative indices that count from the end, and returns an empty string if the start is after the end. `substring` treats negative numbers as zero and silently swaps its arguments if they are the wrong way round — behaviour that hides bugs rather than surfacing them. `substr` takes a length instead of an end index and is deprecated.
1const s = "JavaScript";
2
3console.log(s.slice(4)); // → Script
4console.log(s.slice(-6)); // → Script negative counts from the end
5console.log(s.slice(4, 2)); // → empty, start is after end
6
7console.log(s.substring(4, 2)); // → va arguments SWAPPED silently
8console.log(s.substring(-6)); // → JavaScript negative becomes 0
9
10console.log(s.at(-1)); // → t cleanest way to get the last char
11console.log(s.charAt(0), s[0]); // → J JThere is one more member of this family worth knowing: `split` with a limit, and `at` for indexing from the end. `at(-1)` reads far better than `s[s.length - 1]` and works identically on arrays, which is the kind of small consistency that makes code easier to scan. None of these methods mutate, so they compose freely — chaining slice into replace into trim never surprises you.
How do you reverse a string?
The textbook answer is split, reverse, join. It is what the interviewer expects, and you should give it — but the follow-up worth having ready is that it is wrong for anything outside the Basic Multilingual Plane. `split("")` splits into UTF-16 code units, so an emoji or any character built from a surrogate pair gets torn in half and the output is mojibake.
1const reverse = (s) => s.split("").reverse().join("");
2
3console.log(reverse("interview"));
4// → weivretni correct for plain ASCII
5
6console.log(reverse("héllo 👋"));
7// → �� olleh the emoji's surrogate pair was reversed
8
9// Spread and Array.from iterate by CODE POINT, not code unit
10const reverseSafe = (s) => [...s].reverse().join("");
11console.log(reverseSafe("héllo 👋"));
12// → 👋 olléh correct
13
14// Fully correct for combining marks and ZWJ sequences needs
15// grapheme segmentation:
16const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
17const reverseGraphemes = (s) =>
18 [...seg.segment(s)].map(x => x.segment).reverse().join("");Whether you should volunteer the Unicode caveat depends on the room. In a fast screening round, give the one-liner and move on. If the interviewer asks whether it always works, or the role involves user-generated content, that is the moment — a reverse that mangles emoji is a real production bug in any product with a comments field, and the fix is a single character change from `split("")` to a spread.
How do you check for a palindrome?
Normalise first, then compare. The naive one-liner reversing the whole string is fine and readable; the two-pointer version avoids allocating a second string and is what to reach for if the interviewer asks about memory. Say which one you are writing and why.
1const clean = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2
3// Readable: O(n) time, O(n) extra space
4const isPalindrome = (s) => {
5 const c = clean(s);
6 return c === [...c].reverse().join("");
7};
8
9console.log(isPalindrome("A man, a plan, a canal: Panama"));
10// → true
11console.log(isPalindrome("race a car"));
12// → false
13
14// Two pointers: O(n) time, O(1) extra space
15function isPalindrome2(s) {
16 const c = clean(s);
17 for (let i = 0, j = c.length - 1; i < j; i++, j--) {
18 if (c[i] !== c[j]) return false;
19 }
20 return true;
21}
22console.log(isPalindrome2("Was it a car or a cat I saw?"));
23// → trueThe normalisation step is where most palindrome answers fail rather than the reversal. Interviewers usually feed a sentence with punctuation and mixed case specifically to see whether you thought about it. Say what you are normalising and why — stripping non-alphanumerics and lowercasing — because a candidate who states the assumption is treated very differently from one who happens to handle it.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299How do you check if two strings are anagrams?
Sorting both and comparing is the obvious answer and runs in O(n log n). The frequency-map version is O(n) and is what to offer as the optimisation — count characters up for the first string, down for the second, and check nothing is left over.
1const norm = (s) => s.toLowerCase().replace(/\s/g, "");
2
3const isAnagramSort = (a, b) =>
4 [...norm(a)].sort().join("") === [...norm(b)].sort().join("");
5
6console.log(isAnagramSort("listen", "silent"));
7// → true
8
9function isAnagram(a, b) {
10 a = norm(a); b = norm(b);
11 if (a.length !== b.length) return false; // cheap early exit
12
13 const counts = new Map();
14 for (const ch of a) counts.set(ch, (counts.get(ch) ?? 0) + 1);
15 for (const ch of b) {
16 const n = counts.get(ch);
17 if (!n) return false;
18 counts.set(ch, n - 1);
19 }
20 return true;
21}
22
23console.log(isAnagram("Dormitory", "Dirty Room"));
24// → true
25console.log(isAnagram("hello", "world"));
26// → falseA useful extension to have ready is grouping anagrams: given a list of words, bucket them by their sorted letters. It reuses the same normalise-and-key idea, and it is a common follow-up because it moves the problem from comparing two strings to indexing many. Both versions above also assume the two inputs are the same alphabet — mentioning that you would decide how to treat spaces and case with the interviewer is a cheap way to show you gather requirements.
Which string methods should you actually know?
Interviewers rarely quiz the whole API, but they do expect fluency with the handful that appear in real code. Knowing which ones mutate is a trick question — none of them do.
- →includes, startsWith, endsWith — readable membership checks that replaced indexOf !== -1.
- →replace and replaceAll — replace only swaps the first match unless you pass a global regex; replaceAll always swaps every one.
- →trim, trimStart, trimEnd — whitespace only, and a frequent source of failed form validation.
- →padStart and padEnd — formatting timestamps and ids without a helper.
- →split and join — the pair behind most string manipulation.
- →at — accepts negative indices, unlike charAt.
- →localeCompare — the correct way to sort strings, especially with non-English characters.
- →normalize — collapses different Unicode representations of the same visible text.
1console.log("a-b-c".replace("-", "+"));
2// → a+b-c only the FIRST match
3console.log("a-b-c".replaceAll("-", "+"));
4// → a+b+c
5console.log("a-b-c".replace(/-/g, "+"));
6// → a+b+c the pre-2021 way
7
8// Sorting without localeCompare gets non-English text wrong
9console.log(["ä", "z", "a"].sort());
10// → [ 'a', 'z', 'ä' ] code-unit order
11console.log(["ä", "z", "a"].sort((x, y) => x.localeCompare(y)));
12// → [ 'a', 'ä', 'z' ] what a human expectsOne habit worth carrying into any string round: prefer the method whose name states the intent. `includes` over `indexOf !== -1`, `startsWith` over a slice comparison, `at(-1)` over arithmetic on length. It costs nothing, it removes a class of off-by-one mistakes, and reviewers read it faster — which is exactly the argument you would make on a real team.
How do you count character occurrences or find the first unique character?
Both are frequency-map problems and they come up constantly as warm-ups. Build the map in one pass, then answer the question in a second pass. Doing it in two passes rather than one nested loop is the whole point — it takes the solution from O(n²) to O(n).
1function counts(s) {
2 const m = new Map();
3 for (const ch of s) m.set(ch, (m.get(ch) ?? 0) + 1);
4 return m;
5}
6
7console.log(counts("interview").get("i"));
8// → 2
9
10function firstUnique(s) {
11 const m = counts(s);
12 for (const ch of s) if (m.get(ch) === 1) return ch;
13 return null;
14}
15
16console.log(firstUnique("swiss"));
17// → w
18console.log(firstUnique("aabb"));
19// → null
20
21// Most frequent character
22const top = [...counts("mississippi")].sort((a, b) => b[1] - a[1])[0];
23console.log(top);
24// → [ 'i', 4 ]A `Map` is the right container here rather than a plain object. Object keys are always strings, so a numeric character would be coerced, and an inherited property name like `constructor` can collide with the prototype and give you a count that was never there. `Map` accepts any key type, preserves insertion order, and reports its size directly, which is why it is the default choice for frequency counting in modern code.
How do you capitalise words or convert case?
A small prompt that tests whether you reach for a regex or a split. Both are acceptable; the regex version handles multiple spaces and punctuation more gracefully, which is worth pointing out.
1const title = (s) => s.replace(/\b\w/g, c => c.toUpperCase());
2console.log(title("the quick brown fox"));
3// → The Quick Brown Fox
4
5const kebab = (s) =>
6 s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
7console.log(kebab("backgroundColorValue"));
8// → background-color-value
9
10const camel = (s) =>
11 s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
12console.log(camel("background-color-value"));
13// → backgroundColorValueCase conversion has a locale trap that is worth knowing about even though it rarely appears in interviews. `toUpperCase` on a Turkish dotless i produces the wrong letter unless you use `toLocaleUpperCase` with the right locale, and comparing case-insensitively by uppercasing both sides can therefore disagree with what a user expects. For anything user-facing, `localeCompare` with the `sensitivity` option is the safer tool.
The kebab and camel converters are also a neat demonstration of the replace callback. Passing a function as the second argument gives you the match and its capture groups, so you can transform rather than substitute a fixed string. That single feature covers most of the ad-hoc text munging people otherwise reach for a library to do.
How do strings compare, and what is string interning?
Strings compare by value with both `==` and `===`, so two separately built strings with the same characters are equal — unlike arrays and objects, which compare by reference. Relational operators compare code unit by code unit, which is why uppercase letters sort before lowercase ones and why `localeCompare` exists.
1console.log("abc" === "ab" + "c");
2// → true compared by value
3
4console.log("Z" < "a");
5// → true code unit 90 < 97
6
7console.log(new String("a") === "a");
8// → false object vs primitive
9console.log(typeof new String("a"));
10// → object never use the String constructor with new
11
12console.log("café".normalize("NFC") === "café".normalize("NFD"));
13// → false same visible text, different code pointsIs string concatenation with += slow?
Not usually. Modern engines use rope structures internally so repeated concatenation is far cheaper than the naive model suggests. For very large loops, pushing into an array and joining once is still measurably faster and is the answer to give if asked about optimisation.
What is the difference between a string primitive and a String object?
A primitive is a value; a String object is a wrapper created with new String(). Methods work on primitives because the engine temporarily boxes them, then discards the wrapper. Never use the constructor — the object is truthy even when empty and compares unequal to the equivalent primitive with ===.
How do you check if a string contains a substring?
includes returns a boolean and is the readable choice. indexOf returns the position or -1 and is only better when you need that position. For pattern matching rather than a literal, use a regex with test.
Why does '10' + 1 give '101' but '10' - 1 give 9?
The + operator is overloaded: if either operand is a string it concatenates, so the number is converted to a string. Every other arithmetic operator only works on numbers, so the string is converted to a number instead. This asymmetry is a very common output question.
How do you trim only specific characters?
trim removes whitespace only. For anything else use a regex — replace(/^-+|-+$/g, "") strips leading and trailing hyphens. This comes up when building slugs, where you also want to collapse repeated separators.
Frequently asked questions
- Are strings mutable in JavaScript?
- No. Strings are immutable primitives, so every method returns a new string and the original is unchanged. Assigning to an index does nothing in sloppy mode and throws in strict mode. To change a string you reassign the variable, which is a different thing from mutating the value.
- What is the difference between slice and substring?
- slice accepts negative indices that count from the end and returns an empty string when the start is after the end. substring treats negatives as 0 and silently swaps its arguments when they are reversed, which hides bugs. Prefer slice; substr takes a length instead of an end index and is deprecated.
- How do you reverse a string in JavaScript?
- s.split("").reverse().join("") is the expected answer, but it corrupts emoji and other characters outside the Basic Multilingual Plane because split("") splits UTF-16 code units. Use [...s].reverse().join(""), which iterates by code point, or Intl.Segmenter for full grapheme correctness.
- Why is '👋'.length equal to 2?
- length counts UTF-16 code units, not user-visible characters. Anything outside the Basic Multilingual Plane is stored as a surrogate pair of two units. Use [...s].length to count code points, or Intl.Segmenter with grapheme granularity to count what a reader would call characters.
- How do you check if a string is a palindrome?
- Normalise it first — lowercase and strip non-alphanumeric characters — then either compare it with its reverse, or walk two pointers inward from both ends. The two-pointer version uses O(1) extra space instead of O(n), which is the follow-up interviewers usually ask for.
- How do you check if two strings are anagrams?
- Normalise both, then either sort and compare, which is O(n log n), or build a frequency map counting up for one string and down for the other, which is O(n). Compare lengths first as a cheap early exit.
- What are template literals?
- Backtick strings supporting ${expression} interpolation and real line breaks. Tagged templates go further: a function placed before the backticks receives the literal pieces and the interpolated values separately, which is how styled-components and safe HTML escaping helpers work.
- What is the difference between replace and replaceAll?
- replace substitutes only the first match when given a string pattern; you need a global regex to replace every occurrence. replaceAll always replaces every occurrence and throws if given a non-global regex, which makes the intent explicit at the call site.
- How do you find the first non-repeating character in a string?
- Build a frequency map in one pass, then iterate the string again and return the first character whose count is 1. Two passes keep it O(n); a nested loop comparing every character against every other is O(n²) and is the answer to avoid.
- Why does '10' + 1 return '101'?
- The + operator concatenates when either operand is a string, so the number is converted to a string. Every other arithmetic operator coerces to numbers instead, which is why '10' - 1 gives 9. This asymmetry is one of the most common output questions in JavaScript rounds.
- How do you compare strings correctly for sorting?
- Use localeCompare. The default array sort compares UTF-16 code units, so uppercase sorts before lowercase and accented characters land after z. localeCompare follows language rules and accepts options for case and numeric ordering.
- What is String.raw used for?
- It returns the template literal with escape sequences left uninterpreted, so \n stays as a backslash and an n. It is useful for Windows paths, regex source strings, and anywhere you want the literal text a developer typed rather than the escaped result.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299