Skip to content

Lesson 1.2.3 — Function Scope (Phạm vi Hàm) ​

Bài 1.2.3 — Function Scope
Local Bindings, Function Boundaries & Variable Access • 21 phút
0:00 / 0:00

0. Metadata ​

TrườngGiá trị
Stage1 — JavaScript Execution Model
Module1.2 — Scope & Lexical Environment
Lesson1.2.3
CompetencyC02 — JavaScript Runtime
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.2.1 — Scope là gì?, Lesson 1.2.2 — Global Scope
Cognitive LoadTrung bình

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

Bạn viết một hàm tiện ích để tính toán:

js
function calculateTotal() {
  var tax = 0.1;
  var discount = 5;
  return price * (1 + tax) - discount;
}

Sau đó bạn nhận ra price không được khai báo ở đâu cả, nhưng code vẫn chạy vì nó đọc price từ global. Hoặc ngược lại, bạn khai báo var tax = 0.1 và tưởng rằng bên ngoài hàm không thấy được.

Production Risk

Nếu không hiểu function scope, developer thường:

  • Để lộ biến nội bộ ra ngoài (information leak).
  • Dùng var tưởng rằng nó block-scoped như let, gây bug trong loop hoặc condition.
  • Không hiểu tại sao IIFE "che giấu" được biến.

Function scope trả lời câu hỏi:

"Những biến khai báo bên trong một hàm có thể nhìn thấy từ đâu?"

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 là scope ngoài cùng (Lesson 1.2.2).
  • var, let, const khai báo biến.

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

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

  1. Giải thích function scope là scope được tạo ra khi hàm được định nghĩa.
  2. Phân biệt biến khai báo bên trong hàm (local) và biến bên ngoài (global/outer).
  3. Dự đoán một biến có accessible hay không từ bên ngoài hàm.
  4. Nhận diện sự khác biệt giữa var (function-scoped) và let/const (block-scoped) ở mức preview.
  5. Refactor code để encapsulate biến bằng function scope.

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

Function Scope = Phòng Riêng (Private Room)

Hãy hình dung mỗi function là một phòng riêng trong tòa nhà:

text
Tầng Trệt (Global Scope)
  └── Phòng A (Function Scope)
        ├── Bàn làm việc (local variables)
        └── Cửa sổ nhìn ra tầng trệt (nhìn thấy outer scope)
  • Từ bên trong phòng, bạn nhìn thấy đồ trong phòng và đồ ở tầng trệt.
  • Từ tầng trệt, bạn không nhìn thấy đồ trong phòng.
  • Mỗi lần gọi hàm = mở một phiên làm việc mới trong phòng đó, nhưng cấu trúc phòng (scope) không đổi.

Sai lầm phổ biến

"Function scope chỉ tạo ra khi hàm được gọi."

Sai một nửa. Scope structure (cấu trúc vùng nhìn thấy) được xác định khi hàm được viết (lexical). Nhưng scope instance (các binding cụ thể) được tạo ra khi hàm được gọi (execution context). Đừng nhầm lẫn hai khái niệm này.

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

LoạiNội dung
Essential (Bắt buộc)Function scope, Local variables, Outer scope visibility, Encapsulation
Supporting (Hỗ trợ)var là function-scoped, let/const là block-scoped (preview)
Awareness (Biết tồn tại)Function declaration tạo binding khác function expression
Out of Scope (Không học ở đây)Hoisting (Module 1.3), Closure (Module 1.4), Execution context internals (Module 1.1)

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

Input:

js
const globalRate = 1.2;

function calculate(price) {
  const tax = 0.1;
  const total = price * (1 + tax) * globalRate;
  return total;
}

console.log(calculate(100));
console.log(typeof tax);

Step 1 — Observation: calculate(100) được gọi. Bên trong calculate cần resolve price, tax, globalRate, total. Dòng console.log(typeof tax) chạy ở global scope.

Step 2 — Classification: Variable resolution theo lexical scope. tax là local variable của calculate.

Step 3 — Reasoning:

