Skip to content

Lesson 1.2.8 — Scope vs Execution Context (Phạm vi vs Ngữ cảnh Thực thi) ​

Bài 1.2.8 — Scope vs Execution Context
Lexical Structure, Runtime Context & Mental Model Boundaries • 26 phút
0:00 / 0:00

0. Metadata ​

TrườngGiá trị
Stage1 — JavaScript Execution Model
Module1.2 — Scope & Lexical Environment
Lesson1.2.8
CompetencyC02 — JavaScript Runtime
DepthL2–L3 (Explain → Use)
PrerequisitesModule 1.1 (Execution Context), Lesson 1.2.1–1.2.7 (Scope, Variable Resolution, Lexical Environment)
Cognitive LoadCao

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

Bạn đang debug một hàm được gọi hai lần:

js
function counter() {
  let count = 0;
  count++;
  console.log(count);
}

counter();
counter();

Console in ra 1 rồi 1. Một junior dev hỏi: "Tại sao count không tăng lên 2? Cùng một function, cùng scope, sao biến không giữ lại giá trị?"

Câu trả lời nằm ở sự phân biệt then chốt:

Scope là "blueprint" (bản thiết kế). Execution Context là "công trường" (phiên xây dựng).

Mỗi lần gọi counter() tạo ra một công trường mới dựa trên cùng một bản thiết kế. Biến count được tạo mới trên mỗi công trường, không chia sẻ giữa các lần gọi.

Production Risk

Khi developer nhầm lẫn scope và execution context, họ:

  • Tưởng rằng biến local "dùng chung" giữa các lần gọi function.
  • Dùng "scope" và "execution context" thay thế cho nhau trong code review, gây hiểu nhầm.
  • Không hiểu tại sao closure "giữ lại" được giá trị (vì họ chưa phân biệt scope structure và EC instance).

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

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

  • Execution Context là gì, creation phase vs execution phase (Module 1.1).
  • Scope là vùng nhìn thấy, scope chain (Lesson 1.2.1, 1.2.5).
  • Lexical Environment gồm Environment Record và Outer Reference (Lesson 1.2.7).

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

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

  1. Phân biệt Scope (static visibility) và Execution Context (runtime instance).
  2. Giải thích Lexical Environment là cấu trúc nối giữa scope và execution context.
  3. Dự đoán một biến local có được chia sẻ giữa các lần gọi function hay không.
  4. Phân biệt Scope Chain (tìm biến) và Call Stack (thứ tự thực thi).
  5. Không dùng "scope" và "execution context" thay thế cho nhau trong giải thích.

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

4 Khái niệm, 4 Câu hỏi

Khái niệmTrả lời câu hỏiTính chất
ScopeVariable có thể được nhìn thấy từ đâu?Static (xác định lúc viết code)
Execution ContextCode hiện tại đang được thực thi trong context nào?Dynamic (tạo ra lúc chạy)
Lexical EnvironmentEnvironment lưu bindings + liên kết outerStructure (scope) + Instance (EC)
Call StackContext nào đang thực thi trước context nào?Dynamic (thứ tự runtime)

Analogy:

  • Scope = Bản thiết kế nhà (blueprint). Nói rõ phòng nào nhìn thấy phòng nào. Không đổi dù xây 1 hay 100 căn.
  • Execution Context = Công trường xây nhà. Mỗi lần xây một căn = một EC. Có vật liệu riêng (bindings), có quy trình riêng (creation → execution).
  • Lexical Environment = Danh sách vật liệu + bản đồ liên kết phòng. Tồn tại trong mỗi EC nhưng cấu trúc được vẽ từ blueprint.
  • Call Stack = Thứ tự thi công. Căn này đang xây dở, rồi đến căn kia, rồi quay lại.

Sai lầm phổ biến

"Scope và Execution Context là một."

Sai. Một function có một scope (blueprint) nhưng có thể tạo ra vô số execution contexts (mỗi lần gọi một cái). Scope quyết định nhìn thấy gì. Execution context quyết định đang chạy cái gì, với giá trị nào, ở thời điểm nào.

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

