Lesson 1.2.6 — Variable Resolution (Phân giải Biến)
0. Metadata
| Trường | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.2 — Scope & Lexical Environment |
| Lesson | 1.2.6 |
| Competency | C02 — JavaScript Runtime |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Lesson 1.2.1–1.2.5 — Scope, Global Scope, Function Scope, Block Scope, Nested Scope & Shadowing |
| Cognitive Load | Cao |
1. Why This Exists (Vì sao cần học)
Bạn viết một đoạn code tưởng chừng đơn giản:
function render() {
console.log(user);
}
const user = "alice";
render();Nó chạy. Nhưng sau đó bạn refactor:
function render() {
console.log(user);
}
render();
const user = "alice";Production Risk
ReferenceError: Cannot access 'user' before initialization. Nếu không hiểu variable resolution, developer sẽ fix bằng cách đổi const thành var hoặc di chuyển code một cách mù quáng — mà không hiểu tại sao engine "tìm thấy" user ở một scope nhưng lại "không cho dùng".
Variable resolution trả lời câu hỏi:
"Khi engine gặp một identifier, nó tìm ở đâu và theo thứ tự nào?"
Đây là lesson then chốt của Module 1.2. Mọi thứ bạn học từ scope, shadowing, đến closure đều dựa trên cơ chế này.
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).
- Scope lồng nhau và shadowing (Lesson 1.2.5).
let/constblock-scoped,varfunction-scoped (Lesson 1.2.3–1.2.4).
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Giải thích variable resolution là quá trình engine tìm identifier trên scope chain.
- Mô tả thuật toán: tìm từ current scope → outer → global →
ReferenceError. - Trace đường đi của variable lookup cho code lồng nhau 3–4 tầng.
- Dự đoán kết quả khi variable được tìm thấy (shadowing) hoặc không tìm thấy.
- Phân biệt
ReferenceError(không tìm thấy binding hoặc binding trong TDZ) và trường hợp đặc biệttypeof undeclaredVariabletrả về"undefined"mà không throw (preview).
4. Mental Model (Mô hình tư duy)
Variable Resolution = Leo Thang Tìm Kiếm (Climbing the Ladder)
Hãy hình dung scope chain như một cái thang:
Global Scope ← Bậc cao nhất
↑
Outer Function ← Bậc 2
↑
Inner Function ← Bậc 1
↑
Current Position ← Bậc 0 (nơi code đang đứng)Khi engine cần một biến:
- Nhìn xuống chân mình (current scope) — có không?
- Leo lên một bậc (outer scope) — có không?
- Leo tiếp cho đến khi hết thang.
- Hết thang rồi mà không có →
ReferenceError.
Quy tắc then chốt: Dừng lại ngay khi tìm thấy lần đầu tiên. Nếu bậc 0 đã có, không cần leo lên bậc 1. Đây chính là cơ chế của shadowing.
Sai lầm phổ biến
"Engine tìm biến từ trên xuống dưới (global trước)."
Sai hoàn toàn. Engine tìm từ trong ra ngoài (inside-out), không phải từ ngoài vào trong. Nếu tìm từ global trước, shadowing sẽ không bao giờ hoạt động.
5. Core Concepts (Các khái niệm cốt lõi)
| Loại | Nội dung |
|---|---|
| Essential (Bắt buộc) | Variable resolution, Scope chain traversal, Lookup direction (inside-out), First match wins, ReferenceError |
| Supporting (Hỗ trợ) | Resolution path notation, Scope chain visualization |
| Awareness (Biết tồn tại) | TDZ (tìm thấy binding nhưng chưa initialize → ReferenceError khác loại — Module 1.3) |
| Out of Scope (Không học ở đây) | Environment Record formal algorithm (Lesson 1.2.7), Closure capture mechanism (Module 1.4), this binding (Stage 2) |
6. Worked Example (Ví dụ phân tích từng bước)
Input:
const a = "global-a";
function outer() {
const b = "outer-b";
function middle() {
const c = "middle-c";
function inner() {
console.log(a, b, c, d);
}
inner();
}
middle();
}
outer();Step 1 — Observation: inner() cần resolve 4 identifier: a, b, c, d.
Step 2 — Classification: Variable resolution qua scope chain 4 tầng.
Step 3 — Reasoning — Resolution Path:
| Identifier | inner | middle | outer | Global | Kết quả |
|---|---|---|---|---|---|
c | ✅ "middle-c" | — | — | — | Tìm thấy ở bậc 0 |
b | ❌ | ❌ | ✅ "outer-b" | — | Leo 2 bậc, tìm thấy ở outer |
a | ❌ | ❌ | ❌ | ✅ "global-a" | Leo 3 bậc, tìm thấy ở global |
d | ❌ | ❌ | ❌ | ❌ | Hết thang → ReferenceError |
Step 4 — Conclusion: a, b, c được resolve thành công theo thứ tự từ trong ra ngoài. d không tồn tại trên toàn bộ scope chain nên throw ReferenceError: d is not defined.
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 Resolution
const x = 1;
function foo() {
const y = 2;
function bar() {
console.log(x, y);
}
bar();
}
foo();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
1 2.bar()không cóxvàylocal, nên leo scope chain: tìmyởfoo(1 bậc), tìmxở global (2 bậc). - Giải thích: Variable resolution luôn tìm từ trong ra ngoài.
barnhìn thấy cảxvàyvì scope chain cho phép.
P2 — Shadowing Stops Lookup
const name = "global";
function alpha() {
const name = "alpha";
function beta() {
const name = "beta";
console.log(name);
}
beta();
}
alpha();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
"beta".beta()cónamelocal, engine dừng ngay ở bậc 0 và không leo lênalphahay global. - Giải thích: "First match wins". Shadowing không phải là "ghi đè" mà là "tìm thấy sớm hơn trên đường đi". Engine không biết và không cần biết có
namekhác ở outer scope.
P3 — Broken Chain
function first() {
const secret = "123";
function second() {
function third() {
console.log(secret);
console.log(missing);
}
third();
}
second();
}
first();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
"123"rồi throwReferenceError: missing is not defined.secrettìm thấy ởfirst(2 bậc từthird).missingkhông có ở bất kỳ scope nào trên chain. - Giải thích: Scope chain được thiết lập lexically tại creation phase:
thirdliên kết đếnsecond,secondliên kết đếnfirst,firstliên kết đến global. Đường liên kết này luôn tồn tại bất kể execution path. Nếu identifier không tồn tại ở bất kỳ environment nào trên chuỗi liên kết đó, kết quả làReferenceError.
8. Implementation Lab (Bài lab thực hành)
Lab: Trace Variable Lookup (Guided → Independent)
Yêu cầu: Cho đoạn code sau, hãy viết resolution path cho từng identifier trong console.log.
const host = "api.example.com";
function createService() {
const endpoint = "/users";
function fetchData() {
const method = "GET";
function logRequest() {
console.log(method, endpoint, host, protocol);
}
logRequest();
}
fetchData();
}
createService();Nhiệm vụ:
- Vẽ scope chain dạng thang.
- Cho mỗi identifier (
method,endpoint,host,protocol), viết path:logRequest → ? → ? → ?. - Đoán kết quả cuối cùng.
[Đáp án tham khảo]
- Bạn nghĩ:
method:logRequest(bậc 0) →"GET".endpoint:logRequest❌ →fetchData❌ →createService✅"/users".host:logRequest❌ →fetchData❌ →createService❌ → Global ✅"api.example.com".protocol:logRequest❌ →fetchData❌ →createService❌ → Global ❌ →ReferenceError.
- Giải thích: Resolution path minh họa rõ "leo thang" từ trong ra ngoài.
endpointcần leo 2 bậc,hostcần 3 bậc.protocolkhông tồn tại ở đâu cả.
9. Edge Cases (Các trường hợp ngoại lệ)
EC-1: typeof với Undeclared Variable
console.log(typeof notDeclared);What fails? Nếu nghĩ mọi undeclared variable đều throw ReferenceError, bạn sẽ bối rối khi đoạn này trả về "undefined" mà không lỗi.
Why? typeof là toán tử đặc biệt: nếu operand là undeclared identifier, nó trả về "undefined" thay vì throw ReferenceError. Đây là ngoại lệ duy nhất trong ngôn ngữ. Mọi nơi khác dùng undeclared identifier đều throw ReferenceError.
EC-2: Tìm Thấy nhưng Không Dùng Được (TDZ Preview)
function demo() {
console.log(value);
let value = 10;
}
demo();What fails? Nếu nghĩ "tìm thấy là dùng được", bạn sẽ không hiểu tại sao đoạn này throw ReferenceError dù value rõ ràng được khai báo trong cùng function.
Why? Engine tìm thấy binding value trong scope của demo (không cần leo ra ngoài). Resolution thành công — engine biết chính xác binding nằm ở đâu. Nhưng binding đó nằm trong Temporal Dead Zone (TDZ): đã được tạo nhưng chưa được initialize. Vì vậy access bị từ chối. Kết quả là ReferenceError khác loại — không phải "not defined" mà là "cannot access before initialization". Điều này chứng minh resolution và access là hai bước riêng biệt. Chi tiết thuộc Module 1.3.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): ReferenceError: config is not defined xảy ra sau khi refactor code. Developer nghi ngờ "file import bị lỗi".
Reproduction (Tái hiện lỗi):
const config = { api: "prod" };
function init() {
const config = { api: "dev" };
function connect() {
console.log(confg.api);
}
connect();
}
init();Evidence (Bằng chứng): Stack trace chỉ vào dòng console.log(confg.api) trong connect().
Hypothesis (Giả thuyết): Developer tưởng rằng connect() sẽ nhìn thấy config từ init() hoặc global.
Verification (Xác minh): Kiểm tra chính tả identifier. confg !== config.
Root Cause (Nguyên nhân gốc rễ): Typo in identifier + Scope chain assumption. Engine thực hiện variable resolution cho confg: tìm trong connect → init → global → không tìm thấy. Scope chain không có typo-correction. Lỗi không phải vì scope bị gãy, mà vì identifier đơn giản không tồn tại.
Fix (Sửa):
console.log(config.api);Prevention (Phòng ngừa): Linter (ESLint no-undef) có thể bắt undeclared identifier trước khi chạy. Hiểu variable resolution giúp bạn nhận ra ngay: nếu ReferenceError, identifier không tồn tại trên bất kỳ scope nào trong chain.
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ó cấu trúc:
const settings = { theme: "dark" };
function setupDashboard() {
const settings = { theme: "light" };
function renderWidget() {
console.log(settings.theme);
}
renderWidget();
}
setupDashboard();Symptom: QA báo cáo widget hiển thị theme "light" dù user đã chọn "dark" ở global settings. Developer mất 30 phút tìm kiếm "ai đã đổi global settings".
Decision: Developer cần nhận ra renderWidget() không nhìn thấy global settings vì setupDashboard() đã shadow nó. Variable resolution dừng ở settings của setupDashboard, không bao giờ leo đến global.
Fix: Đổi tên biến local để tránh shadowing, hoặc truyền explicit parameter:
function setupDashboard(globalSettings) {
const widgetSettings = { ...globalSettings, size: "small" };
function renderWidget() {
console.log(widgetSettings.theme);
}
renderWidget();
}💡 Trade-off: Đổi tên (
widgetSettings) làm code explicit hơn nhưng dài hơn. Trong production, đây là trade-off đáng chấp nhận để tránh "ghost bugs" do shadowing gây ra.
13. AI-Assisted Exercise (Bài tập với AI)
Level A — Ask
- Tự trả lời trước: Viết ra giấy: "Engine tìm biến theo thứ tự nào? Tìm từ trong ra ngoài hay từ ngoài vào trong? Tại sao shadowing hoạt động được?"
- Hỏi AI:
How does JavaScript engine look up a variable? Does it search from the innermost scope outward or from global inward? - So sánh: AI có nói "engine searches all scopes and picks the closest" (mơ hồ) hay mô tả đúng mechanism "first match on scope chain"?
- Verify: Đối chiếu với ECMAScript Spec — ResolveBinding hoặc MDN — Scope.
Gợi ý
Để ý xem AI có nhầm:
- "JavaScript looks up variables from global scope first" (sai hoàn toàn).
- "If not found in local scope, it creates the variable globally" (sai: chỉ đúng với implicit global ở non-strict mode, không phải cơ chế chung).
Đáp án tham khảo
- Bạn nghĩ: Engine tìm từ current lexical environment (innermost) → outer environment reference → tiếp tục cho đến global → nếu hết chain thì
ReferenceError. Shadowing hoạt động vì engine dừng ngay khi tìm thấy binding đầu tiên, không cần biết có bao nhiêu binding cùng tên ở ngoài. - AI trả lời: Thường trả lời đúng rằng tìm từ trong ra ngoài, nhưng hay dùng từ mơ hồ như "JavaScript searches the scope chain" hoặc "it checks local scope first, then moves up" mà không giải thích "first match wins".
- So sánh: AI thường không nhấn mạnh điểm then chốt: scope chain traversal là strictly sequential and stops at first match. Điều này giải thích tại sao shadowing không phải overwrite và tại sao undeclared variable ở outer scope không bao giờ được nhìn thấy nếu inner scope có cùng tên (dù chưa initialize — TDZ).
- Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "if the variable is not found, JavaScript creates it in the global scope" — điều này chỉ đúng trong non-strict mode với assignment không khai báo (
x = 1), không phải cơ chế resolution chung. Resolution cho read operation (nhưconsole.log(x)) luôn dẫn đếnReferenceErrornếu không tìm thấy binding. Phân biệt này rất quan trọng để hiểu tại saotypeof xan toàn cònxmột mình thì không. - Kết luận: Nếu bạn chỉ ra được variable resolution là sequential lookup with early termination và phân biệt được nó với implicit global creation, bạn đã hiểu sâu hơn AI. Điều này là prerequisite trực tiếp cho Lexical Environment (Lesson 1.2.7) 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 lỗi ở dòng
console.log(y)? Engine đã tìm ở đâu?"
function outer() {
const x = 10;
function inner() {
const x = 20;
console.log(x);
console.log(y);
}
inner();
}
outer();Dùng đúng terminology: variable resolution, scope chain, lookup path, ReferenceError.
Mô phỏng
- Bạn nói: Khi
inner()chạyconsole.log(x), engine bắt đầu variable resolution: tìmxtrong scope củainnertrước. Tìm thấyx = 20, dừng lại ngay — không cần nhìn raouter. Nên in ra20. Khiinner()chạyconsole.log(y), engine lại tìm tronginner— không có. Leo lênouter— không có. Leo lên global — không có. Hết scope chain. Kết quả:ReferenceError: y is not defined. Lỗi không phải vìybị xóa hay scope bị gãy, mà đơn giản vìykhông tồn tại trên bất kỳ bậc nào của thang scope chain.
💡 Hình dung như tìm người trong tòa nhà: bạn kiểm tra phòng mình trước, rồi lần ra hành lang, rồi xuống tầng trệt. Nếu hết tòa nhà mà không thấy, bạn báo "không tìm thấy" — không phải vì người đó bị xóa khỏi thế giới.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao
xin ra20thay vì10không? - Bạn có thể dự đoán được nếu thêm
var y = 5ở global thì output thay đổi thế nào không? - Nếu họ hỏi "vậy
typeof yở đầuinner()có lỗi không", bạn trả lời được không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Nội dung |
|---|---|---|
| Giải thích variable resolution là gì | Explain (Giải thích) | Dùng lời riêng mô tả quá trình engine tìm biến |
| Mô tả thuật toán tìm từ trong ra ngoài | Prediction (Dự đoán) | Cho code lồng nhau, viết resolution path cho từng biến |
| Trace lookup path 3–4 tầng | Implementation Lab | Vẽ scope chain và viết path inner → middle → outer → global |
| Dự đoán shadowing vs not found | Prediction Exercise (P1–P3) | Cho 3 đoạn code, đoán output hoặc lỗi và giải thích path |
Phân biệt ReferenceError và typeof với undeclared | Edge Case | Giải thích tại sao typeof undeclaredVariable trả về "undefined" mà không throw, và phân biệt với ReferenceError khi đọc undeclared variable |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích variable resolution là quá trình "leo thang" từ current scope ra ngoài.
- [ ] Có thể mô tả thuật toán: current → outer → global →
ReferenceError. - [ ] Có thể trace resolution path cho code lồng nhau 3–4 tầng.
- [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
- [ ] Phân biệt được
ReferenceErrorkhi đọc undeclared variable và trường hợp đặc biệttypeof undeclaredVariabletrả về"undefined".
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Nested Scope & Shadowing (Lesson 1.2.5) — hiểu scope lồng nhau và biến trong che biến ngoài.
Current (Hiện tại): Variable Resolution — hiểu cơ chế engine tìm biến: từ trong ra ngoài, dừng ở lần đầu tiên.
Next (Tiếp theo):
- Lesson 1.2.7 — Lexical Environment (formal model của scope chain: Environment Record + Outer Reference)
- Lesson 1.2.8 — Scope vs Execution Context (phân biệt hai khái niệm then chốt)
- Module 1.3 — Hoisting & TDZ (tại sao tìm thấy binding nhưng vẫn lỗi)
- Module 1.4 — Closure (scope chain + lifetime extension = closure capture)
- Stage 8 — React (stale closure: resolution path đúng nhưng value là snapshot cũ)