Skip to content

Lesson 1.2.5 — Nested Scope & Shadowing (Phạm vi Lồng nhau & Che phủ) ​

Bài 1.2.5 — Nested Scope
Nested Functions, Parent Scope & Scope Chain • 23 phút
0:00 / 0:00

0. Metadata ​

TrườngGiá trị
Stage1 — JavaScript Execution Model
Module1.2 — Scope & Lexical Environment
Lesson1.2.5
CompetencyC02 — JavaScript Runtime
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.2.1–1.2.4 — Scope, Global Scope, Function Scope, Block Scope
Cognitive LoadTrung bình–Cao

1. Why This Exists (Vì sao cần học) ​

Bạn đang debug một hàm xử lý dữ liệu phức tạp:

js
const config = { env: "production" };

function setup() {
  const config = { env: "staging" };

  function connect() {
    console.log(config.env);
  }

  connect();
}

setup();

Production Risk

connect() in ra "staging", không phải "production". Nếu developer không hiểu nested scope và shadowing, họ sẽ mất hàng giờ tìm kiếm "tại sao biến bị đổi" hoặc "ai đã ghi đè config", trong khi thực tế không có mutation nào xảy ra — chỉ là hai biến khác nhau trong hai scope lồng nhau.

Nested scope và shadowing trả lời câu hỏi:

"Khi có nhiều biến cùng tên ở nhiều tầng scope, engine chọn cái nào?"

Nếu không nắm chắc, bạn sẽ không thể:

  • Trace đúng giá trị của biến trong code lồng nhau.
  • Debug "variable seemingly changes value" mà thực ra là shadowing.
  • Hiểu closure environment (Module 1.4) vì closure dựa trên scope chain.

2. Prerequisites (Yêu cầu đầu vào) ​

Bạn cần đã biết:

  • Scope là "vùng nhìn thấy" (Lesson 1.2.1).
  • Global scope, function scope, block scope (Lesson 1.2.2–1.2.4).
  • Variable resolution cơ bản: tìm từ trong ra ngoài (Lesson 1.2.1).

3. Learning Objectives (Mục tiêu học tập) ​

Sau lesson này, bạn có thể:

  1. Giải thích nested scope là scope lồng nhau theo cấu trúc code.
  2. Vẽ/mô tả scope chain (scope graph) cho code có 3–4 tầng lồng nhau.
  3. Định nghĩa shadowing là hiện tượng biến inner scope che biến outer scope cùng tên.
  4. Dự đoán giá trị của biến bị shadow trong nested scope.
  5. Phân biệt shadowing và reassignment/mutation.

4. Mental Model (Mô hình tư duy) ​

Nested Scope = Búp Bê Matryoshka (Lồng nhau)

Hãy hình dung scope như các lớp búp bê Matryoshka:

text
Búp bê ngoài cùng (Global)
  └── Búp bê giữa (Function outer)
        └── Búp bê trong cùng (Function inner)
  • Khi inner cần một biến, nó nhìn vào búp bê của chính mình trước.
  • Nếu không có, nó mở lớp vỏ ra và nhìn vào búp bê bên ngoài.
  • Nếu búp bê trong cùng có một biến cùng tên với búp bê ngoài, nó dùng của mình và không cần mở ra ngoài. Đây là shadowing.

Shadowing không phải "ghi đè" (overwrite). Búp bê ngoài vẫn còn nguyên vẹn — búp bê trong chỉ che mắt engine khỏi thấy búp bê ngoài từ vị trí của nó.

Sai lầm phổ biến

"Shadowing làm biến ngoài bị xóa hoặc thay đổi."

Sai. Shadowing chỉ ảnh hưởng visibility (nhìn thấy), không ảnh hưởng existence hay value của biến ngoài. Biến ngoài vẫn tồn tại và giữ nguyên giá trị — chỉ là từ vị trí inner scope, engine không "nhìn" đến nó.

5. Core Concepts (Các khái niệm cốt lõi) ​

LoạiNội dung
Essential (Bắt buộc)Nested scope, Scope chain, Outer/Inner scope, Shadowing
Supporting (Hỗ trợ)Scope graph visualization, Lexical nesting
Awareness (Biết tồn tại)globalThis property shadowing (edge case)
Out of Scope (Không học ở đây)Closure (Module 1.4), this resolution (Stage 2), TDZ (Module 1.3)