LoạiNội dung
Essential (Bắt buộc)Scope (static), Execution Context (dynamic), Lexical Environment (bridge), Call Stack (order)
Supporting (Hỗ trợ)Mỗi EC có một lexical environment instance; cùng scope có thể có nhiều EC
Awareness (Biết tồn tại)this binding thuộc EC, không thuộc scope (Stage 2)
Out of Scope (Không học ở đây)Closure mechanism đầy đủ (Module 1.4), this deep dive (Stage 2), V8 internals (Stage 11)

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

Input:

js
const rate = 1.1;

function compute(base) {
  const tax = 0.1;
  return base * (1 + tax) * rate;
}

compute(100);
compute(200);

Step 1 — Observation: compute được gọi 2 lần. Cùng một function, cùng scope.

Step 2 — Classification: Cần phân biệt scope (một) và execution contexts (hai).

Step 3 — Reasoning:

text
BLUEPRINT (Scope — 1 cái duy nhất)
==================================
compute: parameters [base], locals [tax], outer [global]

CÔNG TRƯỜNG 1 (EC — lần gọi thứ nhất)
======================================
Lexical Environment Instance
├── base = 100
├── tax = 0.1
└── outer → global
Result: 100 * 1.1 * 1.1 = 121

CÔNG TRƯỜNG 2 (EC — lần gọi thứ hai)
======================================
Lexical Environment Instance
├── base = 200
├── tax = 0.1
└── outer → global
Result: 200 * 1.1 * 1.1 = 242

Step 4 — Conclusion: Hai lần gọi tạo ra hai EC độc lập. Mỗi EC có lexical environment instance riêng chứa base và tax. Scope (blueprint) giống nhau, nhưng giá trị cụ thể (instance) khác nhau. tax được tạo mới trên mỗi EC, không chia sẻ.

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 — Scope giống nhau, EC khác nhau ​

js
function ticket() {
  let number = 0;
  number++;
  return number;
}

console.log(ticket());
console.log(ticket());
[Đáp án & Giải thích]
  • Bạn nghĩ: In ra 1 rồi 1. Mỗi lần gọi ticket() tạo một execution context mới. Trong mỗi EC, number được khởi tạo là 0 rồi tăng lên 1. Cùng scope (blueprint) nhưng khác EC (công trường), nên biến không chia sẻ.
  • Giải thích: Scope quyết định number là local và chỉ nhìn thấy trong ticket. Nhưng execution context quyết định mỗi lần gọi có một number riêng biệt.

P2 — Scope Chain vs Call Stack ​

js
function a() {
  const x = "a";
  b();
  console.log(x);
}

function b() {
  const x = "b";
  c();
}

function c() {
  console.log(x);
}

const x = "global";
a();
[Đáp án & Giải thích]
  • Bạn nghĩ: In ra "global" rồi "a". c() console x — c được viết ở global scope, nên scope chain của c là c → global. Nó tìm thấy x = "global". Call stack lúc đó là global → a → b → c, nhưng call stack không ảnh hưởng scope chain. Sau đó a() console x — a tìm thấy x = "a" local.
  • Giải thích: Đây là điểm then chốt: scope chain (lexical) ≠ call stack (runtime). c được gọi từ b, nhưng c không nhìn thấy x của b vì c được viết ở global. Đừng nhầm "được gọi từ đâu" với "nhìn thấy gì".

P3 — EC pop nhưng Lexical Environment vẫn còn (Preview Closure) ​

js
function makeCounter() {
  let count = 0;

  function increment() {
    count++;
    console.log(count);
  }

  return increment;
}

const counter = makeCounter();
counter();
counter();
[Đáp án & Giải thích]
  • Bạn nghĩ: In ra 1 rồi 2. makeCounter() tạo một EC, bên trong có count = 0 và increment. increment có outer reference trỏ về lexical environment của makeCounter. Khi makeCounter return, EC bị pop khỏi call stack, nhưng lexical environment vẫn được giữ lại trong memory vì counter (tức increment) vẫn trỏ đến nó. Mỗi lần gọi counter(), một EC mới cho increment được tạo, nhưng outer reference vẫn trỏ về cùng một lexical environment cũ — nên count được chia sẻ giữa các lần gọi.
  • Giải thích: Đây là sự khác biệt giữa EC (tạo mới mỗi lần gọi) và lexical environment instance (có thể tồn tại lâu hơn EC nếu được reference giữ lại). Đây chính là mechanism của closure (Module 1.4).

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

