Bài 4.4: Functions và arrow functions trong JavaScript

Bài 4.4: Functions và arrow functions trong JavaScript
Chào mừng bạn trở lại với chuỗi bài học Lập trình Web Front-end! Trong hành trình xây dựng ứng dụng web, chúng ta liên tục cần thực hiện các thao tác lặp đi lặp lại hoặc đóng gói một đoạn logic nhất định. Đây chính là lúc functions (hàm) thể hiện sức mạnh của mình. Functions là những khối code tái sử dụng, giúp chương trình của chúng ta có tổ chức hơn, dễ đọc, dễ bảo trì và tránh lặp code (DRY - Don't Repeat Yourself).
JavaScript cung cấp nhiều cách để định nghĩa hàm, và bài viết này sẽ đưa bạn đi sâu vào hai phong cách phổ biến nhất: Functions truyền thống và Arrow Functions hiện đại.
Functions truyền thống (Traditional Functions)
Trước khi ES6 ra đời, cú pháp function
là cách duy nhất để tạo hàm trong JavaScript. Cú pháp truyền thống này có hai hình thức chính: Function Declarations và Function Expressions.
Function Declarations (Khai báo hàm)
Đây là cách khai báo hàm phổ biến và dễ đọc nhất. Nó bắt đầu bằng từ khóa function
, theo sau là tên hàm, một cặp ngoặc đơn ()
chứa các tham số (nếu có), và một cặp ngoặc nhọn {}
chứa nội dung (thân) của hàm.
Cú pháp:
function tenHam(thamSo1, thamSo2, ...) {
// Nội dung hàm
// Có thể sử dụng từ khóa 'return' để trả về giá trị
}
Một điểm đặc biệt và quan trọng của Function Declarations là chúng được xử lý bằng cơ chế hoisting. Điều này có nghĩa là bạn có thể gọi hàm trước khi nó được định nghĩa trong code!
Ví dụ:
sayHelloDeclaration(); // Hoisting hoạt động! Output: "Xin chào từ Function Declaration!"
function sayHelloDeclaration() {
console.log("Xin chào từ Function Declaration!");
}
sayHelloDeclaration(); // Gọi hàm sau khi định nghĩa cũng hoạt động bình thường
Giải thích: Dù chúng ta gọi sayHelloDeclaration()
ở dòng đầu tiên, trước khi thấy định nghĩa hàm, chương trình vẫn chạy mà không báo lỗi. Đây là vì trong quá trình biên dịch, JavaScript "nâng" (hoist) toàn bộ khai báo hàm lên đầu phạm vi hiện tại.
Function Expressions (Biểu thức hàm)
Function Expressions là khi bạn định nghĩa một hàm như là một phần của một biểu thức, thường là bằng cách gán một hàm vô danh (anonymous function - hàm không có tên sau từ khóa function
) cho một biến.
Cú pháp:
const tenBien = function(thamSo1, thamSo2, ...) {
// Nội dung hàm
// Có thể sử dụng từ khóa 'return'
}; // Chú ý dấu chấm phẩy ở cuối
Không giống như Function Declarations, Function Expressions không được hoisting. Bạn chỉ có thể gọi hàm sau khi nó đã được gán giá trị cho biến.
Ví dụ:
// sayHelloExpression(); // Lỗi! ReferenceError: Cannot access 'sayHelloExpression' before initialization
const sayHelloExpression = function() {
console.log("Xin chào từ Function Expression!");
};
sayHelloExpression(); // Gọi hàm sau khi định nghĩa mới hoạt động
Giải thích: Khi cố gắng gọi sayHelloExpression()
trước khi nó được gán vào biến sayHelloExpression
, JavaScript báo lỗi vì biến này chưa được khởi tạo với giá trị là hàm.
Tham số (Parameters) và Giá trị trả về (Return Values)
Cả hai loại hàm truyền thống đều có thể nhận tham số đầu vào và trả về giá trị sử dụng từ khóa return
.
Ví dụ:
// Sử dụng Declaration
function add(a, b) {
return a + b; // Trả về tổng của a và b
}
let sum1 = add(5, 3); // sum1 = 8
console.log("Kết quả phép cộng (Declaration):", sum1);
// Sử dụng Expression
const subtract = function(a, b) {
return a - b; // Trả về hiệu của a và b
};
let diff = subtract(10, 4); // diff = 6
console.log("Kết quả phép trừ (Expression):", diff);
function greet(name) {
console.log("Xin chào,", name);
// Hàm không có 'return' rõ ràng sẽ trả về 'undefined'
}
let greetingResult = greet("Bạn"); // Output: "Xin chào, Bạn"
console.log("Kết quả hàm greet:", greetingResult); // Output: "Kết quả hàm greet: undefined"
Giải thích: Các hàm add
và subtract
sử dụng return
để gửi kết quả tính toán ra ngoài. Hàm greet
chỉ thực hiện hành động in ra console và không có câu lệnh return
nào, nên mặc định nó sẽ trả về undefined
.
Arrow Functions (=>) - Sự ra đời của cú pháp hiện đại
Được giới thiệu trong ES6 (ECMAScript 2015), Arrow Functions mang đến một cú pháp ngắn gọn hơn đáng kể để viết hàm, đặc biệt là cho các hàm ngắn hoặc dùng làm callbacks. Tên gọi "Arrow Function" xuất phát từ toán tử =>
dùng để định nghĩa hàm.
Cú pháp cơ bản
Arrow Function có cú pháp tổng quát như sau:
const tenBien = (thamSo1, thamSo2, ...) => {
// Nội dung hàm
// Cần 'return' nếu có nhiều dòng hoặc muốn trả về giá trị rõ ràng
};
Ví dụ cơ bản:
const greetArrow = (name) => {
console.log(`Xin chào từ Arrow Function, ${name}!`);
};
greetArrow("Dev"); // Output: "Xin chào từ Arrow Function, Dev!"
Giải thích: Thay vì function (name) { ... }
, chúng ta dùng (name) => { ... }
. Cú pháp này rõ ràng là ngắn gọn hơn.
Các biến thể cú pháp giúp ngắn gọn hơn nữa
Arrow Functions có một số quy tắc đặc biệt giúp viết code cực kỳ cô đọng trong các trường hợp cụ thể:
Chỉ có một tham số: Bạn có thể bỏ qua cặp ngoặc đơn
()
quanh tham số.const square = number => { // Bỏ qua () quanh 'number' return number * number; }; console.log("Bình phương của 5:", square(5)); // Output: "Bình phương của 5: 25"
Không có tham số nào: Bạn bắt buộc phải sử dụng cặp ngoặc đơn rỗng
()
.const sayHi = () => { // Bắt buộc phải có () console.log("Chào bạn từ Arrow Function!"); }; sayHi(); // Output: "Chào bạn từ Arrow Function!"
Thân hàm chỉ có một biểu thức và muốn trả về giá trị của biểu thức đó (Implicit Return): Bạn có thể bỏ qua cặp ngoặc nhọn
{}
và từ khóareturn
. JavaScript sẽ tự động trả về kết quả của biểu thức đó.const multiply = (a, b) => a * b; // Bỏ qua {} và 'return' console.log("Tích của 8 và 9:", multiply(8, 9)); // Output: "Tích của 8 và 9: 72"
Cú pháp này cực kỳ hữu ích khi làm việc với các phương thức xử lý mảng như
map
,filter
,reduce
.Thân hàm có nhiều dòng code hoặc cần thực hiện logic trước khi return (Explicit Return): Bạn vẫn dùng cặp ngoặc nhọn
{}
và cần sử dụngreturn
một cách rõ ràng.const calculateArea = (width, height) => { if (width <= 0 || height <= 0) { return "Kích thước không hợp lệ"; // Cần 'return' rõ ràng } const area = width * height; // Thực hiện logic return area; // Trả về giá trị }; console.log("Diện tích hình chữ nhật:", calculateArea(6, 10)); // Output: "Diện tích hình chữ nhật: 60" console.log("Diện tích hình chữ nhật:", calculateArea(-1, 5)); // Output: "Diện tích hình chữ nhật: Kích thước không hợp lệ"
Trả về một Object ngay lập tức với Implicit Return: Nếu bạn muốn trả về một object literal (
{ key: value }
) bằng cú pháp implicit return (không dùng{}
vàreturn
), bạn cần bao object literal đó trong dấu ngoặc đơn()
. Nếu không có dấu()
, JavaScript sẽ nhầm lẫn cặp ngoặc nhọn của object với block body của hàm.// KHÔNG ĐÚNG nếu muốn trả về object: // const createUser = (name, age) => { name: name, age: age }; // Lỗi hoặc trả về undefined // ĐÚNG: Bao object trong () const createUser = (name, age) => ({ name: name, age: age }); // Hoặc cú pháp ngắn hơn: ({ name, age }) const user = createUser("Alice", 30); console.log("Đối tượng người dùng:", user); // Output: "Đối tượng người dùng: { name: 'Alice', age: 30 }"
Điểm khác biệt QUAN TRỌNG NHẤT: Từ khóa this
Đây là sự khác biệt lớn nhất và thường gây nhầm lẫn giữa Functions truyền thống và Arrow Functions. Cách mà this
được xác định bên trong hàm là hoàn toàn khác nhau:
Trong Functions truyền thống: Giá trị của
this
phụ thuộc vào cách hàm được gọi. Nó có thể là đối tượng mà hàm đó là một phương thức, là đối tượng global (window
trong trình duyệt,global
trong Node.js), làundefined
(trong strict mode khi gọi hàm độc lập), hoặc là một giá trị cụ thể nếu dùngcall
,apply
,bind
. Nói cách khác,this
trong hàm truyền thống là dynamic context.Trong Arrow Functions: Arrow Functions không có
this
riêng của mình. Giá trị củathis
bên trong một Arrow Function được xác định theo phạm vi (scope) mà nó được định nghĩa (lexical scope). Nó đơn giản là kế thừa giá trịthis
từ phạm vi cha gần nhất. Nói cách khác,this
trong Arrow Function là lexical context.
Hãy xem ví dụ kinh điển với setTimeout
:
const person = {
name: "Bob",
greetTraditional: function() {
// 'this' ở đây là đối tượng person (vì hàm được gọi như một method của person)
console.log(`Xin chào, tôi là ${this.name} (Traditional).`);
// Hàm callback của setTimeout là một function truyền thống
setTimeout(function() {
// 'this' ở đây KHÔNG còn là đối tượng person nữa
// Trong non-strict mode (mặc định trình duyệt), this thường là đối tượng global (window)
// Trong strict mode (mặc định các module ES6), this là undefined
console.log(`Chào lần nữa, tôi là ${this?.name} (Trong setTimeout Traditional).`); // Dùng ?. để truy cập an toàn
}, 1000);
},
greetArrow: function() {
// 'this' ở đây là đối tượng person
console.log(`Xin chào, tôi là ${this.name} (Arrow).`);
// Hàm callback của setTimeout là một Arrow Function
setTimeout(() => {
// 'this' ở đây KHÔNG TẠO this riêng
// Nó kế thừa 'this' từ phạm vi cha (hàm greetArrow)
// Vì 'this' trong greetArrow là đối tượng person, nên 'this' ở đây cũng là person
console.log(`Chào lần nữa, tôi là ${this.name} (Trong setTimeout Arrow).`);
}, 1000);
}
};
console.log("Gọi hàm truyền thống:");
person.greetTraditional();
// Sau 1 giây, output sẽ là: "Chào lần nữa, tôi là undefined (Trong setTimeout Traditional)." (nếu strict mode)
// hoặc "Chào lần nữa, tôi là [tên biến global nếu có] (Trong setTimeout Traditional)." (nếu non-strict mode)
console.log("\nGọi Arrow Function:");
person.greetArrow();
// Sau 1 giây, output sẽ là: "Chào lần nữa, tôi là Bob (Trong setTimeout Arrow)."
Giải thích: Ví dụ trên minh họa rất rõ. Khi gọi person.greetTraditional()
, hàm callback bên trong setTimeout
(là hàm truyền thống) mất đi ngữ cảnh this
ban đầu, dẫn đến việc this.name
không truy cập được tên "Bob". Ngược lại, khi gọi person.greetArrow()
, hàm callback bên trong setTimeout
(là Arrow Function) giữ nguyên giá trị this
từ phạm vi ngoài (greetArrow
), nên nó vẫn trỏ đến đối tượng person
và truy cập được this.name
là "Bob".
Sự khác biệt này làm cho Arrow Functions trở thành lựa chọn tuyệt vời cho các hàm callback, đặc biệt là trong các trường hợp cần bảo toàn ngữ cảnh this
từ bên ngoài.
Ứng dụng thực tế và Khi nào sử dụng loại nào?
Sử dụng Arrow Functions khi:
- Bạn cần một cú pháp ngắn gọn, đặc biệt cho các hàm đơn giản hoặc hàm callback.
- Bạn muốn bảo toàn giá trị
this
từ phạm vi cha (ví dụ: trong các phương thức của object, khi làm việc với timers nhưsetTimeout
,setInterval
, hoặc xử lý sự kiện). - Khi làm việc với các phương thức xử lý mảng cấp cao như
map
,filter
,reduce
,forEach
, Arrow Functions giúp code cực kỳ cô đọng.
const numbers = [1, 2, 3, 4, 5]; // map với Arrow Function (implicit return) const squared = numbers.map(num => num * num); console.log("Bình phương:", squared); // Output: [ 1, 4, 9, 16, 25 ] // filter với Arrow Function (implicit return) const evens = numbers.filter(num => num % 2 === 0); console.log("Số chẵn:", evens); // Output: [ 2, 4 ]
Sử dụng Functions truyền thống khi:
- Bạn cần định nghĩa các phương thức cho một object và muốn
this
bên trong phương thức đó chính là object đó (đôi khi cú pháp Arrow Function trong trường hợp này lại gây nhầm lẫn nếu bạn muốnthis
là object chứa nó, mặc dù ví dụgreetArrow
ở trên cho thấy nó vẫn có thể hoạt động, nhưng nó dựa vàothis
của hàm chagreetArrow
, không phảithis
của chính nó như một method). - Bạn cần định nghĩa một constructor function để tạo đối tượng mới với từ khóa
new
. Arrow Functions không thể được sử dụng làm hàm tạo (new Person() sẽ lỗi nếu Person là Arrow Function
). - Bạn cần sử dụng đối tượng
arguments
bên trong hàm (Arrow Functions không có đối tượngarguments
riêng). - Khi khai báo hàm ở cấp cao nhất (global scope) và muốn tận dụng cơ chế hoisting (dù không khuyến khích lạm dụng).
- Bạn cần định nghĩa các phương thức cho một object và muốn
Comments