6. Worked Example (Ví dụ phân tích từng bước) ​

Input:

js
const a = 1;

function outer() {
  const b = 2;

  function inner() {
    const c = 3;
    console.log(a, b, c);
  }

  inner();
}

outer();

Step 1 — Observation: inner() cần resolve a, b, c. c khai báo trong inner, b trong outer, a trong global.

Step 2 — Classification: Variable resolution qua scope chain với 3 tầng nested.

Step 3 — Reasoning — Scope Graph:

text
Global Scope
├── a = 1
└── outer() Scope
    ├── b = 2
    └── inner() Scope
        └── c = 3

Engine thực hiện lexical lookup: tìm identifier từ scope hiện tại ra ngoài, và dừng ngay tại binding đầu tiên tìm thấy (first match). Đây là lý do shadowing hoạt động: một khi đã tìm thấy c trong inner, engine không bao giờ "nhìn qua" để tìm c ở scope ngoài.

Step 4 — Resolution Path:

IdentifierinnerouterGlobalKết quả
c✅ 3——3
b❌✅ 2—2
a❌❌✅ 11

Step 5 — Conclusion: console.log(a, b, c) in ra 1 2 3. Engine đi từ trong ra ngoài theo scope chain cho đến khi tìm thấy hoặc hết chain.

7. Prediction Exercise (Bài tập dự đoán) ​

Đừng chạy code. Đoán output hoặc lỗi, sau đó giải thích tại sao.

P1 — Basic Shadowing ​

js
let x = 10;

function foo() {
  let x = 20;
  console.log(x);
}

foo();
console.log(x);
[Đáp án & Giải thích]
  • Bạn nghĩ: In ra 20 rồi 10. x = 20 bên trong foo shadow x = 10 ở global. Từ bên trong foo, engine tìm thấy x local trước và dừng lại. Từ global, x vẫn là 10.
  • Giải thích: Shadowing không thay đổi biến ngoài. foo có scope chain riêng, x = 20 là binding riêng trong function scope của foo.

P2 — Deep Nesting Shadow ​

js
const value = "global";

function alpha() {
  const value = "alpha";

  function beta() {
    const value = "beta";
    console.log(value);
  }

  beta();
  console.log(value);
}

alpha();
console.log(value);
[Đáp án & Giải thích]
  • Bạn nghĩ: In ra "beta", "alpha", "global". Mỗi tầng có value riêng. beta nhìn thấy value = "beta" (local, tìm trong scope của beta trước và dừng lại). alpha nhìn thấy value = "alpha" (local, vì beta() đã return nên chỉ còn scope của alpha active). Global nhìn thấy value = "global".
  • Giải thích: Đây là shadowing qua 3 tầng. Engine luôn tìm từ trong ra ngoài và dừng ở binding đầu tiên khớp tên. Không có ghi đè. Mỗi scope có một binding độc lập.

P3 — Shadowing trong Block ​

js
let count = 0;

function update() {
  let count = 5;

  if (true) {
    let count = 10;
    console.log(count);
  }

  console.log(count);
}

update();
console.log(count);
[Đáp án & Giải thích]
  • Bạn nghĩ: In ra 10, 5, 0. Block if tạo block scope mới, count = 10 shadow count = 5 của update. update nhìn thấy count = 5 (function scope). Global nhìn thấy count = 0.
  • Giải thích: Kết hợp function scope và block scope. Block scope trong if là tầng lồng nhau sâu hơn. Shadowing hoạt động giống nhau: tìm từ trong ra ngoài, dừng ở lần tìm thấy đầu tiên.

8. Implementation Lab (Bài lab thực hành) ​

Lab: Trace Scope Chain (Guided → Independent) ​

Yêu cầu: Cho đoạn code sau, hãy vẽ/mô tả scope graph và cho biết mỗi console.log in ra gì.

js
const api = "global-api";

function service() {
  const api = "service-api";

  function helper() {
    const api = "helper-api";
    console.log(api);
  }

  helper();
  console.log(api);
}

service();
console.log(api);

Nhiệm vụ:

  1. Vẽ scope graph dạng cây.
  2. Viết resolution path cho từng console.log(api).
  3. Giải thích tại sao có 3 giá trị khác nhau mà không có mutation.
