Lesson 1.3.2 — var (The var Binding)
0. Metadata (Thông tin bài học)
| Field | Value |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.3 — Hoisting & Temporal Dead Zone |
| Lesson | 1.3.2 — var |
| Competency | C01.2 — Variables & Bindings |
| Depth | L2–L3 (Explain → Use) |
| Prerequisites | Lesson 1.3.1 (Declaration vs Initialization), Lesson 1.1.4 (Creation vs Execution Phase), Lesson 1.2.3 (Function Scope) |
| Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn mở một legacy codebase và thấy:
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ể:
- Giải thích tại sao
varcó function scope thay vì block scope. - Trace một
varbinding qua Creation Phase (declare + initializeundefined) và Execution Phase (assignment). - Dự đoán giá trị của
varkhi được truy cập trước dòng khai báo hoặc bên ngoài block. - 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' = undefinedExecution 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
"
varkhô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 Scope | var binding visible trong toàn bộ function body chứa nó, bất kể block lồng nhau. |
| Creation Phase Initialize | var được declare và initialize thành undefined trong Creation Phase. |
| Re-declaration | var cho phép khai báo trùng tên trong cùng scope; binding hiện có được giữ lại. |
| Global Object Pollution | var ở global scope tạo property trên global object (window/globalThis). |
Supporting (Hỗ trợ)
- Hoisting như hệ quả: Việc
varcó 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)
vartrongeval()có behavior đặc biệt (không cần master).- Function declaration cũng có behavior tương tự
varnhư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
CreateMutableBindingvàSetMutableBinding. - Cách
vartươ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:
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
modetrong Function Environment Record. - Initialization: Gán
mode = undefined.
Trạng thái Environment Record của configure:
mode: undefined (declared + initialized)Step 2 — Execution Phase
- Dòng 2:
console.log(mode)— đọc bindingmode→undefined. - Dòng 3:
var mode = "production"— phầnvar 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.
function test() {
console.log(a);
var a = 5;
console.log(a);
}
test();[Đáp án & Giải thích]
Bạn nghĩ
- Dòng 2:
undefinedvìvar ađã được declare + initialize (undefined) trong Creation Phase. - Dòng 4:
5vì assignment đã chạy.
- Dòng 2:
Giải thích
- Creation Phase:
adeclared, initialized toundefined. - Execution:
console.log(a)→undefined;a = 5→ assignment;console.log(a)→5.
- Creation Phase:
Prediction 2
Đừng chạy code. Dự đoán output.
function demo() {
if (true) {
var count = 10;
}
console.log(count);
}
demo();[Đáp án & Giải thích]
Bạn nghĩ
10. Vìvarcó function scope, không bị giới hạn bởi blockif.
Giải thích
var countđược declare + initialize trong Creation Phase củademo.- Dù khai báo nằm trong block
if, binding vẫn thuộc Function Environment Record củademo. console.log(count)bên ngoàiifvẫn resolve được →10.
Prediction 3
Đừng chạy code. Dự đoán output hoặc error.
var rate = 1;
var rate = 2;
console.log(rate);[Đáp án & Giải thích]
Bạn nghĩ
2. Không có error.varcho phép re-declare.
Giải thích
- Dòng 1: Creation Phase declare + initialize
ratetoundefined; Execution Phase assign1. - Dòng 2:
var ratetrong Creation Phase của cùng scope không tạo binding mới (binding đã tồn tại); Execution Phase assign2. varre-declare là no-op ở Creation Phase, chỉ là assignment ở Execution Phase.
- Dòng 1: Creation Phase declare + initialize
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:
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:
function setup() {
var level = 1;
if (true) {
var level = 2;
}
console.log(level);
}| Phase | Binding level | Value |
|---|---|---|
| Creation Phase (setup) | Declared + Initialized | undefined |
| Execution, dòng 2 | ___ | ___ |
| Execution, dòng 4 | ___ | ___ |
| Execution, dòng 6 | ___ | ___ |
[Đáp án & Giải thích]
| Phase | Binding level | Value |
|---|---|---|
| Creation Phase (setup) | Declared + Initialized | undefined |
| Execution, dòng 2 | Assigned | 1 |
| Execution, dòng 4 | Re-assigned | 2 |
| Execution, dòng 6 | Read | 2 |
Level 3 — Independent (Tự thực hiện)
Viết một function ngắn (tối đa 8 dòng) trong đó:
- Một biến
varđược log ra trước dòng khai báo và cho kết quảundefined. - Cùng biến đó được khai báo lại bên trong một
if (true)block. - Giá trị cuối cùng sau block khác với giá trị trước block.
- 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
for (var i = 0; i < 3; i++) {
// i là cùng một binding qua mọi iteration
}
console.log(i); // 3i 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:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 3 3 3INFO
Đâ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
var API_URL = "https://api.example.com";Trong browser:
console.log(window.API_URL); // "https://api.example.com"Trong Node.js (module):
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)
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".
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. modekhông phảinull, mà làundefined.- Nếu đổi
varthànhlet, code throwReferenceError.
Hypothesis (Giả thuyết):
var modeđược declare + initialize (undefined) trong Creation Phase củaprocessConfig.- Vì
config.enabledlàfalse, blockifkhông chạy → assignment"strict"không xảy ra. modevẫ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:
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:
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:
function processConfig(config) {
const mode = config.enabled ? "strict" : "default";
console.log("Running in", mode, "mode");
}Prevention (Phòng ngừa):
- Không dùng
varkhi 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
varthànhlet/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:
- Dùng IIFE để tạo function scope mới cho loop.
- Di chuyển
varkhai báo lên đầu function để rõ ràng hơn. - 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.
- Option 1 (IIFE) là giải pháp classic để mô phỏng block scope trước khi ES6 có
Giải thích
- IIFE tạo một Function Execution Context mới, nên
varbê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.
- IIFE tạo một Function Execution Context mới, nên
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".
- IIFE làm code dài hơn và khó đọc hơn
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:
<script>
var apiKey = "our-secret-key";
</script>
<script src="third-party-analytics.js"></script>Third-party script chứa:
var apiKey = "their-key";
console.log(window.apiKey); // "their-key"Câu hỏi:
- Tại sao third-party script có thể ghi đè biến của bạn?
- Điều này có xảy ra nếu bạn dùng
constthay vìvarkhông? Tại sao? - 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ênwindow. Script thứ hai khai báo lại cùng tên property trênwindow, 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ênwindow/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-declareconstsẽ throwSyntaxError.Câu 3: Dùng module (
type="module") hoặc IIFE để tạo scope riêng, hoặc dùngconsttrong module context.
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge (Thử thách AI)
- Tự trả lời trước: Viết ra giấy định nghĩa của bạn về behavior của
varqua 3 góc độ: scope, phase (Creation/Execution), và re-declaration. - Hỏi AI: "Explain how
varworks in JavaScript. Why can I access avarvariable before its declaration line?" - 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?
- 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ế)
- "
vardeclarations are hoisted to the top of their function scope." - "You can access them before the declaration because they are initialized with
undefinedduring hoisting." - "
varhas function scope, so it ignores block statements likeiforfor." - "You can redeclare
varvariables without errors."
- "
So sánh
- AI đúng về behavior: function scope,
undefinedtrướ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ủavar) và "uninitialized" (củalet).
- AI đúng về behavior: function scope,
Điểm AI nói sai hoặc quá mơ hồ
- AI nói "
varis 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.
- AI nói "
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à
varkhácletở 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).
- Nếu bạn chỉ ra được rằng "hoisting" chỉ là tên gọi cho hệ quả của Creation Phase, và
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
undefinedthay vì throw error? Tại saovartrongifblock lại visible bên ngoài? Dùng đúng terminology: function scope, Creation Phase, initialize. Không được dùng từ 'hoisted'."
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ấyvar x." - "Engine tạo binding
xtrong Function Environment Record củaexample— không phải trong blockif. Vìvarcó function scope." - "Đồng thời, engine initialize
xthànhundefinedngay trong Creation Phase. Cho nên khi Execution Phase chạy đến dòngconsole.log(x)đầu tiên,xđã tồn tại và có giá trị hợp lệ — dù làundefined." - "Dòng
var x = 10bên trongifthự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ấyx→ in ra10." - "Nếu đổi thành
let,xsẽ thuộc block scope củaif, và dòngconsole.logđầu tiên sẽ throwReferenceErrorvìletkhông được initialize trong Creation Phase."
- "Khi
💡 Liên tưởng (Analogy — dùng ở L2-L3): Tưởng tượng
varnhư 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 (blockif), 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 ghiundefinedcho đế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
undefinedkhác vớiuninitializedkhông? - Bạn có thể dự đoán được behavior nếu thay
varbằngletmà không cần chạy code không? - Nếu đồng nghiệp hỏi "vậy
varcó đượ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 var | Explain (Giải thích) + Prediction (Dự đoán) | L2 |
Trace var qua Creation/Execution Phase | Implementation (Thực hành trace) | L3 |
| Dự đoán giá trị trước/sau dòng khai báo | Prediction (Dự đoán) | L3 |
| Nhận biết global object pollution | Classification (Phân loại) | L2 |
Debug environment mismatch do var | Debug Lab (Gỡ lỗi) | L3 |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể giải thích tại sao
varcó function scope thay vì block scope. - [ ] Có thể trace một
varbinding qua Creation Phase (declare + initializeundefined) và Execution Phase (assignment). - [ ] Có thể dự đoán đúng output của code có
varbê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
varcho 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à
varinitialize sớm vớiundefined.
Current (Hiện tại):
varbehavior → bạn hiểu rằngvarlà 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áchlettá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
letthrowReferenceErrorkhi truy cập sớm — vì nó được declare nhưng chưa initialize.- 1.4.5 (Closure trong Loop):
vartrong loop tạo một binding duy nhất, dẫn đến classic closure bug màletsửa bằng cách tạo binding mới mỗi iteration.