Skip to content

Lesson 1.3.2 — var (The var Binding) ​

Bài 1.3.2 — var
Function Scope, Initialization & Hoisting Behavior • 25 phút
0:00 / 0:00

0. Metadata (Thông tin bài học) ​

FieldValue
Stage1 — JavaScript Execution Model
Module1.3 — Hoisting & Temporal Dead Zone
Lesson1.3.2 — var
CompetencyC01.2 — Variables & Bindings
DepthL2–L3 (Explain → Use)
PrerequisitesLesson 1.3.1 (Declaration vs Initialization), Lesson 1.1.4 (Creation vs Execution Phase), Lesson 1.2.3 (Function Scope)
Cognitive LoadMedium

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

Bạn mở một legacy codebase và thấy:

js
if (user.isActive) {
  var status = "online";
}
console.log(status); // "online" ???

Hoặc bạn refactor một file từ năm 2016 và đoạn code này chạy mà không throw error — nhưng giá trị lại là undefined khi bạn mong đợi một string.

let và const đã thay thế var trong hầu hết code mới, nhưng var vẫn tồn tại trong:

  • Triệu dòng code legacy cần maintain.
  • Interview questions kiểm tra understanding về execution model.
  • Minified/bundled code mà bạn debug trong production.

Nếu bạn chỉ biết "var bị hoisted", bạn sẽ không giải thích được tại sao var trong một if block lại visible bên ngoài, tại sao re-declare không throw error, và tại sao global var lại tạo property trên window.

Bài này xây dựng mental model chính xác về var như một function-scoped binding với early initialization.

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

Trước khi học bài này, bạn cần:

  • Phân biệt được Declaration, Initialization, Assignment (Lesson 1.3.1).
  • Hiểu Creation Phase tạo bindings trước khi code chạy (Lesson 1.1.4).
  • Biết Function Scope khác Block Scope như thế nào (Lesson 1.2.3).

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

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

  1. Giải thích tại sao var có function scope thay vì block scope.
  2. Trace một var binding qua Creation Phase (declare + initialize undefined) và Execution Phase (assignment).
  3. Dự đoán giá trị của var khi được truy cập trước dòng khai báo hoặc bên ngoài block.
  4. Nhận biết global object pollution khi dùng var ở global scope.

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

Mental Model: var là Permissive Function-Scoped Binding

var x = 10;

Creation Phase (trong function/script):

1. Declare: Tạo binding 'x' trong Function Environment Record
2. Initialize: Gán 'x' = undefined

Execution Phase (tại dòng code):

3. Assignment: Cập nhật 'x' = 10 (nếu có initializer)

Đặc điểm của var:

  • Function scope: Binding tồn tại trong toàn bộ function body, kể cả bên ngoài block { }.
  • Early initialize: Không bao giờ ở trạng thái "uninitialized". Nó luôn có giá trị hợp lệ (có thể là undefined).
  • Re-declare allowed: Khai báo lại cùng tên trong cùng scope không throw error.

Sai lầm phổ biến

"var không có scope."

Sai. var có scope — nhưng là function scope, không phải block scope. Điều này làm cho var bên trong if/for vẫn visible bên ngoài block.

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

Essential (Bắt buộc) ​

ConceptĐịnh nghĩa
Function Scopevar binding visible trong toàn bộ function body chứa nó, bất kể block lồng nhau.
Creation Phase Initializevar được declare và initialize thành undefined trong Creation Phase.
Re-declarationvar cho phép khai báo trùng tên trong cùng scope; binding hiện có được giữ lại.
Global Object Pollutionvar ở global scope tạo property trên global object (window/globalThis).

Supporting (Hỗ trợ) ​

  • Hoisting như hệ quả: Việc var có thể truy cập trước dòng khai báo không phải là một cơ chế đặc biệt tên "hoisting"; nó là hệ quả của declare + initialize trong Creation Phase.

Awareness (Biết tồn tại) ​

  • var trong eval() có behavior đặc biệt (không cần master).
  • Function declaration cũng có behavior tương tự var nhưng initialize bằng function object (sẽ học ở 1.3.5).

Out of Scope (Không học trong bài này) ​

  • Chi tiết spec algorithm của CreateMutableBinding và SetMutableBinding.
  • Cách var tương tác với module scope (sẽ học ở Stage 7).

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

Trace đoạn code sau:

js
function configure() {
  console.log(mode); // Một dòng code, nhưng engine xử lý qua 2 phase khác nhau
  var mode = "production";
  console.log(mode);
}
configure();