IdentifierLocal calculateOuter/GlobalKết quả
price✅ Tham số—100
tax✅ Khai báo local—0.1
globalRate❌ Không có✅ Global1.2
total✅ Khai báo local—132
tax (từ global)—❌ Không có"undefined"

Step 4 — Conclusion: calculate(100) trả về 132. typeof tax ở global trả về "undefined" vì tax thuộc function scope của calculate, không nhìn thấy từ bên ngoài.

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 Function Scope ​

js
function secret() {
  var code = "1234";
}
secret();
console.log(code);
[Đáp án & Giải thích]
  • Bạn nghĩ: ReferenceError: code is not defined. Vì code được khai báo bên trong secret, thuộc function scope của secret. Từ global scope không thể nhìn thấy vào trong.
  • Giải thích: Function scope là "phòng kín một chiều". Từ trong nhìn ra được, từ ngoài nhìn vào không được.

P2 — Nested Function Visibility ​

js
var level = "easy";

function game() {
  var level = "hard";
  console.log(level);
}

game();
console.log(level);
[Đáp án & Giải thích]
  • Bạn nghĩ: In ra "hard" rồi "easy". Biến level bên trong game shadow biến level bên ngoài, nhưng chỉ trong scope của game.
  • Giải thích: Đây là scope shadowing (sẽ học chi tiết ở Lesson 1.2.5). Mỗi scope có level riêng. console.log(level) ở global resolve đến global level.

P3 — Function Scope vs Block Scope (Preview) ​

js
function testVar() {
  if (true) {
    var x = 10;
  }
  console.log(x);
}

function testLet() {
  if (true) {
    let y = 20;
  }
  console.log(y);
}

testVar();
testLet();
[Đáp án & Giải thích]
  • Bạn nghĩ: testVar() in ra 10. testLet() throw ReferenceError: y is not defined.
  • Giải thích: var là function-scoped: nó "nhảy" ra khỏi block { } và thuộc về function scope. let là block-scoped: nó bị giới hạn trong block { } nơi nó được khai báo. Đây là lý do chính let/const được khuyến nghị thay var trong modern JavaScript.

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

Lab: Private Counter (Guided → Independent) ​

Yêu cầu: Viết createCounter sao cho:

  • count chỉ tồn tại bên trong function scope.
  • increment() và get() có thể truy cập count.
  • Từ bên ngoài, count không thể truy cập trực tiếp.

Starter (đã hoàn thành — tập trung vào việc giải thích tại sao nó hoạt động):

js
function createCounter() {
  // count thuộc function scope của createCounter
  var count = 0;

  function increment() {
    // TODO: Truy cập count tại đây
    count++;
  }

  function get() {
    // TODO: Truy cập count tại đây
    return count;
  }

  return { increment, get };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.get()); // 2
console.log(counter.count); // undefined
[Đáp án tham khảo]
  • Bạn nghĩ: count khai báo bằng var (hoặc let/const) bên trong createCounter sẽ thuộc function scope. increment và get cùng nằm trong function scope nên nhìn thấy count. Object return chỉ chứa increment và get, không chứa count, nên từ bên ngoài không truy cập được.
  • Giải thích: Đây là nền tảng của private state bằng function scope. Kết hợp với closure (Module 1.4), count sẽ được giữ lại qua nhiều lần gọi.

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

EC-1: var trong Function Scope ​

