Lesson 0.5.2 — Function Expression
0. Metadata
| Field | Value |
|---|---|
| Stage | 0 — JavaScript Language Foundation |
| Module | 0.5 — Functions |
| Lesson | 0.5.2 |
| Competency | C01.5 — Functions |
| Depth Target | L2–L3 |
| Prerequisites | Function Declaration (0.5.1), Variables & Bindings (0.2.2) |
| Estimated Cognitive Load | Medium |
1. Why This Exists (Vì sao cần học)
Bạn đã học function declaration — khai báo một công thức có tên để tái sử dụng. Nhưng trong thực tế, bạn thường cần:
- Truyền logic vào một hàm khác (ví dụ:
setTimeout,map,filter). - Quyết định "tạo ra hàm nào" dựa trên điều kiện runtime.
- Gán hàm vào object hoặc biến để tổ chức code.
Vấn đề cốt lõi
Function declaration là tĩnh — bạn viết một lần, dùng mãi mãi. Nhưng JavaScript cần khả năng xử lý logic như dữ liệu: di chuyển, gán, truyền đi. Function expression giải quyết điều này bằng cách biến function thành một giá trị (value) như 42 hay "hello".
Nếu chỉ biết declaration, bạn sẽ không hiểu tại sao đoạn code này chạy được:
setTimeout(function() {
console.log("Done");
}, 1000);Bạn cũng sẽ không hiểu tại sao map nhận một function làm tham số. Bài này xây dựng mental model để giải thích điều đó.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- Viết được function declaration với parameters và
return(0.5.1). - Hiểu biến
const/letlưu giá trị, vàconstcấm reassignment (0.2.2). - Biết
typeoftrả về"function"khi áp dụng cho function (0.2.5).
WARNING
Nếu bạn chưa chắc const x = 5 nghĩa là gán giá trị 5 vào biến x, hãy ôn lại 0.2.2. Function expression dựa trên cùng cơ chế gán này.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Viết function expression và gán vào biến hoặc property.
- Phân biệt function declaration và function expression ở mức syntax và behavior cơ bản.
- Truyền function expression như một argument (callback pattern).
- Dự đoán lỗi khi gọi function expression trước dòng gán.
- Phân biệt anonymous function expression và named function expression.
4. Mental Model (Mô hình tư duy)
Ở bài trước, function declaration giống như đăng ký công thức vào danh bạ nhà nước — có tên, có sẵn, không phụ thuộc vào thứ tự dòng code (ở mức surface).
Function expression giống như tạo ra một vật thể (object) có khả năng chạy, rồi đặt nó vào một hộp (biến).
Mental Model
Primitive value: 42, "hello", true
Object value: {}, []
Function value: function() {}
Tất cả đều có thể:
→ Gán vào biến
→ Truyền vào function khác
→ Trả về từ function khácFunction expression tạo ra một function value. Giá trị này không có tên ở bên ngoài — nó chỉ tồn tại nếu bạn gán nó vào đâu đó (biến, object property, hoặc truyền đi).
Quan trọng: function() {} không phải là một câu lệnh đứng riêng như declaration. Nó là một biểu thức tạo giá trị, giống như 1 + 2 tạo ra 3.
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Concept | Ý nghĩa |
|---|---|
| Function Expression | const x = function() {} — tạo function value rồi gán vào biến. |
| Anonymous Function | Function expression không có tên: function() {}. |
| Named Function Expression | Function expression có tên nội bộ: const x = function y() {}. |
| First-class Value | Function có thể gán, truyền, trả về như mọi giá trị khác. |
| Callback (intro) | Truyền function expression vào function khác để nó gọi lại sau. |
Supporting (Hỗ trợ)
| Concept | Ý nghĩa |
|---|---|
| Function in Object | Object property chứa function expression: const obj = { fn: function() {} }. |
| Stack Trace | Named function expression giúp debug dễ hơn anonymous. |
Awareness (Biết tồn tại)
| Concept | Lý do chưa đào sâu |
|---|---|
| Hoisting difference | Declaration được "đăng ký tên" sớm hơn expression. Cơ chế thuộc Stage 1. |
| IIFE | (function(){}()) — pattern cũ để tạo scope riêng. Sẽ gặp lại ở Stage 1/2. |
Out of Scope (Không thuộc bài này)
- Closure mechanism (Stage 1)
thisbinding (Stage 2)- Arrow function syntax (0.5.3)
- Execution context / lexical environment (Stage 1)
- Deep hoisting / TDZ mechanics (Stage 1)
6. Worked Example (Ví dụ phân tích từng bước)
Xét đoạn code sau:
const operations = {
add: function(a, b) {
return a + b;
},
multiply: function(a, b) {
return a * b;
}
};
function calculate(fn, x, y) {
return fn(x, y);
}
const result = calculate(operations.add, 4, 5);
console.log(result);Walkthrough
Step 1 — Expression Creationfunction(a, b) { return a + b; } tạo ra một function value. Giá trị này được gán vào property add của object operations. Không có tên riêng ở bên ngoài — nó chỉ tồn tại qua operations.add.
Step 2 — Function as Argumentcalculate(operations.add, 4, 5) truyền operations.add (một function value) vào parameter fn của calculate. Lúc này fn và operations.add trỏ đến cùng một function value.
Step 3 — Invocationfn(x, y) tương đương operations.add(4, 5). Body chạy, return 4 + 5 → 9.
Step 4 — Return to Callercalculate trả về 9. result nhận giá trị 9.
Step 5 — Observationconsole.log(result) in 9.
Key Insight: Function calculate không biết fn là add hay multiply. Nó chỉ biết fn là một giá trị có thể gọi được (callable). Đây là sức mạnh của function as value.
7. Prediction Exercise (Bài tập dự đoán)
Đừng chạy code. Đọc và dự đoán output hoặc lỗi, sau đó giải thích tại sao.
Câu 1
const greet = function(name) {
return "Hello " + name;
};
console.log(typeof greet);Câu 2
const math = {
double: function(x) { return x * 2; }
};
console.log(math.double(5));Câu 3
const action = function(x) { return x * 2; };
action = function(x) { return x + 10; };
console.log(action(5));Câu 4
const outer = function inner() {
return typeof inner;
};
console.log(outer());
console.log(typeof inner);[Đáp án & Giải thích]
Câu 1: "function"
- Giải thích:
greetlà biến chứa một function value.typeofvới function value trả về"function".
Câu 2: 10
- Giải thích:
math.doubletrỏ đến function expression. Gọi nó với5trả về10.
Câu 3: throw TypeError
- Giải thích: Biến
action(khai báo bằngconst) không thể reassign? Đúng vậy — nhưng đoạn code này sẽ throw TypeError vìactionlàconst. Nếu dùnglet, kết quả là15. Đây là câu hỏi "bẫy" để kiểm tra xem bạn có để ýconstkhông.
Câu 4: "function" rồi "undefined"
- Giải thích:
innerlà tên nội bộ của function expression, chỉ tồn tại bên trong body của function đó.outer()trả về"function"vìinnertồn tại ở đó. Nhưnginnerkhông tồn tại ở scope bên ngoài, nêntypeof innerlà"undefined".
8. Implementation Lab (Bài lab thực hành)
Level 1 — Guided (Có hướng dẫn)
Viết một function expression định dạng tên người dùng và gán vào biến formatName:
// Yêu cầu:
// - Nhận firstName, lastName
// - Trả về: "LAST, First"
// Ví dụ: formatName("An", "Nguyen") → "Nguyen, An"
const formatName = function(???, ???) {
// implement here
};[Đáp án tham khảo]
const formatName = function(firstName, lastName) {
return `${lastName}, ${firstName}`;
};
console.log(formatName("An", "Nguyen"));
// "Nguyen, An"Giải thích:
- Function nhận 2 tham số: firstName, lastName.
- Dùng template literal để ghép chuỗi theo định dạng "LAST, First".
Level 2 — Partial Scaffold (Khung sẵn)
Hoàn thành hàm process nhận một function expression làm callback:
function process(value, transform) {
// Gọi transform với value và return kết quả
}
// Sử dụng process với một function expression
// để chuyển chuỗi thành chữ hoa
const upper = process("hello", /* expression here */);
console.log(upper); // "HELLO"[Đáp án tham khảo]
function process(value, transform) {
return transform(value);
}
// Sử dụng function expression làm callback
const upper = process("hello", function(value) {
return value.toUpperCase();
});
console.log(upper);
// "HELLO"Giải thích: transform là một function được truyền vào process. Bên trong process, ta gọi transform(value) và trả về kết quả.
Level 3 — Independent (Tự viết)
Tạo một object calculator với 4 phương thức, mỗi phương thức là một function expression:
const calculator = {
add: function(a, b) { /* ... */ },
subtract: function(a, b) { /* ... */ },
multiply: function(a, b) { /* ... */ },
divide: function(a, b) { /* ... */ }
};
// Sau đó viết một hàm `operate` nhận:
// - tên phép toán (string)
// - hai số
// - trả về kết quả bằng cách gọi đúng method trong calculatorGợi ý
Dùng calculator[operation](a, b) để truy cập động. Đây là lần đầu tiên bạn kết hợp bracket notation với function call.
[Đáp án tham khảo]
const calculator = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
},
multiply: function(a, b) {
return a * b;
},
divide: function(a, b) {
return a / b;
}
};
function operate(operation, a, b) {
return calculator[operation](a, b);
}
console.log(operate("add", 10, 5)); // 15
console.log(operate("subtract", 10, 5)); // 5
console.log(operate("multiply", 10, 5)); // 50
console.log(operate("divide", 10, 5)); // 2Giải thích: calculator[operation] cho phép truy cập method dựa trên giá trị của biến operation.
9. Edge Cases (Các trường hợp ngoại lệ)
Anonymous function và stack trace
const bad = function() {
throw new Error("Oops");
};
bad();Tại sao: Stack trace hiển thị (anonymous) hoặc tương tự. Khi debug production, rất khó biết lỗi từ đâu.
Cách nhận biết: Error message không có tên function rõ ràng.
Cách xử lý: Dùng named function expression khi logic phức tạp hoặc có khả năng throw error.
Gọi expression trước dòng gán
console.log(fn());
const fn = function() { return 1; };Tại sao: const fn tồn tại ở đầu scope nhưng chưa được gán giá trị. Truy cập trước dòng gán gây ReferenceError — biến đã được "khai báo" nhưng chưa "sẵn sàng" để dùng. (Cơ chế chi tiết, gọi là TDZ, thuộc Stage 1.). Khác với declaration, expression không sẵn sàng trước dòng code của nó.
Cách nhận biết: Lỗi Cannot access 'fn' before initialization.
Cách xử lý: Luôn đặt function expression trước nơi sử dụng, hoặc dùng declaration nếu cần gọi sớm.
Nhắc nhở: Cơ chế chi tiết (TDZ, hoisting) thuộc Stage 1. Ở đây bạn chỉ cần nhớ: expression phải có trước khi dùng.
Reassign biến chứa function
let handler = function() { return "A"; };
handler = null;
handler();Tại sao: handler giờ là null, không còn callable. Gọi handler() throw TypeError.
Cách nhận biết: TypeError: handler is not a function.
Cách xử lý: Nếu function không đổi, khai báo bằng const để ngăn reassignment.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng): TypeError: formatter is not a function
const formatter = function(price) {
return price.toLocaleString() + " VND";
};
// ... nhiều dòng code ...
formatter = "1,000,000 VND"; // Junior sửa bug và gán nhầm
const display = formatter(1500000);Reproduction (Tái hiện lỗi): Chạy đoạn code trên. Lỗi xuất hiện ở dòng gọi formatter(...).
Evidence (Bằng chứng):
formatterđược khai báoconstnên dòng gán lại sẽ throw TypeError (reassign const).- Nếu dùng
let, lỗi sẽ làTypeError: formatter is not a functiontại dòng gọi.
Hypothesis (Giả thuyết): Biến chứa function bị gán đè bởi một string. Lỗi không xuất hiện ngay tại dòng gán mà xuất hiện khi cố gọi như function.
Verification (Xác minh): Đổi const thành let để xem lỗi chuyển từ Assignment to constant variable sang is not a function. Điều này xác nhận root cause là reassignment.
Root Cause (Nguyên nhân gốc rễ): Dùng let thay vì const cho biến function, hoặc gán nhầm giá trị không phải function vào biến đó.
Fix (Sửa):
const formatter = function(price) {
return price.toLocaleString() + " VND";
};
const display = formatter(1500000);Prevention (Phòng ngừa):
- Luôn dùng
constcho biến gán function expression. Nếu cần reassign, đặt câu hỏi: tại sao biến này cần đổi? - Đặt tên rõ ràng để tránh nhầm lẫn với biến string:
formatPricethay vìpriceString.
11. Design Exercise (Bài tập thiết kế giải pháp)
Bạn đang viết một module xử lý dữ liệu. Có hai cách tổ chức:
function processItems(items) {
return items.map(function(item) {
return item.price * 2;
});
}function doublePrice(item) {
return item.price * 2;
}
function processItems(items) {
return items.map(doublePrice);
}Câu hỏi:
- Pattern A dùng function expression trực tiếp trong
map. Pattern B tách ra declaration. Nếu logicprice * 2chỉ dùng ở đây, pattern nào phù hợp hơn? - Nếu bạn cần viết unit test cho logic
price * 2, pattern nào dễ test hơn? - Bạn chọn pattern nào nếu logic đó dùng ở 5 nơi khác nhau?
[Đáp án tham khảo]
Bạn nghĩ:
- Pattern A phù hợp hơn khi logic chỉ dùng một lần — không cần đặt tên riêng, code gọn.
- Pattern B dễ test hơn vì
doublePricelà một đơn vị độc lập có tên, có thể import và test riêng. - Nếu dùng 5 nơi, Pattern B rõ ràng hơn — tránh lặp lại expression, dễ sửa một chỗ.
Kết luận: Function expression inline tiện cho local, one-off logic. Function declaration hoặc named expression tốt hơn khi logic cần tái sử dụng hoặc test.
12. Production Scenario (Tình huống thực tế)
Context: Bạn review code của junior trong một dashboard component:
const handlers = {
click: function handleClick() {
throw new Error("Network failed");
},
hover: function() {
throw new Error("Network failed");
}
};Symptom: Sentry báo lỗi từ dashboard. Stack trace của lỗi hover chỉ hiển thị (anonymous), trong khi lỗi click hiển thị handleClick.
Constraint: Bạn không được thay đổi cấu trúc object handlers.
Câu hỏi:
- Tại sao
clickcó tên trong stack trace cònhoverkhông? handleClickcó thể gọi từ bên ngoài bằnghandlers.handleClick()không? Tại sao?- Sửa lại
hoverđể stack trace có tên rõ ràng.
[Đáp án tham khảo]
Bạn nghĩ:
clickdùng named function expression (function handleClick()). TênhandleClickđược engine ghi nhận nội bộ để hiển thị trong stack trace.hoverlà anonymous nên hiển thị(anonymous).- Không.
handleClickchỉ tồn tại bên trong body của function expression. Bên ngoài, property vẫn làhandlers.click. Tên nội bộ không tạo binding bên ngoài. - Sửa thành:js
hover: function handleHover() { throw new Error("Network failed"); }
Bài học: Named function expression không tốn thêm chi phí runtime đáng kể nhưng cải thiện debuggability đáng kể. Đây là trade-off miễn phí trong production.
13. AI-assisted Exercise (Bài tập với AI)
Level B — Challenge
- Tự trả lời trước: Viết ra 2 điểm khác biệt quan trọng nhất giữa function declaration và function expression.
- Hỏi AI: "What is the difference between function declaration and function expression in JavaScript?"
- So sánh câu trả lời AI với câu của bạn. AI có nói "function expression không có tên" không? Nếu có, đó là điểm sai.
- Verify bằng MDN: tìm trang "Function expression" trên MDN Web Docs và đọc phần "Named function expression".
Gợi ý
AI thường trả lời đúng về hoisting nhưng lại bỏ sót khía cạnh quan trọng nhất: function expression tạo ra một giá trị (value) trước khi gán vào biến. Điểm này là cốt lõi của first-class function. Nếu AI không nhắc đến "value", bạn đã nhìn thấy điểm mù của nó.
Đáp án tham khảo
Bạn nghĩ: Function declaration đăng ký tên vào scope; function expression tạo ra một giá trị rồi gán vào biến. Expression có thể có tên (named FE) hoặc không (anonymous).
AI trả lời (typical):
- Function declarations are hoisted; function expressions are not.
- Function expressions can be anonymous.
- Function declarations create a variable in the current scope.
So sánh: AI đúng về hoisting nhưng không giải thích tại sao expression không hoisted (vì nó là giá trị của một biến, không phải binding độc lập). AI cũng thường nói "expressions are anonymous" mà không nhắc named FE.
Điểm AI nói sai hoặc quá mơ hồ: "Function expressions are anonymous" — câu này sai.
const x = function y() {}hoàn toàn hợp lệ và hữu ích cho stack trace. AI thường bỏ qua named function expression vì nó là pattern ít phổ biến hơn nhưng rất quan trọng trong production debugging.Kết luận: Nếu bạn chỉ ra được rằng expression tạo value và có thể có tên nội bộ, bạn đã hiểu sâu hơn câu trả lời surface-level của AI. Concept này sẽ quay lại khi học closure (Stage 1) và callback architecture (Stage 8).
14. Teach Back (Dạy lại)
Giả sử một junior developer hỏi bạn:
"Em thấy
const fn = function() {}vàfunction fn() {}đều cho ra hàmfn. Tại sao đôi khi người ta dùng cách thứ nhất?"
Hãy giải thích trong 2 phút, dùng ít nhất 2 ví dụ thực tế: một về gán function vào object, một về truyền function vào hàm khác.
Mô phỏng
- Bạn nói: Cách
const fn = function(){}gọi là function expression. Nó tạo ra một giá trị rồi đặt vào biến. Vì là giá trị, bạn có thể làm những việc declaration không làm được dễ dàng. Ví dụ một: gán vào object —const obj = { process: function(){} }. Ví dụ hai: truyền vào hàm khác —setTimeout(function(){}, 1000). Declaration thì bạn không thể viết một declaration đứng một mình rồi coi chính declaration như một value để truyền ngay. Expression cho phép bạn tạo logic "tại chỗ" mà không cần đặt tên riêng ở ngoài.
💡 Tưởng tượng declaration như một cửa hàng có biển hiệu cố định trên đường. Expression như một food truck — không có địa chỉ cố định, bạn đặt nó ở đâu cũng được, thậm chí đưa thẳng vào hội chợ (callback) mà không cần mặt bằng riêng.
Gợi ý đánh giá bản thân
- Đồng nghiệp có hiểu tại sao expression linh hoạt hơn không?
- Bạn có dùng được từ "value" để giải thích không?
- Nếu đồng nghiệp hỏi: "Vậy khi nào dùng declaration?" — bạn trả lời được không? (Gợi ý: khi cần hàm có sẵn ở mọi nơi trong scope, không phụ thuộc thứ tự dòng code.)
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Task |
|---|---|---|
| Write function expression (Viết function expression) | Implementation (Thực hành) | Implementation Lab Level 1 |
| Use function as argument (Dùng function làm argument) | Implementation (Thực hành) | Implementation Lab Level 2 |
| Distinguish declaration vs expression (Phân biệt declaration và expression) | Prediction (Dự đoán) | Câu 4, Prediction Exercise |
| Debug reassignment of function variable (Gỡ lỗi gán đè biến function) | Debug Lab | Debug Lab Section |
| Choose inline vs named function (Lựa chọn inline hay named) | Design Decision | Design Exercise |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể viết function expression và gán vào biến bằng
const. - [ ] Có thể truyền function expression như argument cho function khác.
- [ ] Có thể dự đoán lỗi khi gọi function expression trước dòng gán (với
const). - [ ] Có thể phân biệt anonymous function expression và named function expression.
- [ ] Có thể giải thích tại sao function được gọi là "first-class value" trong JavaScript.
Gợi ý: vì
functionđược đối xử giống như bất kỳ giá trị nào khác trong JS:- Gán vào biến
- Truyền vào function khác như một argument
returntừ một function- Lưu trong object hoặc array
- [ ] Có thể chọn giữa inline expression và named declaration dựa trên nhu cầu tái sử dụng.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Function Declaration (0.5.1) — bạn đã biết cách khai báo một đơn vị logic có tên, tĩnh, và sẵn sàng trong scope.
Current (Hiện tại): Function Expression — bạn học rằng function là giá trị, có thể gán, truyền, và trả về. Đây là nền tảng của callback và higher-order function.
Next (Tiếp theo):
- 0.5.3 — Arrow Functions: cú pháp ngắn gọn đặc biệt phù hợp cho function expression.
- 0.5.5 — Callback: function expression là công cụ chính để truyền logic vào hàm khác.
- 0.5.6 — Higher-order Functions: function trả về function, xây dựng trên mental model "function là value".
- Stage 1 — Execution Model & Closure: khi function expression "nhớ" được môi trường nơi nó được tạo ra.