Step 1 — Creation Phase (Function Execution Context của configure)

  • Engine quét function body, thấy var mode.
  • Declaration: Tạo binding mode trong Function Environment Record.
  • Initialization: Gán mode = undefined.

Trạng thái Environment Record của configure:

text
mode: undefined  (declared + initialized)

Step 2 — Execution Phase

  • Dòng 2: console.log(mode) — đọc binding mode → undefined.
  • Dòng 3: var mode = "production" — phần var mode đã xử lý xong ở Creation Phase; dòng này chỉ thực hiện Assignment (mode = "production").
  • Dòng 4: console.log(mode) — đọc binding → "production".

Code Review Lens

Khi thấy var x = ... trong code, đừng nghĩ nó là "một dòng". Hãy tách thành:

  • var x → Creation Phase (D + I)
  • x = ... → Execution Phase (A)

7. Prediction Exercise (Bài tập dự đoán) ​

Prediction 1

Đừng chạy code. Dự đoán output và giải thích bằng Creation Phase / Execution Phase.

js
function test() {
  console.log(a);
  var a = 5;
  console.log(a);
}
test();
[Đáp án & Giải thích]
  • Bạn nghĩ

    • Dòng 2: undefined vì var a đã được declare + initialize (undefined) trong Creation Phase.
    • Dòng 4: 5 vì assignment đã chạy.
  • Giải thích

    • Creation Phase: a declared, initialized to undefined.
    • Execution: console.log(a) → undefined; a = 5 → assignment; console.log(a) → 5.

Prediction 2

Đừng chạy code. Dự đoán output.

js
function demo() {
  if (true) {
    var count = 10;
  }
  console.log(count);
}
demo();
[Đáp án & Giải thích]
  • Bạn nghĩ

    • 10. Vì var có function scope, không bị giới hạn bởi block if.
  • Giải thích

    • var count được declare + initialize trong Creation Phase của demo.
    • Dù khai báo nằm trong block if, binding vẫn thuộc Function Environment Record của demo.
    • console.log(count) bên ngoài if vẫn resolve được → 10.

Prediction 3

Đừng chạy code. Dự đoán output hoặc error.

js
var rate = 1;
var rate = 2;
console.log(rate);
[Đáp án & Giải thích]
  • Bạn nghĩ

    • 2. Không có error. var cho phép re-declare.
  • Giải thích

    • Dòng 1: Creation Phase declare + initialize rate to undefined; Execution Phase assign 1.
    • Dòng 2: var rate trong Creation Phase của cùng scope không tạo binding mới (binding đã tồn tại); Execution Phase assign 2.
    • var re-declare là no-op ở Creation Phase, chỉ là assignment ở Execution Phase.

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

Level 1 — Guided (Hướng dẫn) ​

Viết comment mô tả phase cho đoạn code sau:

js
function init() {
  // Creation Phase: var message → declare + initialize to ___
  console.log(message);
  // Execution Phase: assignment
  var message = "ready";
  console.log(message);
}

Level 2 — Partial Scaffold (Khung mẫu một phần) ​

Điền trạng thái Environment Record sau từng phase:

js
function setup() {
  var level = 1;
  if (true) {
    var level = 2;
  }
  console.log(level);
}
PhaseBinding levelValue
Creation Phase (setup)Declared + Initializedundefined
Execution, dòng 2______
Execution, dòng 4______
Execution, dòng 6______
[Đáp án & Giải thích]
PhaseBinding levelValue
Creation Phase (setup)Declared + Initializedundefined
Execution, dòng 2Assigned1
Execution, dòng 4Re-assigned2
Execution, dòng 6Read2

Level 3 — Independent (Tự thực hiện) ​

Viết một function ngắn (tối đa 8 dòng) trong đó:

  1. Một biến var được log ra trước dòng khai báo và cho kết quả undefined.
  2. Cùng biến đó được khai báo lại bên trong một if (true) block.
  3. Giá trị cuối cùng sau block khác với giá trị trước block.
  4. Giải thích bằng đúng 3 khái niệm: function scope, Creation Phase, assignment.

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

Edge Case 1: var trong for loop ​

js
for (var i = 0; i < 3; i++) {
  // i là cùng một binding qua mọi iteration
}
console.log(i); // 3

i không bị giới hạn trong block for. Nó là function-scoped (hoặc global-scoped nếu ở top level). Đây là nguyên nhân của classic closure bug:

js
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Output: 3 3 3

INFO

Đây là preview của Lesson 1.4.5 (Closure trong Loop). var tạo một binding duy nhất; mỗi callback đều reference cùng binding đó.

