Lesson 0.6.1 — Strings
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.6 — Data Structures |
| Lesson | 0.6.1 |
| Competency | C01.6 — Arrays / Objects (String Operations) |
| Depth Target | L2–L3 |
| Prerequisites | Values (0.2.1), Primitive Types (0.2.3), typeof (0.2.5), Variables & Bindings (0.2.2), Functions (0.5.1–0.5.3) |
| Estimated Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn đã biết string là primitive type. Nhưng trong thực tế, hầu hết dữ liệu bạn xử lý đều là text: tên người dùng, địa chỉ email, log message, URL, JSON key.
Xét đoạn code sau:
function cleanInput(input) {
input.trim();
input.replace(" ", "_");
return input.toLowerCase();
}
const name = " Alice Smith ";
const result = cleanInput(name);
console.log(result); // " alice smith "
console.log(name); // " Alice Smith "Vấn đề cốt lõi
cleanInput không làm gì cả — hoặc đúng hơn, nó không làm những gì bạn tưởng. trim() và replace() không sửa chuỗi gốc. Chúng trả về chuỗi mới. Bạn bỏ qua giá trị trả về, nên input vẫn nguyên vẹn .
Đây là lỗi cực kỳ phổ biến ở Junior: nhầm tưởng string giống array/object, có thể "sửa" trực tiếp. Bài này xây mental model đúng về string: immutable sequence — đọc được từng ký tự, nhưng thay đổi thì phải tạo chuỗi mới.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- Phân biệt primitive value và object value (0.2.1).
- Biết
stringlà primitive, không phải object (0.2.3). - Hiểu
constcấm reassignment nhưng không cấm mutation — tuy nhiên string không thể mutate (0.2.2). - Biết gọi function và dùng
return(0.5.1).
WARNING
Nếu bạn chưa chắc tại sao const x = "a"; x = "b" lỗi nhưng const arr = [1]; arr.push(2) hợp lệ, quay lại 0.2.2. Sự khác biệt giữa reassignment và mutation là nền tảng của bài này.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Giải thích tại sao string trong JavaScript là immutable và điều đó ảnh hưởng thế nào đến cách viết code.
- Dự đoán output của các method string phổ biến:
slice,substring,split,replace,includes,startsWith,endsWith. - Phân biệt
slicevàsubstringkhi gặp index âm hoặc ngược thứ tự. - Sử dụng template literal thay cho string concatenation để xây dựng chuỗi động.
- Viết function xử lý chuỗi mà không mutate input (vì string không cho phép mutate trực tiếp).
- Nhận diện bug khi bỏ qua giá trị trả về của string method.
4. Mental Model (Mô hình tư duy)
Mental Model
String = Dãy ký tự không thể thay đổi (Immutable Sequence)
Index: 0 1 2 3 4
Chars: H e l l o
↑ ↑
str[0] str[4]
Đọc: ✅ str[0] → "H"
Ghi: ❌ str[0] = "X" → Không thay đổi được chuỗi
(non-strict: bị bỏ qua silently; strict mode: throw TypeError)
String Method = Máy sản xuất chuỗi mới
Input string ──→ [ Method ] ──→ New string
↓
Không sửa input
Luôn trả về giá trị mớiQuy tắc vàng: String methods không bao giờ thay đổi chuỗi gốc. Nếu bạn không gán hoặc return kết quả, bạn vừa làm một thao tác vô ích.
Template Literal là cú pháp viết chuỗi cho phép nhúng expression trực tiếp, thay vì nối chuỗi bằng +.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Ý nghĩa |
|---|---|
| String Literal | 'hello' hoặc "hello" — tạo string primitive. |
| Template Literal | `hello ${name}` — cho phép interpolation và multi-line. |
| Indexing | str[i] đọc ký tự tại index i. Bắt đầu từ 0. |
| Length | str.length — số lượng ký tự (UTF-16 code units). |
| Immutability | Không thể thay đổi ký tự tại index. Mọi "thay đổi" tạo string mới. |
slice(begin, end) | Cắt chuỗi từ begin đến end (không bao gồm end). Hỗ trợ index âm. |
substring(start, end) | Cắt chuỗi từ start đến end (không bao gồm end). Index âm được coi như 0. |
split(separator) | Tách chuỗi thành mảng các chuỗi con. |
replace(search, replacement) | Thay thế lần đầu tiên tìm thấy. Trả về chuỗi mới. |
includes(substring) | Trả về true nếu chuỗi chứa substring. |
startsWith(prefix) | Kiểm tra prefix. |
endsWith(suffix) | Kiểm tra suffix. |
indexOf(search) | Trả về vị trí đầu tiên tìm thấy, hoặc -1. |
lastIndexOf(search) | Trả về vị trí cuối cùng tìm thấy, hoặc -1. |
toUpperCase() | Trả về chuỗi mới với chữ hoa. |
toLowerCase() | Trả về chuỗi mới với chữ thường. |
trim() | Trả về chuỗi mới đã bỏ khoảng trắng đầu/cuối. |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
| Escape Sequences | \n (newline), \t (tab), \\ (backslash), \', \". |
| String Comparison | So sánh theo thứ tự từ điển (lexicographic), dựa trên UTF-16 code unit. |
Awareness (Biết tồn tại)
| Concept | Lý do chưa đào sâu |
|---|---|
| UTF-16 / Surrogate Pairs | length của emoji như "🚀".length === 2. Sẽ đề cập ở Edge Case nhưng không đi sâu encoding. |
String.prototype methods nâng cao | match, matchAll, search, replaceAll với RegExp. Học ở Stage 2. |
Out of Scope (Không thuộc bài này)
- Regular Expressions (Stage 2).
IntlAPI (định dạng ngày tháng, số, collation).- String iterator /
for...ofprotocol (Stage 2). - String encoding/decoding (
TextEncoder,atob).
6. Worked Example (Ví dụ phân tích từng bước)
Bài toán: Chuẩn hóa một dòng log từ server.
const raw = " [ERROR] Connection timeout on /api/users ";Yêu cầu:
- Bỏ khoảng trắng đầu/cuối.
- Kiểm tra có phải log lỗi không.
- Tách phần level (
ERROR) và phần message. - Thay thế
/api/usersbằng[REDACTED]. - Trả về chuỗi định dạng:
"ERROR | Connection timeout on [REDACTED]".
function parseLog(line) {
line.trim();
line.replace("/api/users", "[REDACTED]");
if (line.startsWith("[ERROR]")) {
return "Error: " + line;
}
return line;
}
console.log(parseLog(raw));
// " [ERROR] Connection timeout on /api/users " ❌function parseLog(line) {
// Step 1: Trim tạo chuỗi mới
const trimmed = line.trim();
// Step 2: Kiểm tra prefix
const isError = trimmed.startsWith("[ERROR]");
if (!isError) return trimmed;
// Step 3: Tách level và message
const parts = trimmed.split("]"); // ["[ERROR", " Connection timeout on /api/users"]
const level = parts[0].slice(1); // "ERROR"
const message = parts[1].trim(); // "Connection timeout on /api/users"
// Step 4: Replace tạo chuỗi mới
const cleanMessage = message.replace("/api/users", "[REDACTED]");
// Step 5: Template literal xây output
return `${level} | ${cleanMessage}`;
}
console.log(parseLog(raw));
// "ERROR | Connection timeout on [REDACTED]" ✅Walkthrough
Step 1 — Trimline.trim() trả về "[ERROR] Connection timeout on /api/users". Nếu không gán vào trimmed, line vẫn giữ 4 khoảng trắng.
Step 2 — Kiểm tra prefixstartsWith("[ERROR]") là cách đọc rõ intent hơn so với indexOf. Nó trả về boolean.
Step 3 — Tách bằng splitsplit("]") tách tại dấu ]. Lưu ý: split trả về mảng (sẽ đào sâu ở 0.6.2). Ta dùng slice(1) để bỏ dấu [ ở đầu.
Step 4 — Replacereplace("/api/users", "[REDACTED]") chỉ thay thế lần xuất hiện đầu tiên. Trả về chuỗi mới.
Step 5 — Template Literal`${level} | ${cleanMessage}` rõ ràng hơn level + " | " + cleanMessage, đặc biệt khi nhiều biến.
Key Insight: Mỗi method trả về giá trị mới. Chuỗi gốc raw không bao giờ bị thay đổi.
7. Prediction Exercise (Bài tập dự đoán)
Đừng chạy code. Đọc và dự đoán output, sau đó giải thích tại sao.
Câu 1
const s = "hello";
s[0] = "H";
console.log(s);Câu 2
const path = "/home/user/docs";
console.log(path.slice(1, 5));
console.log(path.substring(5, 1));Câu 3
const msg = "foo bar foo";
const r = msg.replace("foo", "baz");
console.log(r);
console.log(msg);Câu 4
const email = "user@example.com";
console.log(email.includes("@"));
console.log(email.startsWith("user"));
console.log(email.endsWith(".org"));Câu 5
const a = "5";
const b = 10;
console.log(a + b);
console.log(`${a}${b}`);[Đáp án & Giải thích]
Câu 1: "hello" — Immutability
- Giải thích: String là primitive immutable. Gán
s[0] = "H"ở strict mode sẽ silent fail (non-strict) hoặc throw TypeError (strict mode), nhưng trong mọi trường hợp chuỗiskhông đổi.
Câu 2: home và home — slice vs substring
- Giải thích:
slice(1, 5)cắt từ index 1 đến 4 →"home".substring(5, 1)khi start > end, nó tự đổi chỗ thành(1, 5)→ cũng"home". Đây là điểm khác biệt quan trọng:substringhoán đổi tham số nếu start > end, cònslicethì không.
Câu 3: "baz bar foo" và "foo bar foo" — replace chỉ thay lần đầu
- Giải thích:
replacechỉ thay thế lần đầu tìm thấy"foo". Chuỗi gốcmsgkhông đổi.
Câu 4: true, true, false — Kiểm tra nội dung
- Giải thích:
includes("@")true.startsWith("user")true.endsWith(".org")false vì kết thúc bằng.com.
Câu 5: "510" và "510" — Concatenation vs Template Literal
- Giải thích: Cả hai đều thực hiện string concatenation. Vì
alà string,a + bưu tiên nối chuỗi và chuyểnbthành string. Template literal cũng chuyển expression bên trong thành string qua ToString. Kết quả giống nhau.
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Có hướng dẫn)
Viết function formatUserName nhận firstName và lastName, trả về "Họ Và Tên" (viết hoa chữ cái đầu mỗi từ, bỏ khoảng trắng thừa).
function formatUserName(firstName, lastName) {
// 1. Trim cả hai
// 2. Viết hoa chữ cái đầu
// 3. Nối bằng " " (template literal)
}
console.log(formatUserName(" alice ", "SMITH"));
// "Alice Smith"Gợi ý
Dùng slice hoặc substring để tách chữ cái đầu. Ví dụ: name.slice(0, 1).toUpperCase() + name.slice(1).toLowerCase().
[Đáp án tham khảo]
function formatUserName(firstName, lastName) {
const f = firstName.trim();
const l = lastName.trim();
const format = (str) => {
if (str.length === 0) return str;
return str[0].toUpperCase() + str.slice(1).toLowerCase();
};
return `${format(f)} ${format(l)}`;
}
console.log(formatUserName(" alice ", "SMITH"));
// "Alice Smith"
console.log(formatUserName("BOB", ""));
// "Bob "Giải thích:
trim()tạo chuỗi mới không có khoảng trắng đầu/cuối.str[0]đọc ký tự đầu tiên.slice(1)lấy phần còn lại từ index 1 đến hết.- Template literal
`${format(f)} ${format(l)}`nối hai chuỗi có khoảng trắng ở giữa. - Function helper
formatgiúp tái sử dụng logic viết hoa.
Level 2 — Partial Scaffold (Khung sẵn)
Hoàn thành function parseFileName để tách tên file và extension.
function parseFileName(fullName) {
// Tìm vị trí dấu chấm cuối cùng
const dotIndex = fullName._______(".");
if (dotIndex === -1) {
return { name: fullName, ext: "" };
}
const name = fullName._______(0, dotIndex);
const ext = fullName._______(dotIndex + 1);
return { name, ext };
}
console.log(parseFileName("document.pdf"));
// { name: "document", ext: "pdf" }
console.log(parseFileName("archive.tar.gz"));
// { name: "archive.tar", ext: "gz" }[Đáp án tham khảo]
function parseFileName(fullName) {
const dotIndex = fullName.lastIndexOf(".");
if (dotIndex === -1) {
return { name: fullName, ext: "" };
}
const name = fullName.slice(0, dotIndex);
const ext = fullName.slice(dotIndex + 1);
return { name, ext };
}
console.log(parseFileName("document.pdf"));
// { name: "document", ext: "pdf" }
console.log(parseFileName("archive.tar.gz"));
// { name: "archive.tar", ext: "gz" }Giải thích:
lastIndexOf(".")tìm dấu chấm cuối cùng để lấy đúng extension (tránh nhầm với dấu chấm trong tên file).slice(0, dotIndex)lấy phần trước dấu chấm.slice(dotIndex + 1)lấy phần sau dấu chấm đến hết chuỗi.slicehỗ trợ index âm nhưng ở đây không cần.
Level 3 — Independent (Tự viết)
Viết các function xử lý chuỗi sau (không dùng thư viện, không mutate input):
// 1. truncate(text, maxLength) → Nếu text dài hơn maxLength, cắt và thêm "..."
// Ví dụ: truncate("Hello World", 8) → "Hello..."
// 2. slugify(title) → Chuyển thành dạng URL-friendly
// lowercase, thay khoảng trắng bằng "-", bỏ dấu câu
// Ví dụ: slugify("Hello, World!") → "hello-world"
// 3. maskEmail(email) → Ẩn phần trước @, chỉ để lại 2 ký tự đầu và "****"
// Ví dụ: maskEmail("alice@example.com") → "al****@example.com"
// Sử dụng:
console.log(truncate("JavaScript Engineering", 10));
console.log(slugify("Hello, World!!!"));
console.log(maskEmail("bob.smith@company.org"));[Đáp án tham khảo]
function truncate(text, maxLength) {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - 3) + "...";
}
function slugify(title) {
const lower = title.toLowerCase();
// Bỏ dấu câu bằng chuỗi thao tác (không dùng regex)
let cleaned = lower;
for (const char of ".,!?:;") {
cleaned = cleaned.split(char).join("");
}
// Thay khoảng trắng liên tiếp bằng "-"
const words = cleaned.trim().split(" ").filter(w => w !== "");
return words.join("-");
}
function maskEmail(email) {
const atIndex = email.indexOf("@");
const local = email.slice(0, atIndex);
const domain = email.slice(atIndex);
const visible = local.slice(0, 2);
return visible + "****" + domain;
}
console.log(truncate("JavaScript Engineering", 10));
// "JavaScr..."
console.log(slugify("Hello, World!!!"));
// "hello-world"
console.log(maskEmail("bob.smith@company.org"));
// "bo****@company.org"Giải thích:
truncate: Kiểm tra độ dài trước. Nếu cần cắt, chừa 3 ký tự cho"...".slugify: Chuỗi hóa thành lowercase → bỏ dấu câu bằngreplacevới regex đơn giản → thay khoảng trắng liên tiếp bằng-.maskEmail: Tách tại@, giữ 2 ký tự đầu local part, nối lại.- Cả ba function đều trả về chuỗi mới, không thay đổi input.
Out of Scope
Có thể dùng Regular Expressions sẽ học ở Stage 2 cho function này
function slugify(title) {
const lower = title.toLowerCase();
const noPunctuation = lower.replace(/[.,!?:;]/g, "");
const normalized = noPunctuation.trim().replace(/\s+/g, "-");
return normalized;
}9. Edge Cases (Các trường hợp ngoại lệ)
String indexing không cho phép gán
const s = "hello";
s[0] = "H";
console.log(s); // "hello"Tại sao: String primitive là immutable. Trong non-strict mode, gán bị bỏ qua silently. Trong strict mode ("use strict"), sẽ throw TypeError.
Cách nhận biết: Code có vẻ "sửa" chuỗi nhưng kết quả không đổi.
Cách xử lý: Tạo chuỗi mới: "H" + s.slice(1).
slice vs substring với index âm
const s = "hello";
console.log(s.slice(-2)); // "lo"
console.log(s.substring(-2)); // "hello" (coi -2 như 0)
console.log(s.slice(3, 1)); // "" (start > end → empty)
console.log(s.substring(3, 1));// "el" (hoán đổi thành 1, 3)Tại sao: slice hỗ trợ index âm (tính từ cuối) và không hoán đổi tham số. substring không hỗ trợ index âm và luôn hoán đổi nếu start > end.
Cách nhận biết: Kết quả cắt chuỗi bất ngờ khi dùng số âm.
Cách xử lý: Ưu tiên dùng slice vì behavior nhất quán hơn. Chỉ dùng substring khi bạn cố ý muốn hoán đổi.
replace chỉ thay lần đầu tiên
const s = "foo foo foo";
const r = s.replace("foo", "bar");
console.log(r); // "bar foo foo"Tại sao: replace với string literal chỉ thay thế first occurrence.
Cách nhận biết: Chuỗi kết quả vẫn còn substring cũ.
Cách xử lý: Dùng replaceAll (ES2021) hoặc split + join:
s.split("foo").join("bar"); // "bar bar bar"Hoặc RegExp với global flag (học ở Stage 2).
split với empty string
const s = "hello";
console.log(s.split("")); // ["h", "e", "l", "l", "o"]
console.log([...s]); // ["h", "e", "l", "l", "o"]Tại sao: Cả hai đều tách string thành mảng theo UTF-16 code units. split("") là cách truyền thống, spread (...) là cách hiện đại. Với emoji như "🚀", cả hai đều tách thành 2 phần (surrogate pairs) — đây là giới hạn của mental model "mỗi index là một ký tự hiển thị".
Cách nhận biết: Không phải bug, nhưng cần biết cả hai cách đều tồn tại và đều dựa trên UTF-16 code units.
Template literal không phải là "string thông thường"
const x = 5;
console.log("Value: ${x}"); // "Value: ${x}" ❌
console.log(`Value: ${x}`); // "Value: 5" ✅Tại sao: Chỉ dấu backtick (`) mới kích hoạt interpolation. Dấu nháy đơn/kép không có tác dụng.
Cách nhận biết: Biến không được thay thế, chuỗi in ra nguyên văn ${x}.
Cách xử lý: Kiểm tra dấu bao chuỗi. Nếu cần backtick trong chuỗi, dùng escape: \`.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Function chuẩn hóa tên file luôn trả về chuỗi gốc, không thay đổi.
function normalizeFileName(name) {
name.trim();
name.toLowerCase();
name.replace(/\s+/g, "-");
return name;
}
console.log(normalizeFileName(" My Document "));
// " My Document " ❌Reproduction (Tái hiện lỗi): Chạy code trên.
Evidence (Bằng chứng):
- Output giống hệt input, kể cả khoảng trắng đầu/cuối.
- Không có lỗi throw.
Hypothesis (Giả thuyết): Các method trim(), toLowerCase(), replace() đều trả về chuỗi mới. Code gọi method nhưng không gán hoặc return kết quả. Do string immutable, name không bao giờ thay đổi.
Verification (Xác minh):
const s = " test ";
const t = s.trim();
console.log(s === t); // false
console.log(s); // " test "Chứng minh trim() tạo chuỗi mới, không sửa s.
Root Cause (Nguyên nhân gốc rễ): Hiểu nhầm string behavior giống mutable object. Junior quen với array.push() (mutate array) nên nghĩ string.trim() cũng mutate.
Fix (Sửa):
function normalizeFileName(name) {
return name
.trim()
.toLowerCase()
.replace(/\s+/g, "-");
}Prevention (Phòng ngừa):
- Khi gọi string method, luôn hỏi: "Kết quả có được gán hoặc return không?"
- Nếu method trả về giá trị (như hầu hết string methods), phải bắt lấy.
- Trong code review, nghi ngờ mọi dòng
str.method()đứng một mình không gán. - Bật linter rule
no-unused-expressionshoặc để ý cảnh báo của IDE khi một expression không được sử dụng.
11. Design Exercise (Bài tập thiết kế giải pháp)
Bạn cần kiểm tra xem một chuỗi có phải là "safe path" không (không chứa .., không bắt đầu bằng /, không chứa khoảng trắng).
function isSafePath(path) {
if (path.startsWith("/")) return false;
if (path.includes("..")) return false;
if (path.includes(" ")) return false;
return true;
}function isSafePath(path) {
return !path.startsWith("/") && !path.includes("..") && !path.includes(" ");
}Câu hỏi:
- Option A dễ đọc hơn ở điểm nào?
- Option B có lợi gì?
- Nếu cần thêm quy tắc "không chứa
\", bạn sửa option nào dễ hơn? Tại sao?
[Đáp án tham khảo]
Bạn nghĩ:
- Option A dễ đọc vì mỗi điều kiện là một dòng, có thể đặt comment hoặc log riêng. Khi debug, có thể đặt breakpoint từng dòng.
- Option B ngắn gọn, expression duy nhất, phù hợp khi logic ổn định và không cần debug thường xuyên.
- Option A dễ sửa hơn: thêm một dòng
if (path.includes("\\")) return false;. Option B phải thêm vào chuỗi&&, làm dòng dài hơn và khó đọc.
Kết luận: Với validation logic (nơi dễ thay đổi rule và cần debug), Option A tốt hơn. Đây là ví dụ về "single responsibility từng dòng" áp dụng cho string checking.
12. Production Scenario (Tình huống thực tế)
Context: Bạn nhận user input từ form đăng ký:
const rawEmail = " Alice.Example@Domain.COM ";Yêu cầu production:
- Chuẩn hóa email trước khi lưu database.
- Không được mutate biến gốc (có thể cần log raw input sau này).
- Logic phải dễ test: cho input, kiểm tra output chính xác.
Code hiện tại của team:
function processEmail(input) {
input.trim();
input.toLowerCase();
return input;
}Symptom: Email lưu vào database vẫn có khoảng trắng và chữ hoa. Log raw input thì đúng, nhưng sau khi qua processEmail vẫn không đổi.
Constraint: Không được thay đổi signature function (vẫn nhận 1 tham số, trả về string).
Câu hỏi:
- Tại sao email không được chuẩn hóa?
- Viết lại
processEmailđúng. - Giải thích tại sao việc "không mutate input" ở đây lại dễ đạt được.
[Đáp án tham khảo]
Bạn nghĩ:
trim()vàtoLowerCase()trả về chuỗi mới nhưng không được gán. Input là primitive string nên không thể mutate dù muốn. Tuy nhiên, việc bỏ qua return value khiến function trả về chuỗi gốc.- js
function processEmail(input) { return input.trim().toLowerCase(); } - String là primitive immutable. Dù function trên có vô tình "quên" gán, string gốc cũng không bị sửa. Đây là lợi ích của immutability: an toàn mặc định (safe by default). So với object/array (có thể mutate bất cẩn), string bảo vệ caller khỏi side effect.
Bài học: Immutability của string là một đặc điểm defensive. Trong production, nó ngăn chặn một lớp bug "mutate input argument" mà object/array dễ mắc phải.
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge
- Tự trả lời trước: Viết một đoạn code JavaScript tạo ra chuỗi mới từ chuỗi cũ mà AI có thể nghĩ là "đang mutate string". Gợi ý: dùng
replacehoặcslicenhưng không gán kết quả. - Hỏi AI: "Đoạn code này có mutate chuỗi
originalkhông?"jsconst original = "error: connection failed"; original.replace("error", "warn"); console.log(original); - So sánh câu trả lời AI với nhận định của bạn. AI có nhấn mạnh rằng
originalkhông đổi không? - Verify bằng MDN: tìm
String.prototype.replacetrên MDN và đọc phần "Return value".
Gợi ý
AI thường trả lời đúng về immutability ở mức surface. Nhưng khi được hỏi về "tại sao console.log vẫn in ra chuỗi cũ", AI đôi khi giải thích mơ hồ bằng các từ như "chuỗi được xử lý nhưng không lưu lại" mà không nói rõ string primitive là immutable — không có method nào có thể thay đổi nội dung của nó. Nếu AI không dùng từ "immutable" hoặc "trả về chuỗi mới", bạn đã tìm ra điểm mù.
Đáp án tham khảo
Bạn nghĩ:
originalkhông đổi.replacetrả về chuỗi mới"warn: connection failed"nhưng bị bỏ qua.originalvẫn là"error: connection failed".AI trả lời (typical):
- No,
originalis not mutated. replacereturns a new string.- The result is not assigned, so it is lost.
originalremains unchanged.
- No,
So sánh: AI thường nhận ra immutability nhưng đôi khi diễn đạt như thể "bạn quên lưu kết quả" là lỗi duy nhất. Điều này đúng nhưng chưa đủ sâu.
Điểm AI nói sai hoặc quá mơ hồ: AI hiếm khi giải thích rằng: "Ngay cả khi bạn cố tình muốn mutate
original, JavaScript cũng không cho phép. String primitive không có setter cho index. Đây là đặc tính của language, không chỉ là thói quen 'nhớ gán kết quả'." Sự khác biệt này quan trọng vì nó phân biệt string (immutable) với array/object (mutable).Kết luận: Nếu bạn chỉ ra được rằng immutability của string là invariant của language (bất biến ngôn ngữ), không chỉ là "best practice", bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở Stage 2 (Object Model — tại sao
new String()là object mutable nhưng không nên dùng) và Stage 8 (React — state immutability pattern).
14. Teach Back (Dạy lại)
Giả sử một junior developer hỏi bạn:
"Anh/chị ơi, sao em viết
name[0] = "A"mà chuỗinamekhông đổi? Em đã dùngletrồi mà. Vàname.trim()cũng không làm gì luôn. JavaScript bị lỗi à?"
Hãy giải thích trong 2 phút, dùng đúng terminology: primitive, immutable, return value, indexing.
Mô phỏng
- Bạn nói: Không phải lỗi đâu. String trong JavaScript là primitive và immutable — nghĩa là một khi tạo ra, nội dung bên trong không thể thay đổi. Dùng
lethayconstchỉ liên quan đến việc bạn có thể gán biến sang chuỗi khác hay không, chứ không liên quan đến việc sửa nội dung chuỗi.
Về name[0] = "A": JavaScript không cho phép gán vào index của string. Câu lệnh này bị bỏ qua. Về name.trim(): trim() không sửa name — nó tạo ra một chuỗi mới đã được trim. Nếu em không gán hoặc return nó, chuỗi mới đó biến mất. Cách đúng là const cleaned = name.trim();.
Kết luận: Hãy nghĩ string như một tờ giấy đã in. Em không thể sửa chữ trên tờ giấy đó. Muốn thay đổi, em phải photo ra tờ mới với nội dung khác.
💡 Tưởng tượng string như một chiếc vé số đã in. Bạn không thể tẩy xóa số trên vé. Nếu muốn số khác, bạn phải mua vé mới.
slice,replace,trimchính là "quầy đổi vé" — họ nhận vé cũ, in vé mới, nhưng vé cũ vẫn nằm nguyên trong tay bạn.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
letkhông giúp mutate string không? - Bạn có tránh được việc nói "string giống array" không? (Array là mutable object, string là immutable primitive — behavior hoàn toàn khác.)
- Nếu đồng nghiệp hỏi: "Vậy tại sao
name[0]vẫn đọc được?" — bạn trả lời được không? (Gợi ý: read access được phép, write access bị cấm.) - Nếu đồng nghiệp hỏi:
"hello".split("").reverse().join("")có mutate"hello"không? — bạn trả lời được trong 5 giây không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Task |
|---|---|---|
| Explain string immutability (Giải thích immutability) | Teach Back | Teach Back Section |
| Predict method output (Dự đoán output method) | Prediction (Dự đoán) | Prediction Exercise |
| Use template literal (Sử dụng template literal) | Implementation (Thực hành) | Implementation Lab Level 1 |
Distinguish slice vs substring (Phân biệt slice/substring) | Prediction (Dự đoán) | Prediction Exercise Câu 2 |
| Handle string without mutating (Xử lý string không mutate) | Implementation (Thực hành) | Implementation Lab Level 3 |
| Debug ignored return value (Gỡ lỗi bỏ qua return value) | Debug Lab | Debug Lab Section |
| Choose validation style (Chọn style validation) | Design Decision | Design Exercise |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích tại sao string là immutable primitive và điều đó ảnh hưởng thế nào đến việc gọi method.
- [ ] Có thể dự đoán output của
slice,substring,split,replace,includes,startsWith,endsWithtrong các scenario cơ bản. - [ ] Có thể viết function xử lý chuỗi bằng cách kết hợp các method, trả về chuỗi mới, không bỏ qua giá trị trả về.
- [ ] Có thể phân biệt
slicevàsubstringkhi gặp index âm hoặc start > end. - [ ] Có thể sử dụng template literal thay cho string concatenation trong ít nhất 2 scenario.
- [ ] Có thể debug bug "string method không có tác dụng" do không gán return value.
- [ ] Có thể nhận diện rằng
replacechỉ thay thế lần đầu tiên nếu không dùng RegExp.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Values & Types (0.2.1–0.2.5) — bạn đã biết string là primitive value, có thể kiểm tra bằng
typeof. Giờ bạn học cách thao tác trên giá trị đó.
Current (Hiện tại): String Operations — immutability, indexing, methods, template literals. Đây là foundation cho mọi thao tác text trong JavaScript.
Next (Tiếp theo):
- 0.6.2 (Arrays) — Mảng cũng là sequence, nhưng mutable. So sánh string (immutable primitive) với array (mutable object) sẽ củng cố mental model về mutation.
- 0.6.5 (Objects) — Object property access tương tự string indexing về syntax, nhưng behavior khác biệt sâu sắc.
- Stage 2 — Object Model: tại sao
new String("hello")tạo object wrapper mutable nhưng không bao giờ nên dùng.- Stage 6 (TypeScript) — Template literal types:
`error-${string}`.- Stage 8 (React) — JSX cũng dùng template-like syntax, và state immutability pattern yêu cầu tạo mới thay vì sửa cũ — giống hệt string behavior.