Lab: Trace EC + Scope Together (Guided → Independent) ​

Yêu cầu: Cho đoạn code sau, hãy mô tả:

  1. Có bao nhiêu scope (blueprint)?
  2. Có bao nhiêu execution context được tạo ra?
  3. Vẽ call stack tại thời điểm console.log(a, b, c).
  4. Vẽ scope chain của console.log(a, b, c).
js
const a = "global-a";

function outer() {
  const b = "outer-b";

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

  inner();
}

outer();
[Đáp án tham khảo]
  • Bạn nghĩ:

    • Scope (blueprint): 3 — Global, outer, inner.
    • Execution contexts created: 3 — Global EC, outer() EC, inner() EC.
    • Call stack tại console.log: global → outer → inner (inner đang chạy ở top).
    • Scope chain của console.log: inner → outer → global (lexical, không đổi).
    IdentifierScope Chain PathValue
    cinner (local)"inner-c"
    binner → outer"outer-b"
    ainner → outer → global"global-a"
  • Giải thích: Call stack và scope chain là hai cấu trúc khác nhau. Call stack nói "ai đang chạy". Scope chain nói "biến nào nhìn thấy". Trong ví dụ này chúng trùng hướng, nhưng P2 đã chứng minh chúng có thể khác nhau hoàn toàn.

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

EC-1: this thuộc EC, không thuộc Scope ​

js
const obj = {
  name: "Alice",
  greet: function () {
    console.log(this.name);
  },
};

const fn = obj.greet;
fn();

What fails? Nếu nghĩ this được resolve theo scope chain (lexical), bạn sẽ bối rối khi fn() in ra undefined (hoặc lỗi) thay vì "Alice".

Why? this là binding của execution context, không phải identifier được resolve theo scope chain. Khi obj.greet() được gọi, this là obj. Khi fn() được gọi (detached), this là global object (non-strict browser) hoặc undefined (strict mode / Node.js). Điều này thuộc Stage 2 (Object Model & this).

EC-2: eval tạo EC nhưng không tạo Scope mới ​

js
const x = 1;

function test() {
  const x = 2;
  eval("console.log(x); var y = 3;");
  console.log(typeof y);
}

test();

What fails? eval chạy trong EC hiện tại. var y = 3 bên trong eval vẫn thuộc function scope của test, không phải block scope mới.

Why? eval chạy code trong current lexical environment của nó và có thể tạo binding mới trong environment đó. var y bên trong eval vẫn thuộc function scope của test, không phải block scope mới. 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 báo cáo "biến bị reset" khi gọi hàm nhiều lần. Họ mong đợi biến local giữ lại giá trị giữa các lần gọi.

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

js
function accumulate(value) {
  let total = 0;
  total += value;
  return total;
}

console.log(accumulate(5));
console.log(accumulate(10));

Evidence (Bằng chứng): Console in ra 5 rồi 10, không phải 5 rồi 15.

Hypothesis (Giả thuyết): Developer tưởng rằng vì total được khai báo trong accumulate, và accumulate có "cùng một scope", nên total sẽ được chia sẻ giữa các lần gọi.

Verification (Xác minh): Thêm console.log trong accumulate để xác nhận total luôn bắt đầu từ 0.

Root Cause (Nguyên nhân gốc rễ): Nhầm lẫn Scope và Execution Context. Developer nghĩ "cùng scope = cùng biến". Thực tế, mỗi lần gọi accumulate() tạo một execution context mới, và mỗi EC có lexical environment instance riêng chứa total = 0. Scope (blueprint) chỉ nói rằng total là local — nó không nói rằng total được chia sẻ giữa các lần gọi.

Fix (Sửa): Nếu cần giữ lại state giữa các lần gọi, dùng closure (Module 1.4) hoặc global/module variable:

