Skip to content

Lesson 0.6.1 — Strings ​

Bài 0.6.1 — Strings
Creation, Indexing, Template literals & Methods • 31 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.6 — Data Structures
Lesson0.6.1
CompetencyC01.6 — Arrays / Objects (String Operations)
Depth TargetL2–L3
PrerequisitesValues (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 LoadMedium

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:

js
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 string là primitive, không phải object (0.2.3).
  • Hiểu const cấ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ể:

  1. 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.
  2. Dự đoán output của các method string phổ biến: slice, substring, split, replace, includes, startsWith, endsWith.
  3. Phân biệt slice và substring khi gặp index âm hoặc ngược thứ tự.
  4. Sử dụng template literal thay cho string concatenation để xây dựng chuỗi động.
  5. Viết function xử lý chuỗi mà không mutate input (vì string không cho phép mutate trực tiếp).
  6. 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ới

Quy 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.
Indexingstr[i] đọc ký tự tại index i. Bắt đầu từ 0.
Lengthstr.length — số lượng ký tự (UTF-16 code units).
ImmutabilityKhô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 ComparisonSo sánh theo thứ tự từ điển (lexicographic), dựa trên UTF-16 code unit.

Awareness (Biết tồn tại) ​

ConceptLý do chưa đào sâu
UTF-16 / Surrogate Pairslength của emoji như "🚀".length === 2. Sẽ đề cập ở Edge Case nhưng không đi sâu encoding.
String.prototype methods nâng caomatch, 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).
  • Intl API (định dạng ngày tháng, số, collation).
  • String iterator / for...of protocol (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.

js
const raw = "  [ERROR]  Connection timeout on /api/users  ";

Yêu cầu:

  1. Bỏ khoảng trắng đầu/cuối.
  2. Kiểm tra có phải log lỗi không.
  3. Tách phần level (ERROR) và phần message.
  4. Thay thế /api/users bằng [REDACTED].
  5. Trả về chuỗi định dạng: "ERROR | Connection timeout on [REDACTED]".
js
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  " ❌
js
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 ​

js
const s = "hello";
s[0] = "H";
console.log(s);

Câu 2 ​

js
const path = "/home/user/docs";
console.log(path.slice(1, 5));
console.log(path.substring(5, 1));

Câu 3 ​

js
const msg = "foo bar foo";
const r = msg.replace("foo", "baz");
console.log(r);
console.log(msg);

Câu 4 ​

js
const email = "user@example.com";
console.log(email.includes("@"));
console.log(email.startsWith("user"));
console.log(email.endsWith(".org"));

Câu 5 ​

js
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ỗi s khô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: substring hoán đổi tham số nếu start > end, còn slice thì không.

Câu 3: "baz bar foo" và "foo bar foo" — replace chỉ thay lần đầu

  • Giải thích: replace chỉ thay thế lần đầu tìm thấy "foo". Chuỗi gốc msg khô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ì a là string, a + b ưu tiên nối chuỗi và chuyển b thà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).

js
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]
js
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 format giú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.

js
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]
js
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.
  • slice hỗ 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):

js
// 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]
js
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ằng replace vớ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

js
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

js
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

js
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

js
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:

js
s.split("foo").join("bar"); // "bar bar bar"

Hoặc RegExp với global flag (học ở Stage 2).

split với empty string

js
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"

js
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.

js
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):

js
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):

js
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-expressions hoặ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).

js
function isSafePath(path) {
  if (path.startsWith("/")) return false;
  if (path.includes("..")) return false;
  if (path.includes(" ")) return false;
  return true;
}
js
function isSafePath(path) {
  return !path.startsWith("/") && !path.includes("..") && !path.includes(" ");
}

Câu hỏi:

  1. Option A dễ đọc hơn ở điểm nào?
  2. Option B có lợi gì?
  3. 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ĩ:

    1. 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.
    2. 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.
    3. 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ý:

js
const rawEmail = "  Alice.Example@Domain.COM  ";

Yêu cầu production:

  1. Chuẩn hóa email trước khi lưu database.
  2. Không được mutate biến gốc (có thể cần log raw input sau này).
  3. Logic phải dễ test: cho input, kiểm tra output chính xác.

Code hiện tại của team:

js
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:

  1. Tại sao email không được chuẩn hóa?
  2. Viết lại processEmail đúng.
  3. 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ĩ:

    1. 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.
    2. js
      function processEmail(input) {
        return input.trim().toLowerCase();
      }
    3. 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

  1. 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 replace hoặc slice nhưng không gán kết quả.
  2. Hỏi AI: "Đoạn code này có mutate chuỗi original không?"
    js
    const original = "error: connection failed";
    original.replace("error", "warn");
    console.log(original);
  3. 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 original không đổi không?
  4. Verify bằng MDN: tìm String.prototype.replace trê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ĩ: original không đổi. replace trả về chuỗi mới "warn: connection failed" nhưng bị bỏ qua. original vẫn là "error: connection failed".

  • AI trả lời (typical):

    • No, original is not mutated.
    • replace returns a new string.
    • The result is not assigned, so it is lost.
    • original remains unchanged.
  • 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ỗi name không đổi? Em đã dùng let rồ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 let hay const chỉ 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, trim chí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 let khô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 BackTeach 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 LabDebug Lab Section
Choose validation style (Chọn style validation)Design DecisionDesign 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, endsWith trong 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 slice và substring khi 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 replace chỉ 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.
📴 Offline Mode — Content served from cache