Skip to content

Lesson 1.2.7 — Lexical Environment (Môi trường Từ vựng) ​

Bài 1.2.7 — Lexical Environment
Environment Records, Outer References & Identifier Resolution • 25 phút
0:00 / 0:00

0. Metadata ​

TrườngGiá trị
Stage1 — JavaScript Execution Model
Module1.2 — Scope & Lexical Environment
Lesson1.2.7
CompetencyC02 — JavaScript Runtime
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.2.1–1.2.6 — Scope, Global Scope, Function Scope, Block Scope, Nested Scope, Variable Resolution
Cognitive LoadCao

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:

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

  1. Giải thích lexical environment là cấu trúc gồm 2 phần: environment record và outer reference.
  2. Mô tả outer environment reference là "đường link" tạo nên scope chain.
  3. Liên kết lexical environment với variable resolution đã học.
  4. Dự đoán outer reference của một function dựa trên vị trí nó được viết.
  5. 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ơ:

text
┌─────────────────────────────┐
│  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ạiNộ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:

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

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

IdentifierLocal totalcalculate (via Outer)Global (via Outer→Outer)Kết quả
base❌✅ 100—100
tax❌✅ 0.1—0.1
rate❌❌✅ 1.11.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 ​

js
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 trong outer, nên outer reference của inner trỏ về lexical environment của outer. Khi resolve a, engine tìm trong inner (không có), theo outer reference sang outer (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. inner luôn trỏ về outer vì nó được viết bên trong outer.

P2 — Function Created in Global ​

js
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() return bar. bar được tạo bên trong foo, nên outer reference của bar trỏ về lexical environment của foo (nơi x = 2). Khi baz gọi fn() (tức bar), bar vẫn tìm x theo outer reference của nó — về foo, không phải về baz (nơi x = 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.
js
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. mystery khô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.

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

  1. Liệt kê 3 lexical environments (global, createMultiplier, apply).
  2. Cho mỗi environment, viết environment record và outer reference.
  3. Trace variable resolution cho base, multiplier, offset khi doublePlusTen() 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 ✅ 5
    • multiplier: apply ❌ → createMultiplier ❌ → Global ✅ 2
    • offset: apply ❌ → createMultiplier ✅ 10

    Result: 5 * 2 + 10 = 20

  • Giải thích: apply có outer reference trỏ về createMultiplier vì nó được viết bên trong createMultiplier. Điều này cho phép apply truy cập base và offset dù createMultiplier đã return.

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

EC-1: Arrow Function và Lexical Environment ​

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

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

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

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

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

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

js
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à process tham chiếu đến (tức environment của createPaymentProcessor). Đây là behavior đúng của closure, nhưng nếu fee cầ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

  1. 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ì?"
  2. Hỏi AI: What is a lexical environment in JavaScript? When is the outer reference determined?
  3. So sánh: AI có nói "outer reference is determined at runtime when the function is called" không?
  4. 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 log vẫn đọc được user dù createLogger đã return từ lâu?"

js
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 createLogger chạy, nó tạo ra một lexical environment mới. Trong environment record của nó có user và log. log là 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ủa createLogger. Khi createLogger return, execution context của nó bị pop khỏi call stack, nhưng lexical environment vẫn còn trong memory vì log vẫn giữ reference đến nó. Khi logAlice() chạy, engine mở lexical environment của log (trống), không thấy user, theo outer reference sang environment của createLogger, tìm thấy user = "alice". Đó là lý do log vẫn đọc được user dù 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ầnExplain (Giải thích)Dùng lời riêng mô tả Environment Record và Outer Reference
Mô tả outer reference tạo scope chainPrediction (Dự đoán)Cho code lồng nhau, vẽ/mô tả lexical environment chain
Liên kết lexical environment với resolutionImplementation LabTrace variable resolution qua environment record và outer ref
Dự đoán outer reference dựa trên vị trí viếtPrediction 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ườngTeach BackGiả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à this binding 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)
📴 Offline Mode — Content served from cache