Lesson 1.2.7 — Lexical Environment (Môi trường Từ vựng)
0. Metadata
| Trường | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.2 — Scope & Lexical Environment |
| Lesson | 1.2.7 |
| Competency | C02 — JavaScript Runtime |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Lesson 1.2.1–1.2.6 — Scope, Global Scope, Function Scope, Block Scope, Nested Scope, Variable Resolution |
| Cognitive Load | Cao |
1. Why This Exists (Vì sao cần học)
Bạn đã biết engine tìm biến từ trong ra ngoài (Lesson 1.2.6). Nhưng câu hỏi tiếp theo là:
"Engine biết 'ngoài' là ở đâu bằng cách nào?"
Khi một function được tạo ra bên trong function khác:
function outer() {
const secret = 42;
function inner() {
console.log(secret);
}
return inner;
}inner không chỉ là "đoạn code". Nó còn mang theo một tham chiếu đến môi trường nơi nó được sinh ra. Nếu không có khái niệm này, bạn không thể hiểu closure — một trong những mechanism quan trọng nhất của JavaScript.
Production Risk
Developer không hiểu lexical environment thường giải thích closure bằng những câu mơ hồ như "function nhớ scope của nó". Điều này đủ để vượt qua interview nhưng không đủ để debug stale closure trong React hoặc memory leak.
2. Prerequisites (Yêu cầu đầu vào)
Bạn cần đã biết:
- Scope chain và variable resolution từ trong ra ngoài (Lesson 1.2.5–1.2.6).
- Function scope và block scope (Lesson 1.2.3–1.2.4).
- Execution context là môi trường runtime (Module 1.1).
3. Learning Objectives (Mục tiêu học tập)
Sau lesson này, bạn có thể:
- Giải thích lexical environment là cấu trúc gồm 2 phần: environment record và outer reference.
- Mô tả outer environment reference là "đường link" tạo nên scope chain.
- Liên kết lexical environment với variable resolution đã học.
- Dự đoán outer reference của một function dựa trên vị trí nó được viết.
- Giải thích tại sao function "ghi nhớ" môi trường nơi nó được tạo ra.
4. Mental Model (Mô hình tư duy)
Lexical Environment = Bìa Hồ sơ 2 Ngăn
Hãy hình dung mỗi scope được đóng thành một bìa hồ sơ:
┌─────────────────────────────┐
│ Lexical Environment │
│ │
│ ┌─────────────────────┐ │
│ │ Environment Record │ │
│ │ (local bindings) │ │
│ │ • const x = 10 │ │
│ │ • function foo() │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ Outer Reference │ │
│ │ (link to parent) │ │
│ │ → outer environment │ │
│ └─────────────────────┘ │
└─────────────────────────────┘- Environment Record: Danh sách biến và hàm khai báo trực tiếp trong scope này.
- Outer Reference: Con trỏ trỏ về lexical environment của scope bên ngoài — chính là "đường link" tạo nên scope chain.
Khi engine cần resolve một biến, nó mở bìa hồ sơ hiện tại, kiểm tra Environment Record. Nếu không có, theo Outer Reference sang bìa hồ sơ cha. Lặp lại cho đến khi hết chain.
Sai lầm phổ biến
"Lexical environment chỉ tồn tại khi function đang chạy."
Sai một nửa. Structure của lexical environment (có những biến gì) được suy ra từ source code tại parse time. Outer reference (link đi đâu) được khóa cứng tại function creation time — tức thời điểm function object được tạo ra trong quá trình thực thi outer scope. Environment instance (các giá trị cụ thể) được tạo ra tại runtime (khi execution context được tạo).
5. Core Concepts (Các khái niệm cốt lõi)
| Loại | Nội dung |
|---|---|
| Essential (Bắt buộc) | Lexical Environment, Environment Record, Outer Environment Reference |
| Supporting (Hỗ trợ) | Lexical environment được xác định tại parse time, instance tại runtime |
| Awareness (Biết tồn tại) | Environment Record có 2 loại: declarative và object (chi tiết ở spec, không cần master) |
| Out of Scope (Không học ở đây) | Spec algorithm chi tiết, Closure mechanism đầy đủ (Module 1.4), this binding (Stage 2) |
6. Worked Example (Ví dụ phân tích từng bước)
Input:
const rate = 1.1;
function calculate(base) {
const tax = 0.1;
function total() {
return base * (1 + tax) * rate;
}
return total;
}
const getTotal = calculate(100);
console.log(getTotal());Step 1 — Observation: calculate(100) tạo execution context. Bên trong có tax và total. total được return ra ngoài và gán cho getTotal. Sau đó getTotal() được gọi.
Step 2 — Classification: Cần hiểu lexical environment của total để biết nó tìm base, tax, rate ở đâu sau khi calculate đã return.
Step 3 — Reasoning — Lexical Environment Chain:
Global Lexical Environment
├── rate = 1.1
└── calculate() Lexical Environment
├── base = 100
├── tax = 0.1
└── total() Lexical Environment
├── (không có local binding nào)
└── Outer → calculate()Khi getTotal() (tức total()) chạy:
| Identifier | Local total | calculate (via Outer) | Global (via Outer→Outer) | Kết quả |
|---|---|---|---|---|
base | ❌ | ✅ 100 | — | 100 |
tax | ❌ | ✅ 0.1 | — | 0.1 |
rate | ❌ | ❌ | ✅ 1.1 | 1.1 |
Step 4 — Conclusion: getTotal() trả về 100 * 1.1 * 1.1 = 121. Điểm then chốt: total "mang theo" outer reference trỏ về lexical environment của calculate, nên vẫn có thể tìm thấy base và tax dù calculate đã return.
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 — Outer Reference Direction
const a = "global";
function outer() {
const a = "outer";
function inner() {
console.log(a);
}
inner();
}
outer();[Đáp án & Giải thích]
- Bạn nghĩ: In ra
"outer".innerđược tạo bên trongouter, nên outer reference củainnertrỏ về lexical environment củaouter. Khi resolvea, engine tìm tronginner(không có), theo outer reference sangouter(tìm thấy"outer"), dừng lại. - Giải thích: Outer reference được xác định bởi vị trí viết code (lexical), không phải vị trí gọi.
innerluôn trỏ vềoutervì nó được viết bên trongouter.
P2 — Function Created in Global
const x = 1;
function foo() {
const x = 2;
function bar() {
console.log(x);
}
return bar;
}
function baz(fn) {
const x = 3;
fn();
}
baz(foo());[Đáp án & Giải thích]
- Bạn nghĩ: In ra
2.foo()returnbar.barđược tạo bên trongfoo, nên outer reference củabartrỏ về lexical environment củafoo(nơix = 2). Khibazgọifn()(tứcbar),barvẫn tìmxtheo outer reference của nó — vềfoo, không phải vềbaz(nơix = 3). - Giải thích: Đây là điểm then chốt của lexical scope: outer reference được khóa cứng tại thời điểm tạo function, không thay đổi khi truyền function sang chỗ khác để gọi.
P3 — Missing Outer Link
function standalone() {
console.log(mystery);
}
standalone();[Đáp án & Giải thích]
- Bạn nghĩ:
ReferenceError: mystery is not defined.standaloneđược tạo ở global scope, nên outer reference của nó trỏ về global lexical environment.mysterykhông tồn tại trong global environment record. - Giải thích: Outer reference của function được tạo ở global luôn trỏ về global lexical environment. Nếu global không có biến, chain kết thúc và throw
ReferenceError.
8. Implementation Lab (Bài lab thực hành)
Lab: Map Lexical Environments (Guided → Independent)
Yêu cầu: Cho đoạn code sau, hãy vẽ/mô tả lexical environment chain và cho biết result cuối cùng.
const multiplier = 2;
function createMultiplier(base) {
const offset = 10;
function apply() {
return base * multiplier + offset;
}
return apply;
}
const doublePlusTen = createMultiplier(5);
const result = doublePlusTen();Nhiệm vụ:
- Liệt kê 3 lexical environments (global, createMultiplier, apply).
- Cho mỗi environment, viết environment record và outer reference.
- Trace variable resolution cho
base,multiplier,offsetkhidoublePlusTen()chạy.
[Đáp án tham khảo]
Bạn nghĩ:
- Global Environment: Record
{ multiplier: 2, createMultiplier: fn }, Outer:null - createMultiplier Environment: Record
{ base: 5, offset: 10, apply: fn }, Outer: Global - apply Environment: Record
{}(không có local binding), Outer: createMultiplier
Resolution khi
apply()chạy:base: apply ❌ → createMultiplier ✅5multiplier: apply ❌ → createMultiplier ❌ → Global ✅2offset: apply ❌ → createMultiplier ✅10
Result:
5 * 2 + 10 = 20- Global Environment: Record
Giải thích:
applycó outer reference trỏ vềcreateMultipliervì nó được viết bên trongcreateMultiplier. Điều này cho phépapplytruy cậpbasevàoffsetdùcreateMultiplierđã return.
9. Edge Cases (Các trường hợp ngoại lệ)
EC-1: Arrow Function và Lexical Environment
const outer = 1;
const arrow = () => {
console.log(outer);
};
function normal() {
console.log(outer);
}
arrow();
normal();What fails? Nếu nghĩ arrow function không có lexical environment, bạn sẽ hiểu nhầm cách nó resolve biến.
Why? Arrow function có lexical environment giống mọi function khác. Điểm khác biệt của arrow function là nó không tạo binding this riêng (Stage 2), không phải không có lexical environment. Arrow function vẫn có environment record và outer reference đầy đủ.
EC-2: eval và Dynamic Scope (Awareness)
const x = 1;
function test() {
const x = 2;
eval("console.log(x)");
}
test();What fails? eval("console.log(x)") in ra 2, không phải 1. Điều này có vẻ đi ngược lại lexical scope.
Why? eval chạy code trong current lexical environment của nó và có thể tạo binding mới trong environment đó. Đây là ngoại lệ hiếm hoi cho phép "đục thủng" lexical scope tĩnh. Trong strict mode, eval vẫn hoạt động nhưng tạo lexical environment riêng để cô lập binding, tránh leak ra outer scope. Vì lý do này, eval được khuyến cáo tránh dùng trong production.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): Developer refactor code và một callback đột nhiên đọc sai giá trị. Họ nghi ngờ "biến bị đổi sau khi return".
Reproduction (Tái hiện lỗi):
const config = { env: "prod" };
function createLogger() {
const config = { env: "dev" };
function log() {
console.log("Env:", config.env);
}
return log;
}
const logger = createLogger();
logger();Evidence (Bằng chứng): Console in ra "Env: dev" thay vì "Env: prod".
Hypothesis (Giả thuyết): Developer mong đợi log() nhìn thấy global config vì createLogger đã return và "không còn tồn tại".
Verification (Xác minh): Đổi tên const config trong createLogger thành localConfig — log() sẽ in ra "Env: prod" vì không còn bị shadow.
Root Cause (Nguyên nhân gốc rễ): Lexical environment reference + shadowing. log được tạo bên trong createLogger, nên outer reference của log trỏ về lexical environment của createLogger (nơi có config = { env: "dev" }). Khi log() chạy, engine theo outer reference và tìm thấy config ở đó trước khi leo lên global. Developer nhầm lẫn giữa "function đã return" (execution context pop) và "lexical environment vẫn được giữ lại" (closure — Module 1.4).
Fix (Sửa):
function createLogger(globalConfig) {
const localConfig = { env: "dev", ...globalConfig };
function log() {
console.log("Env:", localConfig.env);
}
return log;
}Hoặc đổi tên để tránh shadowing:
function createLogger() {
const devConfig = { env: "dev" };
function log() {
console.log("Env:", devConfig.env);
}
return log;
}Prevention (Phòng ngừa): Tránh dùng cùng tên biến ở inner scope với outer scope nếu inner function sẽ được return hoặc pass ra ngoài. Hiểu lexical environment giúp bạn dự đoán được outer reference trỏ về đâu.
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 vùng địa lý:
const region = "US";
function createPaymentProcessor() {
const region = "EU";
function process(amount) {
const fee = region === "EU" ? 0.02 : 0.05;
return amount * (1 + fee);
}
return process;
}
const processor = createPaymentProcessor();
console.log(processor(100));Symptom: processor(100) trả về 102 (fee EU) dù team operations đã đổi global region = "US" và mong đợi fee US.
Decision: Developer cần nhận ra process có outer reference trỏ về lexical environment của createPaymentProcessor, không phải global. Global region bị shadow bởi region trong createPaymentProcessor.
Fix: Truyền region qua tham số hoặc đổi tên biến local để explicit:
function createPaymentProcessor(regionCode) {
const fee = regionCode === "EU" ? 0.02 : 0.05;
function process(amount) {
return amount * (1 + fee);
}
return process;
}💡 Trade-off: Biến
feeđược "bắt" (capture) trong lexical environment màprocesstham chiếu đến (tức environment củacreatePaymentProcessor). Đây là behavior đúng của closure, nhưng nếufeecần thay đổi theo thời gian, bạn sẽ gặp stale closure (Module 1.4).
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: "Lexical environment gồm những gì? Outer reference được xác định khi nào và dựa trên cái gì?"
- Hỏi AI:
What is a lexical environment in JavaScript? When is the outer reference determined? - So sánh: AI có nói "outer reference is determined at runtime when the function is called" không?
- Verify: Đối chiếu với MDN — Closure hoặc ECMAScript Spec — Lexical Environment.
Gợi ý
Để ý xem AI có nhầm:
- "Outer reference is set when the function is called" (sai: được xác định khi function được tạo trong quá trình thực thi outer scope, không phải khi được gọi).
- "Lexical environment only exists during function execution" (sai: outer reference được khóa tại creation time, instance có thể tồn tại sau khi function return nhờ closure).
Đáp án tham khảo
- Bạn nghĩ: Lexical environment gồm 2 phần: Environment Record (danh sách local bindings) và Outer Environment Reference (link đến environment cha). Outer reference được xác định tại function creation time (khi function object được tạo ra trong quá trình thực thi outer scope), dựa trên vị trí lexical của function trong source code. Nó không thay đổi khi function được truyền đi hay gọi ở chỗ khác.
- AI trả lời: Thường trả lời đúng rằng lexical environment liên quan đến scope, nhưng hay dùng từ mơ hồ như "it stores variables" hoặc "it points to the parent scope" mà không phân biệt parse time vs runtime.
- So sánh: AI thường không nhấn mạnh điểm then chốt: outer reference là immutable link được khóa tại creation time. Điều này giải thích tại sao callback vẫn "nhớ" scope cũ dù được gọi từ scope mới.
- Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "a function remembers the scope where it was created" — đúng nhưng quá mơ hồ. Cụ thể hơn: function lưu một reference đến lexical environment của parent scope tại thời điểm định nghĩa. Đây là cơ chế concrete, không phải "trí nhớ" magic.
- Kết luận: Nếu bạn chỉ ra được lexical environment là cấu trúc 2 phần cụ thể (record + outer ref) và outer ref được khóa cứng tại parse time, bạn đã hiểu sâu hơn AI. Điều này là prerequisite trực tiếp cho Closure (Module 1.4) và stale closure debugging (Stage 8).
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 function
logvẫn đọc đượcuserdùcreateLoggerđã return từ lâu?"
function createLogger(user) {
function log() {
console.log("User:", user);
}
return log;
}
const logAlice = createLogger("alice");
logAlice();Dùng đúng terminology: lexical environment, environment record, outer reference, scope chain.
Mô phỏng
- Bạn nói: Khi
createLoggerchạy, nó tạo ra một lexical environment mới. Trong environment record của nó cóuservàlog.loglà một function, và khi JavaScript tạo function, nó gắn cho function đó một outer reference trỏ về lexical environment hiện tại — tức là environment củacreateLogger. KhicreateLoggerreturn, execution context của nó bị pop khỏi call stack, nhưng lexical environment vẫn còn trong memory vìlogvẫn giữ reference đến nó. KhilogAlice()chạy, engine mở lexical environment củalog(trống), không thấyuser, theo outer reference sang environment củacreateLogger, tìm thấyuser = "alice". Đó là lý dologvẫn đọc đượcuserdùcreateLoggerđã kết thúc.
💡 Hình dung như bạn mượn một cuốn sổ địa chỉ từ văn phòng cũ. Văn phòng đã đóng cửa (execution context pop), nhưng bạn vẫn giữ bản photo trang địa chỉ đó (outer reference trỏ về lexical environment) — và vẫn có thể tra cứu khi cần.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao không phải "function có trí nhớ ma thuật" không?
- Bạn có thể dự đoán được nếu
createLoggerđược gọi 2 lần với 2 user khác nhau thì có 2 lexical environment riêng biệt không? - Nếu họ hỏi "vậy lexical environment có bị xóa khi nào", bạn trả lời được không? (Gợi ý: khi không còn reference nào trỏ đến — garbage collection, Stage 11.)
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Nội dung |
|---|---|---|
| Giải thích lexical environment gồm 2 phần | Explain (Giải thích) | Dùng lời riêng mô tả Environment Record và Outer Reference |
| Mô tả outer reference tạo scope chain | Prediction (Dự đoán) | Cho code lồng nhau, vẽ/mô tả lexical environment chain |
| Liên kết lexical environment với resolution | Implementation Lab | Trace variable resolution qua environment record và outer ref |
| Dự đoán outer reference dựa trên vị trí viết | Prediction Exercise (P1–P3) | Cho function được truyền đi, đoán biến nó nhìn thấy |
| Giải thích function "ghi nhớ" môi trường | Teach Back | Giải thích callback vẫn truy cập outer variable sau khi outer return |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích lexical environment gồm Environment Record (local bindings) và Outer Reference (link to parent).
- [ ] Có thể mô tả outer reference là "đường link" tạo nên scope chain.
- [ ] Có thể vẽ/mô tả lexical environment chain cho code có 3 tầng lồng nhau.
- [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
- [ ] Giải thích được tại sao function vẫn truy cập được outer variable sau khi outer function return.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Variable Resolution (Lesson 1.2.6) — hiểu engine tìm biến từ trong ra ngoài.
Current (Hiện tại): Lexical Environment — hiểu cấu trúc cho phép tìm kiếm đó: environment record + outer reference.
Next (Tiếp theo):
- Lesson 1.2.8 — Scope vs Execution Context (phân biệt hai khái niệm then chốt)
- Module 1.4 — Closure (lexical environment + lifetime extension = closure)
- Stage 2 — Object Model (method lexical environment và
thisbinding khác nhau)- Stage 8 — React (stale closure: lexical environment đúng nhưng giá trị là snapshot)
- Stage 11 — Memory (lexical environment retention và garbage collection)