Edge Case 2: Global Object Pollution ​

js
var API_URL = "https://api.example.com";

Trong browser:

js
console.log(window.API_URL); // "https://api.example.com"

Trong Node.js (module):

js
console.log(globalThis.API_URL); // undefined (vì module scope không pollute global)

WARNING

var ở global scope của script (không phải module) tạo configurable property trên global object. Điều này có thể gây collision giữa các script third-party.

Edge Case 3: var vs Function Declaration (Preview cho Lesson 1.3.5) ​

js
console.log(foo); // [Function: foo]

function foo() {}

Function declaration cũng được xử lý trong Creation Phase, nhưng thay vì initialize thành undefined, nó initialize thành function object. Đây là lý do bạn có thể gọi function trước dòng khai báo.

INFO

Chi tiết về function declaration hoisting sẽ được xử lý ở Lesson 1.3.5. Tại đây chỉ cần nhận biết sự khác biệt behavior so với var.

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

Symptom (Triệu chứng): Developer báo cáo "biến bị undefined một cách bí ẩn".

js
function processConfig(config) {
  if (config.enabled) {
    var mode = "strict";
  }
  console.log("Running in", mode, "mode");
}

processConfig({ enabled: false });
// Output: "Running in undefined mode"

Reproduction (Tái hiện lỗi): Chạy processConfig({ enabled: false }) → mode là undefined.

Evidence (Bằng chứng):

  • Không throw ReferenceError.
  • mode không phải null, mà là undefined.
  • Nếu đổi var thành let, code throw ReferenceError.

Hypothesis (Giả thuyết):

  • var mode được declare + initialize (undefined) trong Creation Phase của processConfig.
  • Vì config.enabled là false, block if không chạy → assignment "strict" không xảy ra.
  • mode vẫn tồn tại (đã declared) với giá trị undefined.

Verification (Xác minh): Tách var mode = "strict" thành declaration và assignment riêng biệt, sau đó log trước và sau assignment để quan sát giá trị undefined từ Creation Phase:

js
function processConfig(config) {
  var mode; // D+I trong Creation Phase → mode = undefined
  console.log("Before if:", mode); // undefined — binding đã tồn tại và được initialize
  if (config.enabled) {
    mode = "strict"; // Assignment trong Execution Phase
  }
  console.log("Running in", mode, "mode");
}

Output: Before if: undefined → Running in undefined mode. Giả thuyết đúng: binding tồn tại và có giá trị undefined trước khi block if chạy, chứng minh var được initialize sớm trong Creation Phase.

Root Cause (Nguyên nhân gốc rễ): Developer mong đợi var có block scope như let. Khi if không chạy, họ nghĩ mode sẽ không tồn tại. Thực tế var tạo binding function-scoped và initialize sớm với undefined.

Fix (Sửa): Nếu cần block scope behavior:

js
function processConfig(config) {
  let mode; // block-scoped, nhưng vẫn cần initialize trước khi dùng
  if (config.enabled) {
    mode = "strict";
  } else {
    mode = "default";
  }
  console.log("Running in", mode, "mode");
}

Hoặc refactor logic để không dựa vào binding tồn tại sẵn:

js
function processConfig(config) {
  const mode = config.enabled ? "strict" : "default";
  console.log("Running in", mode, "mode");
}

Prevention (Phòng ngừa):

  • Không dùng var khi cần block-scoped binding.
  • Luôn initialize var ở đầu function nếu buộc phải dùng legacy code.

11. Design Exercise (Bài tập thiết kế giải pháp) ​

Depth L2–L3: Decision (Quyết định)

Bạn đang maintain một file legacy sử dụng var rải rác. Team quyết định không refactor toàn bộ file ngay (risk quá cao), nhưng cần sửa một bug cụ thể.

Context: Bug xảy ra vì var trong for loop bị leak ra ngoài và ghi đè một biến cùng tên ở outer scope.

Constraints:

  • Không được đổi var thành let/const (vì file đang dùng trong hệ thống build cũ không hỗ trợ block scope polyfill đáng tin cậy).
  • Không được đổi tên biến outer scope (vì API public phụ thuộc vào tên đó).

Options:

  1. Dùng IIFE để tạo function scope mới cho loop.
  2. Di chuyển var khai báo lên đầu function để rõ ràng hơn.
  3. Tách loop thành một function riêng.

Question: Option nào an toàn nhất? Tại sao?