[Đáp án tham khảo]
  • Bạn nghĩ:
    • helper() in ra "helper-api" (local api trong helper).
    • service() in ra "service-api" (local api trong service, helper đã return nên không nhìn thấy helper-api).
    • Global in ra "global-api" (global binding).
  • Giải thích:
    • Scope graph:
      Global: api = "global-api"
      └── service(): api = "service-api"
          └── helper(): api = "helper-api"
    • Mỗi api là binding độc lập. Shadowing làm engine dừng tìm kiếm ở scope gần nhất. Không có mutation vì 3 biến này ở 3 scope khác nhau. :::

9. Edge Cases (Các trường hợp ngoại lệ) ​

EC-1: Shadowing không ngăn truy cập outer bằng window/globalThis ​

Baseline: Ví dụ này giả định đang chạy trong browser global script (không phải ES module). Trong ES module, var ở top-level không tạo property trên global object.

js
var x = 100;

function test() {
  var x = 200;
  console.log(x);
  console.log(window.x);
}

test();

What fails? Nếu nghĩ shadowing làm biến outer "biến mất hoàn toàn", bạn sẽ bối rối khi window.x vẫn truy cập được 100.

Why? Shadowing chỉ ảnh hưởng lexical resolution (tìm theo scope chain). window.x là property access trên global object, không phải lexical lookup. var x = 100 trong global script tạo property window.x, vẫn tồn tại độc lập với local x.

EC-2: Shadowing và Reassignment ​

js
let a = 1;

function demo() {
  a = 2;
  let a = 3;
  console.log(a);
}

demo();

What fails? Dòng a = 2 throw ReferenceError: Cannot access 'a' before initialization (TDZ).

Why? Mặc dù let a = 3 ở dưới, scope của demo đã "đăng ký" binding a trong creation phase. Dòng a = 2 cố gắng resolve a trong scope của demo, tìm thấy binding chưa initialize (TDZ). Engine không nhảy ra global để dùng a = 1. Điều này thuộc Module 1.3 (Hoisting & TDZ).

10. Debug Lab (Bài lab gỡ lỗi) ​

Symptom (Triệu chứng): Developer báo cáo "config bị đổi sau khi gọi hàm helper". Họ kiểm tra và thấy config trong outer function vẫn đúng, nhưng connect() lại dùng sai giá trị.

Reproduction (Tái hiện lỗi):

js
const config = { env: "production", url: "prod.api.com" };

function setup() {
  const config = { env: "staging" };

  function connect() {
    console.log(config.env, config.url);
  }

  connect();
}

setup();

Evidence (Bằng chứng): Console in ra "staging undefined" thay vì "staging prod.api.com" hoặc "production prod.api.com".

Hypothesis (Giả thuyết): Developer mong đợi connect() "merge" config: lấy env từ setup và url từ global. Hoặc họ tưởng config trong setup là "cập nhật" global config.

Verification (Xác minh): Thêm console.log(config) trong từng scope để xác định binding nào đang được resolve.

Root Cause (Nguyên nhân gốc rễ): Shadowing + Partial Object Assumption. connect() resolve config trong scope của setup (tìm thấy config = { env: "staging" }). Nó không tự động merge với global config. Shadowing không phải inheritance. Mỗi scope chỉ chọn một binding đầu tiên tìm thấy trên scope chain.

Shadowing ≠ Reassignment: Nếu đây là reassignment, config ở global scope phải bị thay đổi giá trị. Nhưng kiểm tra config sau khi setup() chạy vẫn là { env: "production", url: "prod.api.com" } — chứng tỏ không có mutation nào xảy ra. Đây là hai biến độc lập, không phải một biến bị ghi đè.

Fix (Sửa):

js
// ❌ `globalThis.config` không đúng với `const` ở global scope
function setup() {
  const config = { env: "staging", url: globalThis.config.url };

  function connect() {
    console.log(config.env, config.url);
  }

  connect();
}

Lý do: const config ở global scope của một module/file (ES module hoặc Node.js module) không tạo property trên globalThis. Cách fix hiện tại (globalThis.config.url) sẽ cho TypeError: Cannot read properties of undefined trong hầu hết environment hiện đại. Cần sửa thành pattern truyền tham số hoặc đổi tên biến — vừa đúng technical, vừa khớp với Prevention ("đặt tên rõ ràng").

✅ Nên dùng:

Truyền đầy đủ config qua tham số thay vì dựa vào outer scope.

