Skip to content

Lesson 0.5.2 — Function Expression ​

Bài 0.5.2 — Function Expression
Function là value, gán vào biến, truyền như argument • 23 phút
0:00 / 0:00

0. Metadata ​

FieldValue
Stage0 — JavaScript Language Foundation
Module0.5 — Functions
Lesson0.5.2
CompetencyC01.5 — Functions
Depth TargetL2–L3
PrerequisitesFunction Declaration (0.5.1), Variables & Bindings (0.2.2)
Estimated Cognitive LoadMedium

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:

js
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 / let lưu giá trị, và const cấm reassignment (0.2.2).
  • Biết typeof trả 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ể:

  1. Viết function expression và gán vào biến hoặc property.
  2. Phân biệt function declaration và function expression ở mức syntax và behavior cơ bản.
  3. Truyền function expression như một argument (callback pattern).
  4. Dự đoán lỗi khi gọi function expression trước dòng gán.
  5. 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ác

Function 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 Expressionconst x = function() {} — tạo function value rồi gán vào biến.
Anonymous FunctionFunction expression không có tên: function() {}.
Named Function ExpressionFunction expression có tên nội bộ: const x = function y() {}.
First-class ValueFunction 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 ObjectObject property chứa function expression: const obj = { fn: function() {} }.
Stack TraceNamed function expression giúp debug dễ hơn anonymous.

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

ConceptLý do chưa đào sâu
Hoisting differenceDeclaration đượ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)
  • this binding (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:

js
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 ​

js
const greet = function(name) {
  return "Hello " + name;
};
console.log(typeof greet);

Câu 2 ​

js
const math = {
  double: function(x) { return x * 2; }
};
console.log(math.double(5));

Câu 3 ​

js
const action = function(x) { return x * 2; };
action = function(x) { return x + 10; };
console.log(action(5));

Câu 4 ​

js
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: greet là biến chứa một function value. typeof với function value trả về "function".

Câu 2: 10

  • Giải thích: math.double trỏ đến function expression. Gọi nó với 5 trả về 10.

Câu 3: throw TypeError

  • Giải thích: Biến action (khai báo bằng const) không thể reassign? Đúng vậy — nhưng đoạn code này sẽ throw TypeError vì action là const. Nếu dùng let, kết quả là 15. Đây là câu hỏi "bẫy" để kiểm tra xem bạn có để ý const không.

Câu 4: "function" rồi "undefined"

  • Giải thích: inner là 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ì inner tồn tại ở đó. Nhưng inner không tồn tại ở scope bên ngoài, nên typeof inner là "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:

js
// 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]
js
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:

js
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]
js
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:

js
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 calculator

Gợ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]
js
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));   // 2

Giả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

js
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

js
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

js
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

js
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áo const nê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 function tạ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):

js
const formatter = function(price) {
  return price.toLocaleString() + " VND";
};

const display = formatter(1500000);

Prevention (Phòng ngừa):

  • Luôn dùng const cho 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: formatPrice thay 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:

js
function processItems(items) {
  return items.map(function(item) {
    return item.price * 2;
  });
}
js
function doublePrice(item) {
  return item.price * 2;
}

function processItems(items) {
  return items.map(doublePrice);
}

Câu hỏi:

  1. Pattern A dùng function expression trực tiếp trong map. Pattern B tách ra declaration. Nếu logic price * 2 chỉ dùng ở đây, pattern nào phù hợp hơn?
  2. Nếu bạn cần viết unit test cho logic price * 2, pattern nào dễ test hơn?
  3. 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ĩ:

    1. 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.
    2. Pattern B dễ test hơn vì doublePrice là một đơn vị độc lập có tên, có thể import và test riêng.
    3. 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:

js
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:

  1. Tại sao click có tên trong stack trace còn hover không?
  2. handleClick có thể gọi từ bên ngoài bằng handlers.handleClick() không? Tại sao?
  3. Sửa lại hover để stack trace có tên rõ ràng.
[Đáp án tham khảo]
  • Bạn nghĩ:

    1. click dùng named function expression (function handleClick()). Tên handleClick được engine ghi nhận nội bộ để hiển thị trong stack trace. hover là anonymous nên hiển thị (anonymous).
    2. Không. handleClick chỉ 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.
    3. 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

  1. 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.
  2. Hỏi AI: "What is the difference between function declaration and function expression in JavaScript?"
  3. 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.
  4. 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àm fn. 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 LabDebug Lab Section
Choose inline vs named function (Lựa chọn inline hay named)Design DecisionDesign 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
    • return từ 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.
📴 Offline Mode — Content served from cache