[Đáp án tham khảo]
  • Bạn nghĩ

    • Option 1 (IIFE) là giải pháp classic để mô phỏng block scope trước khi ES6 có let.
    • Option 2 chỉ làm rõ intent nhưng không fix leak.
    • Option 3 cũng hoạt động nhưng có overhead function call và phá vỡ structure code.
  • Giải thích

    • IIFE tạo một Function Execution Context mới, nên var bên trong IIFE là function-scoped nhưng chỉ trong IIFE đó.
    • Đây là lý do IIFE từng là pattern phổ biến trước ES6.
js
for (var i = 0; i < 3; i++) {
  (function (index) {
    setTimeout(() => console.log(index), 0);
  })(i);
}
  • Trade-off
    • IIFE làm code dài hơn và khó đọc hơn let, nhưng là giải pháp duy nhất trong constraint "không dùng let/const".

12. Production Scenario (Tình huống thực tế) ​

Bạn debug một production crash trong ứng dụng embed third-party script:

html
<script>
  var apiKey = "our-secret-key";
</script>
<script src="third-party-analytics.js"></script>

Third-party script chứa:

js
var apiKey = "their-key";
console.log(window.apiKey); // "their-key"

Câu hỏi:

  1. Tại sao third-party script có thể ghi đè biến của bạn?
  2. Điều này có xảy ra nếu bạn dùng const thay vì var không? Tại sao?
  3. Bạn sẽ prevent điều này như thế nào?
[Đáp án tham khảo]
  • Câu 1: var apiKey ở global scope tạo property configurable trên window. Script thứ hai khai báo lại cùng tên property trên window, ghi đè giá trị.

  • Câu 2: const apiKey ở global scope trong module hoặc strict mode tạo global binding nhưng không tạo property trên window/globalThis. Trong non-strict script thuần, behavior có thể khác (vẫn tạo property nhưng không configurable), nhưng codebase hiện đại thường dùng module. Ngoài ra, re-declare const sẽ throw SyntaxError.

  • Câu 3: Dùng module (type="module") hoặc IIFE để tạo scope riêng, hoặc dùng const trong module context.

13. AI-assisted Exercise (Bài tập với AI) ​

Level B — Challenge (Thử thách AI)

  1. Tự trả lời trước: Viết ra giấy định nghĩa của bạn về behavior của var qua 3 góc độ: scope, phase (Creation/Execution), và re-declaration.
  2. Hỏi AI: "Explain how var works in JavaScript. Why can I access a var variable before its declaration line?"
  3. So sánh: AI có dùng từ "hoisted" không? Nó có phân biệt rõ "function scope" vs "block scope" không? Nó có nhắc đến Creation Phase không?
  4. Verify: Kiểm tra lại bằng MDN hoặc ECMAScript specification (từ khóa: var statement, Function Environment Record).

Gợi ý

Để ý xem AI có nói "var is hoisted" như một cơ chế đặc biệt không. Nếu có, đó là dấu hiệu mental model của AI đang dùng approximation thay vì engine mechanism. Bạn đã học rằng đây chỉ là hệ quả của Creation Phase.

Đáp án tham khảo
  • Bạn nghĩ

    • Scope: Function scope, không phải block scope.
    • Creation Phase: Declare + Initialize to undefined.
    • Execution Phase: Assignment nếu có initializer.
    • Re-declare: Cho phép, không throw error.
  • AI trả lời (mô phỏng phản hồi thực tế)

    • "var declarations are hoisted to the top of their function scope."
    • "You can access them before the declaration because they are initialized with undefined during hoisting."
    • "var has function scope, so it ignores block statements like if or for."
    • "You can redeclare var variables without errors."
  • So sánh

    • AI đúng về behavior: function scope, undefined trước dòng khai báo, re-declare được.
    • AI thiếu depth: Nó dùng "hoisted" như cơ chế nguyên nhân thay vì hệ quả. Nó không giải thích rằng engine chạy Creation Phase trước Execution Phase.
    • AI không nhấn mạnh sự khác biệt giữa "initialize to undefined" (của var) và "uninitialized" (của let).
  • Điểm AI nói sai hoặc quá mơ hồ

    • AI nói "var is hoisted" — câu này đúng behavior nhưng sai mechanism. Không có cơ chế tên "hoisting" trong engine; đó là hệ quả của việc quét và tạo binding trong Creation Phase.
    • AI thường không nhắc đến global object pollution khi var ở global scope.
  • Kết luận

    • Nếu bạn chỉ ra được rằng "hoisting" chỉ là tên gọi cho hệ quả của Creation Phase, và var khác let ở chỗ initialize timing chứ không phải "có được đưa lên đầu hay không", bạn đã hiểu sâu hơn AI. Concept này sẽ quay lại ở bài 1.3.3 (let/const) và 1.4.5 (Closure trong loop).