js
function setup(globalConfig) {
  const localConfig = { env: "staging", url: globalConfig.url };

  function connect() {
    console.log(localConfig.env, localConfig.url);
  }

  connect();
}

setup(config); // truyền global config vào

Hoặc nếu muốn giữ nguyên signature:

js
function setup() {
  // Đổi tên biến local để tránh shadowing hoàn toàn
  const stagingConfig = { env: "staging", url: config.url };

  function connect() {
    console.log(stagingConfig.env, stagingConfig.url);
  }

  connect();
}

Prevention (Phòng ngừa): Khi dùng cùng tên biến ở nhiều scope, đặt tên rõ ràng (stagingConfig, globalConfig) để tránh shadowing gây confusion. Nếu cần merge object, làm explicit bằng spread operator hoặc Object.assign.

11. Design Exercise (Bài tập thiết kế) ​

Không áp dụng ở depth L2–L3. Design Exercise sẽ xuất hiện từ các lesson L5–L6 trở đi.

12. Production Scenario (Tình huống thực tế) ​

Context: Một ứng dụng có module cấu hình theo môi trường:

js
// config.js
const config = { timeout: 5000 };

// feature.js
function createFeature() {
  const config = { timeout: 1000 };

  function sendRequest() {
    return fetch("/api", { timeout: config.timeout });
  }

  return { sendRequest };
}

Symptom: Developer A sửa config.timeout = 8000 trong config.js nhưng sendRequest vẫn dùng 1000. Developer A nghi ngờ "import không hoạt động" hoặc "cache cũ".

Decision: Developer cần nhận ra config trong createFeature shadow config từ module import. Không có lỗi import hay cache. Đơn giản là hai biến cùng tên ở hai scope khác nhau.

Fix: Đổi tên biến local để tránh shadowing, hoặc dùng explicit parameter:

js
function createFeature(featureConfig) {
  function sendRequest() {
    return fetch("/api", { timeout: featureConfig.timeout });
  }
  return { sendRequest };
}

💡 Trade-off: Shadowing không phải lúc nào cũng xấu — nó giúp tái sử dụng tên biến ngắn gọn (config, data, item) trong scope nhỏ mà không ảnh hưởng outer scope. Tuy nhiên trong production, shadowing giữa module import và local variable là nguồn gốc của "ghost bugs" khó trace.

13. AI-Assisted Exercise (Bài tập với AI) ​

Level A — Ask

  1. Tự trả lời trước: Viết ra giấy: "Shadowing có làm thay đổi giá trị biến ngoài không? Tại sao inner() không nhìn thấy biến bị che?"
  2. Hỏi AI: Does shadowing in JavaScript overwrite the outer variable? Why can't an inner function see the outer variable if they have the same name?
  3. So sánh: AI có nói "shadowing modifies the outer variable" hoặc "the outer variable is hidden forever" không?
  4. Verify: Đối chiếu với MDN — Scope.

Gợi ý

Để ý xem AI có nhầm:

  • "Shadowing deletes the outer variable" (sai: chỉ che visibility).
  • "Inner function cannot access any outer variables if one is shadowed" (sai: chỉ che biến cùng tên, các biến khác vẫn nhìn thấy).
Đáp án tham khảo
  • Bạn nghĩ: Shadowing không thay đổi giá trị biến ngoài. Nó chỉ tạo một binding mới trong inner scope. Khi engine resolve identifier, nó dừng ở lần tìm thấy đầu tiên trên scope chain. Vì vậy inner() không "nhìn qua" được biến cùng tên ở outer scope — nhưng các biến khác ở outer scope vẫn nhìn thấy bình thường.
  • AI trả lời: Thường trả lời đúng rằng shadowing không overwrite, nhưng hay dùng từ mơ hồ như "the inner variable hides the outer one" hoặc "the outer variable is no longer accessible from the inner scope".
  • So sánh: AI thường không phân biệt rõ: outer variable vẫn accessible từ các scope khác (không bị shadow), và vẫn tồn tại trong memory. AI diễn đạt như thể shadowing làm biến mất hoàn toàn.
  • Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "the outer variable is hidden" mà không giải thích mechanism: lexical lookup stops at first match on scope chain. Điều này gây hiểu nhầm rằng shadowing là một cơ chế "active hiding" thay vì "lookup resolution order".
  • Kết luận: Nếu bạn chỉ ra được shadowing là kết quả của scope chain traversal (tìm từ trong ra ngoài, dừng ở lần đầu tiên), bạn đã hiểu sâu hơn AI. Điều này là prerequisite cho variable resolution (Lesson 1.2.6) và closure (Module 1.4).