js
function createAccumulator() {
  let total = 0;

  return function (value) {
    total += value;
    return total;
  };
}

const accumulate = createAccumulator();
console.log(accumulate(5)); // 5
console.log(accumulate(10)); // 15

Prevention (Phòng ngừa): Khi thấy biến local "bị reset", đừng nghĩ scope sai. Hãy hỏi: "Tôi có cần giữ lại state giữa các lần gọi không?" Nếu có, bạn cần mechanism khác (closure, object, global) — không phải "sửa scope".

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 hệ thống xử lý request HTTP:

js
function handleRequest(requestId) {
  const startTime = Date.now();

  function logDuration() {
    const duration = Date.now() - startTime;
    console.log(`Request ${requestId}: ${duration}ms`);
  }

  processData(logDuration);
}

handleRequest("A");
handleRequest("B");

Symptom: Khi request B bắt đầu, developer thấy startTime của A vẫn tồn tại trong memory (qua heap snapshot). Họ nghi ngờ "scope bị leak".

Decision: Developer cần phân biệt: mỗi handleRequest() tạo một EC riêng với lexical environment chứa startTime và logDuration. Nếu processData giữ lại logDuration (ví dụ: callback async), lexical environment của handleRequest không được garbage collect dù EC đã pop. Đây không phải "scope bị leak" mà là closure retaining environment — behavior đúng nhưng cần quản lý.

Fix: Đảm bảo callback được cleanup sau khi chạy:

js
function handleRequest(requestId) {
  const startTime = Date.now();

  function logDuration() {
    const duration = Date.now() - startTime;
    console.log(`Request ${requestId}: ${duration}ms`);
  }

  processData(logDuration, () => {
    // cleanup reference if possible
  });
}

💡 Trade-off: Closure là feature mạnh nhưng cần awareness về memory retention. Trong production, hãy đảm bảo callbacks không được giữ lại vô thời hạn (ví dụ: event listeners không remove). Chi tiết thuộc Stage 11 (Memory).

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: "Scope khác Execution Context như thế nào? Cùng một function gọi 2 lần tạo ra bao nhiêu scope và bao nhiêu execution context?"
  2. Hỏi AI: What is the difference between scope and execution context in JavaScript? If I call the same function twice, how many scopes and execution contexts are created?
  3. So sánh: AI có nói "each function call creates a new scope" không? Nếu có, đó là điểm mù.
  4. Verify: Đối chiếu với ECMAScript Spec — Execution Contexts hoặc MDN — JavaScript Execution.

Gợi ý

Để ý xem AI có nhầm:

  • "Each function call creates a new scope" (sai: tạo EC mới, không tạo scope mới; scope là static).
  • "Scope determines the order of execution" (sai: đó là call stack).
  • "Execution context is the same as lexical environment" (sai: lexical environment là một phần của EC).
Đáp án tham khảo
  • Bạn nghĩ: Scope là static structure (blueprint) xác định visibility. Cùng một function chỉ có một scope. Execution context là runtime instance (công trường) — mỗi lần gọi tạo một EC mới. Gọi function 2 lần = 1 scope + 2 ECs.
  • AI trả lời: Thường trả lời đúng rằng scope là "where variables are accessible" và execution context là "the environment where code runs", nhưng hay dùng từ mơ hồ và đồng nhất hóa hai khái niệm.
  • So sánh: AI thường không phân biệt rõ: scope là property của source code (1 lần viết = 1 scope), EC là property của runtime (N lần gọi = N ECs). AI cũng hay nói "function scope is created when function is called" — điều này nhầm lẫn scope structure với EC instance.
  • Điểm AI nói sai hoặc quá mơ hồ: AI hay nói "when a function is called, a new scope is created" — scope (visibility structure) không được "tạo" khi gọi; nó là static property của source code, với outer reference được khóa tại function creation time. Điều được tạo khi gọi là lexical environment instance bên trong EC.
  • Kết luận: Nếu bạn chỉ ra được scope là một (static blueprint) và EC là nhiều (runtime instances), bạn đã hiểu sâu hơn AI. Điều này là prerequisite cho closure (Module 1.4) và memory debugging (Stage 11).

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 gọi ticket() hai lần đều in ra 1? Tại sao không phải do 'scope bị reset'?"