14. Teach Back (Dạy lại) ​

Yêu cầu: Giải thích cho một đồng nghiệp junior trong 2 phút:

"Tại sao đoạn code này in undefined thay vì throw error? Tại sao var trong if block lại visible bên ngoài? Dùng đúng terminology: function scope, Creation Phase, initialize. Không được dùng từ 'hoisted'."

js
function example() {
  console.log(x);
  if (true) {
    var x = 10;
  }
  console.log(x);
}
example();
Mô phỏng
  • Bạn nói
    • "Khi example() được gọi, engine chạy Creation Phase trước. Trong phase này, engine nhìn qua toàn bộ function và thấy var x."
    • "Engine tạo binding x trong Function Environment Record của example — không phải trong block if. Vì var có function scope."
    • "Đồng thời, engine initialize x thành undefined ngay trong Creation Phase. Cho nên khi Execution Phase chạy đến dòng console.log(x) đầu tiên, x đã tồn tại và có giá trị hợp lệ — dù là undefined."
    • "Dòng var x = 10 bên trong if thực chất chỉ là assignment trong Execution Phase. Phần declare và initialize đã xảy ra trước rồi."
    • "Vì binding thuộc về function scope, dòng console.log(x) cuối cùng vẫn nhìn thấy x → in ra 10."
    • "Nếu đổi thành let, x sẽ thuộc block scope của if, và dòng console.log đầu tiên sẽ throw ReferenceError vì let không được initialize trong Creation Phase."

💡 Liên tưởng (Analogy — dùng ở L2-L3): Tưởng tượng var như một chiếc bàn được đặt ở cửa ra vào của function. Dù bạn để đồ lên bàn ở phòng nào bên trong (block if), chiếc bàn vẫn ở cửa ra vào — cả function đều thấy nó. Và chiếc bàn luôn có một tấm biển tạm ghi undefined cho đến khi bạn dán nhãn thật lên.

Lưu ý: Đây là analogy để hình dung function scope. Ở level cao hơn (S11+), bạn cần reasoning trực tiếp qua Environment Record thay vì analogy.

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

  • Đồng nghiệp có hiểu tại sao undefined khác với uninitialized không?
  • Bạn có thể dự đoán được behavior nếu thay var bằng let mà không cần chạy code không?
  • Nếu đồng nghiệp hỏi "vậy var có được đưa lên đầu function không?", bạn trả lời được không mà không dùng từ "hoisted"?

15. Assessment (Đánh giá) ​

Objective (Mục tiêu)Hình thức đánh giáDepth
Giải thích function scope của varExplain (Giải thích) + Prediction (Dự đoán)L2
Trace var qua Creation/Execution PhaseImplementation (Thực hành trace)L3
Dự đoán giá trị trước/sau dòng khai báoPrediction (Dự đoán)L3
Nhận biết global object pollutionClassification (Phân loại)L2
Debug environment mismatch do varDebug Lab (Gỡ lỗi)L3

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

  • [ ] Có thể giải thích tại sao var có function scope thay vì block scope.
  • [ ] Có thể trace một var binding qua Creation Phase (declare + initialize undefined) và Execution Phase (assignment).
  • [ ] Có thể dự đoán đúng output của code có var bên trong block (if/for) khi được truy cập từ bên ngoài block.
  • [ ] Có thể giải thích tại sao var cho phép re-declare mà không throw error.
  • [ ] Có thể nhận biết global object pollution khi dùng var ở global scope.
  • [ ] Có thể dự đoán đúng 3/3 scenarios trong Prediction Exercise mà không chạy code.

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

Previous (Trước): Declaration vs Initialization → bạn đã biết binding trải qua nhiều phase, và var initialize sớm với undefined.

Current (Hiện tại): var behavior → bạn hiểu rằng var là function-scoped binding được declare + initialize trong Creation Phase, cho phép early access và re-declaration.

Next (Tiếp theo):

  • 1.3.3 (let/const): Xem cách let tách declaration và initialization, tạo block scope, và cấm re-declare.
  • 1.3.4 (TDZ): Hiểu chính xác tại sao let throw ReferenceError khi truy cập sớm — vì nó được declare nhưng chưa initialize.
  • 1.4.5 (Closure trong Loop): var trong loop tạo một binding duy nhất, dẫn đến classic closure bug mà let sửa bằng cách tạo binding mới mỗi iteration.
📴 Offline Mode — Content served from cache