14. Teach Back (Dạy lại) ​

Yêu cầu: Giải thích cho một junior dev trong 2 phút:

"Tại sao đoạn code này in ra 3 giá trị khác nhau mà không có dòng nào gán lại biến?"

js
const level = "global";

function outer() {
  const level = "outer";

  function inner() {
    const level = "inner";
    console.log(level);
  }

  inner();
  console.log(level);
}

outer();
console.log(level);

Dùng đúng terminology: nested scope, scope chain, shadowing, lexical lookup.

Mô phỏng
  • Bạn nói: Có ba biến level ở ba scope khác nhau: global, function outer, và function inner. JavaScript dùng lexical scope, nghĩa là engine tìm biến từ trong ra ngoài theo scope chain. Khi inner() chạy console.log(level), engine tìm trong scope của inner trước, thấy "inner" và dừng lại — không cần nhìn ra ngoài. Khi outer() chạy console.log(level), inner() đã return, engine tìm trong scope của outer, thấy "outer" và dừng. Ở global, engine thấy "global". Không có dòng nào gán lại vì đây là ba biến hoàn toàn riêng biệt, không phải một biến bị đổi giá trị. Hiện tượng này gọi là shadowing: biến trong che biến ngoài cùng tên.

💡 Hình dung như ba người tên Nam sống ở ba tầng chung cư. Khi gọi "Nam" từ tầng 3, bạn chỉ nghe thấy Nam ở tầng 3 — không phải vì Nam tầng 1 biến mất, mà vì bạn đứng ở tầng 3.

Gợi ý đánh giá bản thân

  • Đồng nghiệp có hiểu tại sao không phải "một biến bị đổi giá trị" không?
  • Bạn có thể dự đoán được nếu bỏ const level = "inner" thì inner() sẽ in ra gì không?
  • Nếu họ hỏi "vậy làm sao để inner() nhìn thấy level của outer khi đã shadow", bạn trả lời được không? (Gợi ý: đổi tên biến.)

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáNội dung
Giải thích nested scope là scope lồng nhauExplain (Giải thích)Dùng lời riêng mô tả cấu trúc scope lồng nhau và hướng lookup
Vẽ/mô tả scope chain 3–4 tầngImplementation LabCho code lồng nhau, vẽ scope graph và viết resolution path
Định nghĩa shadowingExplain (Giải thích)Giải thích shadowing bằng terminology scope chain
Dự đoán giá trị biến bị shadowPrediction Exercise (P1–P3)Cho 3 đoạn code nested, đoán output của mỗi console.log
Phân biệt shadowing và reassignment/mutationDebug LabTìm root cause "biến bị đổi" trong khi thực ra là shadowing; verify bằng cách kiểm tra global binding không đổi giá trị sau khi hàm chạy

16. Exit Criteria (Tiêu chí qua bài) ​

  • [ ] Có thể giải thích nested scope là các scope lồng nhau theo cấu trúc code.
  • [ ] Có thể vẽ/mô tả scope chain cho code có 3 tầng lồng nhau.
  • [ ] Có thể định nghĩa shadowing là "biến trong che biến ngoài cùng tên, không phải ghi đè".
  • [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
  • [ ] Có thể trace được lý do connect() dùng sai config trong Debug Lab bằng scope chain resolution.

17. Spiral Connection (Liên kết xoắn ốc) ​

Previous (Trước): Block Scope (Lesson 1.2.4) — hiểu scope bên trong { }.

Current (Hiện tại): Nested Scope & Shadowing — hiểu scope lồng nhau, scope chain, và shadowing.

Next (Tiếp theo):

  • Lesson 1.2.6 — Variable Resolution (cơ chế tìm biến trên scope chain)
  • Lesson 1.2.7 — Lexical Environment (formal model của scope chain)
  • Module 1.4 — Closure (scope chain + lifetime extension)
  • Stage 2 — Object Model (method shadowing, property lookup — cùng mechanism "first match", khác layer)
  • Stage 8 — React (prop shadowing, context resolution)
📴 Offline Mode — Content served from cache