js
function ticket() {
  let number = 0;
  number++;
  return number;
}

console.log(ticket());
console.log(ticket());

Dùng đúng terminology: scope, execution context, lexical environment instance, blueprint.

Mô phỏng
  • Bạn nói: ticket có một scope duy nhất — đó là blueprint xác định number là biến local và chỉ nhìn thấy trong ticket. Nhưng mỗi lần gọi ticket(), JavaScript tạo một execution context mới. Trong mỗi EC có một lexical environment instance riêng, và trong đó number được khởi tạo là 0, rồi tăng lên 1. Lần gọi thứ hai tạo EC hoàn toàn mới, với number = 0 mới. Không có "scope bị reset" — scope không đổi. Đơn giản là mỗi EC có instance riêng. Nếu muốn number giữ lại giá trị, ta cần closure: tạo một EC cha, để EC con (lần gọi sau) chia sẻ lexical environment với EC con (lần gọi trước).

💡 Hình dung như in vé số: cùng một máy in (scope — blueprint) nhưng mỗi lần bấm nút là một phiên in mới (EC — instance). Máy luôn bắt đầu đếm từ 0 mỗi phiên vì mỗi EC có lexical environment instance riêng. Nếu muốn đếm liên tục, bạn cần một cuốn sổ ghi chép bên ngoài máy in (closure giữ lexical environment cũ).

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

  • Đồng nghiệp có hiểu tại sao không phải "scope bị reset" không?
  • Bạn có thể dự đoán được nếu đổi let thành var thì kết quả thay đổi không? (Gợi ý: không đổi — vẫn là local trong mỗi EC.)
  • Nếu họ hỏi "vậy làm sao để giữ lại number giữa các lần gọi", bạn trả lời được 2 cách không? (Closure hoặc global variable.)

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáNội dung
Phân biệt Scope (static) và Execution Context (dynamic)Explain (Giải thích)Dùng lời riêng và analogy blueprint/công trường
Giải thích Lexical Environment là cầu nốiPrediction (Dự đoán)Mô tả lexical environment thuộc EC nào và link đến scope nào
Dự đoán biến local có chia sẻ giữa các lần gọiPrediction Exercise (P1–P3)Cho function gọi nhiều lần, đoán output và giải thích bằng EC instance
Phân biệt Scope Chain và Call StackPrediction Exercise (P2)Giải thích tại sao c() nhìn thấy global x dù được gọi từ b() (scope chain lexical ≠ call stack runtime)
Không dùng "scope" và "execution context" thay thế nhauTeach BackGiải thích đúng terminology trong 2 phút

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

  • [ ] Có thể phân biệt Scope (1 blueprint) và Execution Context (N instances khi gọi N lần).
  • [ ] Có thể giải thích Lexical Environment là cấu trúc bên trong mỗi EC, link đến outer scope.
  • [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
  • [ ] Có thể vẽ đồng thời call stack và scope chain cho cùng một đoạn code.
  • [ ] Không dùng "scope" và "execution context" thay thế cho nhau trong giải thích.

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

Previous (Trước): Lexical Environment (Lesson 1.2.7) — hiểu cấu trúc environment record + outer reference.

Current (Hiện tại): Scope vs Execution Context — tổng hợp và phân biệt 4 khái niệm then chốt của Module 1.1 và 1.2.

Next (Tiếp theo):

  • Module 1.3 — Hoisting & Temporal Dead Zone (tại sao biến tồn tại trong lexical environment nhưng vẫn không dùng được)
  • Module 1.4 — Closure (lexical environment instance tồn tại sau khi EC pop)
  • Module 1.5 — Call Stack & Execution Tracing (call stack deep dive)
  • Stage 2 — Object Model & this (this binding thuộc EC, không thuộc scope)
  • Stage 8 — React (re-render tạo EC mới, hooks closure giữ lexical environment cũ)
  • Stage 11 — Memory (lexical environment retention sau khi EC pop)
📴 Offline Mode — Content served from cache