js
function outer() {
  var x = 1;

  function inner() {
    var x = 2;
    console.log(x);
  }

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

outer();

What fails? Nếu nghĩ var x = 2 bên trong inner ghi đè var x = 1 của outer, bạn sẽ bối rối khi outer() vẫn in ra 1.

Why? Mỗi function có scope riêng. x trong inner là một binding hoàn toàn khác x trong outer. Chúng không ghi đè lẫn nhau mà shadow (che) nhau trong scope tương ứng. var trong inner không "leak" ra outer.

EC-2: Tham số là Local Binding ​

js
function modify(value) {
  value = 999;
  console.log(value);
}

var value = 100;
modify(value);
console.log(value);

What fails? Nếu nghĩ value = 999 bên trong modify sẽ thay đổi value bên ngoài, bạn sẽ hiểu nhầm về pass-by-value và scope.

Why? Tham số value của modify là một local binding trong function scope của modify. Dòng value = 999 chỉ thay đổi local binding, không ảnh hưởng đến value ở global scope. Tuy nhiên, nếu value là object, việc mutate property bên trong function sẽ ảnh hưởng object bên ngoài — điều này thuộc về Value Semantics (Stage 2), không phải scope.

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

Symptom (Triệu chứng): Một hàm helper trả về kết quả sai sau khi refactor. Developer nghi ngờ "biến bị ghi đè".

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

js
var name = "global";

function render() {
  var name = "local";
  helper();
}

function helper() {
  console.log(name);
}

render();

Evidence (Bằng chứng): Console in ra "global" thay vì "local".

Hypothesis (Giả thuyết): Developer tưởng rằng vì helper() được gọi từ bên trong render(), nên helper() sẽ nhìn thấy name của render().

Verification (Xác minh): Di chuyển helper vào bên trong render hoặc truyền name như tham số.

Root Cause (Nguyên nhân gốc rễ): Scope mismatch do nhầm lẫn dynamic scope. JavaScript dùng lexical scope, nghĩa là helper() nhìn thấy scope nơi nó được viết (global), không phải scope nơi nó được gọi (render()). name trong render() là local binding của render, không liên quan đến helper.

Fix (Sửa):

js
function render() {
  var name = "local";
  helper(name);
}

function helper(name) {
  console.log(name);
}

Prevention (Phòng ngừa): Khi một hàm cần dữ liệu từ scope khác, truyền qua tham số thay vì dựa vào outer scope. Điều này làm code explicit và dễ trace hơn.

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: Bạn đang review một pull request. Một junior dev viết module quản lý trạng thái form:

js
var formState = { dirty: false };

function validate() {
  var formState = { dirty: true };
  // validate logic...
}

function submit() {
  console.log(formState.dirty);
}

validate();
submit();

Symptom: submit() luôn log ra false dù validate() đã set dirty: true.

Decision: Developer cần nhận ra var formState bên trong validate() là một local binding hoàn toàn mới trong function scope của validate. Nó không ghi đè formState ở global scope. Đây là bug do hiểu nhầm function scope.

Fix: Không khai báo lại var formState bên trong validate nếu mục đích là mutate global state. Hoặc tốt hơn: refactor để truyền state qua tham số và return.

js
function validate(state) {
  state.dirty = true;
  return state;
}

var formState = { dirty: false };
formState = validate(formState);
submit(formState);

💡 Trade-off: Truyền state qua tham số làm code explicit hơn nhưng tăng số lượng tham số. Trong production, bạn sẽ dùng module pattern hoặc state management (Stage 8) thay vì global variable.

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: "Tại sao biến khai báo bằng var bên trong hàm không nhìn thấy từ bên ngoài? Điều gì xảy ra nếu bỏ var đi?"
  2. Hỏi AI: Why can't I access a var variable declared inside a function from outside? What happens if I remove var?
  3. So sánh: AI có giải thích rõ sự khác biệt giữa local binding và implicit global không?
  4. Verify: Đối chiếu với MDN — Function scope.

Gợi ý

Để ý xem AI có nhầm:

  • "Function scope only exists after the function is called" (sai: scope structure tồn tại tại parse time).
  • "Removing var makes it a global variable automatically" (đúng ở non-strict, nhưng AI thường không nhắc strict mode).
Đáp án tham khảo
  • Bạn nghĩ: var bên trong hàm tạo local binding trong function scope. Từ bên ngoài, engine không thể resolve identifier đó vì nó nằm ngoài visibility zone. Nếu bỏ var, JavaScript ở non-strict mode sẽ tạo implicit global property — đây là nguồn gốc của global pollution.
  • AI trả lời: Thường giải thích đúng rằng var tạo local variable, nhưng hay dùng từ mơ hồ như "the variable is trapped inside the function" hoặc "it becomes local to the function" mà không phân biệt lexical scope structure và execution context instance.
  • So sánh: AI hiếm khi nhấn mạnh điểm then chốt: scope được xác định bởi vị trí viết code (lexical), không phải vị trí gọi. Điều này quan trọng để hiểu closure và debug scope shadowing.
  • Điểm AI nói sai hoặc quá mơ hồ: AI thường nói "variables declared inside a function are local" nhưng không giải thích tại sao — vì function tạo ra một scope boundary mà outer scope không thể nhìn qua. AI cũng thường bỏ qua strict mode khi nói về implicit global.
  • Kết luận: Nếu bạn chỉ ra được function scope là lexical boundary và implicit global chỉ xảy ra ở non-strict mode, bạn đã hiểu sâu hơn AI. Điều này sẽ giúp bạn viết code an toàn hơn ở các module sau.

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 global thay vì local? Tại sao không phải do biến bị ghi đè?"

js
var name = "global";

function render() {
  var name = "local";
  helper();
}

function helper() {
  console.log(name);
}

render();

Dùng đúng terminology: function scope, lexical scope, local binding, outer scope.

Mô phỏng
  • Bạn nói: helper được viết ở global scope, nên lexical scope của helper là global. Khi helper() cần resolve name, nó nhìn vào scope nơi nó được viết — global scope — và tìm thấy name = "global". Biến name = "local" bên trong render thuộc function scope của render, hoàn toàn tách biệt. Đây không phải ghi đè mà là shadowing trong scope khác. Để helper nhìn thấy name của render, ta phải truyền qua tham số hoặc định nghĩa helper bên trong render.

💡 Hình dung mỗi function như một căn phòng có cửa sổ một chiều: từ trong nhìn ra ngoài được, nhưng mỗi phòng chỉ nhìn thấy những gì ở bên ngoài phòng đó, không phải phòng bên cạnh.

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

  • Đồng nghiệp có hiểu tại sao không phải do "biến bị ghi đè" không?
  • Bạn có thể dự đoán được nếu đổi var thành let ở cả hai chỗ không?
  • Nếu họ hỏi "vậy làm sao để helper nhìn thấy local name", bạn trả lời được ít nhất 2 cách không?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáNội dung
Giải thích function scope là scope tạo bởi hàmExplain (Giải thích)Dùng lời riêng mô tả function scope và tại sao biến local không nhìn thấy từ ngoài
Phân biệt local và global/outer variablesPrediction (Dự đoán)Cho code có biến cùng tên ở global và function, đoán giá trị từng vị trí
Dự đoán accessibility từ bên ngoài hàmPrediction Exercise (P1–P3)Cho 3 đoạn code, đoán biến nào accessible từ global
Nhận diện var function-scoped vs let block-scopedPrediction (P3)Giải thích tại sao var thoát khỏi block nhưng không thoát khỏi function
Encapsulate biến bằng function scopeImplementation LabViết hàm tạo private state, biến nội bộ không lộ ra ngoài

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

  • [ ] Có thể giải thích function scope là "phòng riêng" mà từ ngoài không nhìn vào được.
  • [ ] Có thể phân biệt local binding (trong hàm) và outer binding (bên ngoài hàm).
  • [ ] Dự đoán đúng 3/3 scenarios trong Prediction Exercise.
  • [ ] Có thể trace được lý do helper() nhìn thấy global name thay vì local name trong Debug Lab.
  • [ ] Implement được createCounter với private state bằng function scope.

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

Previous (Trước): Global Scope (Lesson 1.2.2) — hiểu scope ngoài cùng.

Current (Hiện tại): Function Scope — hiểu scope bên trong hàm, local vs outer, encapsulation.

Next (Tiếp theo):

  • Lesson 1.2.4 — Block Scope ({ }, let, const)
  • Lesson 1.2.5 — Nested Scope & Shadowing
  • Module 1.4 — Closure (function scope + lifetime extension)
  • Stage 2 — Object Model & this (method scope behavior)
  • Stage 8 — React (component function scope, hooks closure)
📴 Offline Mode — Content served from cache