Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang xây một quán cà phê. Mỗi lần khách gọi một ly cà phê sữa đá, bạn không thể bắt nhân viên đi học lại từ đầu cách rang hạt, pha máy, rồi cho đá. Thay vào đó, bạn viết ra một công thức một lần, đặt tên cho nó là "cà phê sữa đá", và mỗi lần cần thì chỉ việc gọi tên công thức đó. Trong JavaScript, cái "công thức" ấy chính là function (hàm).
Function là một trong những viên gạch nền tảng quan trọng nhất của JavaScript. Một function là một thủ tục (procedure) — một tập hợp các câu lệnh thực hiện một nhiệm vụ hoặc tính toán ra một giá trị nào đó. Nếu bạn từng cảm thấy code của mình lặp đi lặp lại, rối rắm, sửa một chỗ phải sửa mười chỗ khác, thì lý do gần như chắc chắn là bạn chưa biết tổ chức code bằng function.
Ở những bài trước, bạn đã làm quen với JavaScript (Bài 1) và đã hiểu về biến cùng kiểu dữ liệu (Bài 8). Bây giờ chúng ta bước sang một tầng cao hơn: làm thế nào để đóng gói logic thành những khối có thể tái sử dụng. Đây là kỹ năng phân biệt giữa một người "viết được code chạy" và một người "viết được code chuyên nghiệp, dễ bảo trì". Trong suốt phần còn lại của khóa học — và trong cả sự nghiệp lập trình của bạn — bạn sẽ viết function mỗi ngày. Vì vậy, hãy dành thời gian nắm thật chắc bài này.
Khái niệm cốt lõi
Function là gì?
Function là một khối code được đặt tên, có thể nhận đầu vào (gọi là tham số — parameters), thực hiện một loạt thao tác, và thường trả về một đầu ra (qua từ khóa return). Bạn định nghĩa nó một lần, rồi "gọi" (call/invoke) nó bao nhiêu lần tùy thích.
function chao(ten) {
return "Xin chào, " + ten + "!";
}
console.log(chao("Lan")); // Xin chào, Lan!
console.log(chao("Minh")); // Xin chào, Minh!
Ở đây chao là tên function, ten là tham số, và phần sau return là giá trị trả về. Mỗi lần gọi chao(...), chúng ta truyền vào một đối số (argument) khác nhau và nhận lại kết quả tương ứng.
Các cách khai báo function
JavaScript cho bạn nhiều cách viết function, mỗi cách có sắc thái riêng.
1. Function Declaration (khai báo hàm)
function tinhDienTich(dai, rong) {
return dai * rong;
}
Đặc điểm nổi bật: nó được hoisting (kéo lên đầu) — bạn có thể gọi function này trước dòng khai báo nó trong code, và vẫn chạy được. Đây là cách phổ biến nhất cho các hàm tiện ích.
2. Function Expression (biểu thức hàm)
const tinhDienTich = function (dai, rong) {
return dai * rong;
};
Ở đây function được gán vào một biến. Nó không được hoisting như trên — bạn phải khai báo trước khi gọi. Cách này hữu ích khi muốn truyền function như một giá trị.
3. Arrow Function (hàm mũi tên — ES6)
const tinhDienTich = (dai, rong) => dai * rong;
Đây là cú pháp ngắn gọn, hiện đại, rất được ưa chuộng từ năm 2015 đến nay. Nếu thân hàm chỉ có một biểu thức, bạn có thể bỏ luôn {} và return — JavaScript tự động trả về kết quả. Arrow function còn có cách xử lý từ khóa this đặc biệt, nhưng ở giai đoạn này bạn chỉ cần nhớ: nó ngắn gọn và phù hợp cho các hàm nhỏ.
Tham số, đối số và giá trị trả về
- Tham số (parameter): tên biến trong định nghĩa hàm — ví dụ
dai,rong. - Đối số (argument): giá trị thực tế bạn truyền vào khi gọi — ví dụ
tinhDienTich(5, 3)thì5và3là đối số. return: kết thúc function và đẩy một giá trị ra ngoài. Nếu không córeturn, function trả vềundefined.
function tinhTienShip(khoangCach, giaMoiKm = 5000) {
return khoangCach * giaMoiKm;
}
tinhTienShip(10); // 50000 (dùng mặc định 5000)
tinhTienShip(10, 7000); // 70000 (ghi đè)
Phạm vi biến (Scope)
Biến khai báo bên trong một function chỉ tồn tại bên trong nó — gọi là local scope (phạm vi cục bộ). Bên ngoài không thể truy cập. Điều này rất quan trọng: nó giúp function của bạn "kín đáo", không vô tình làm hỏng biến của phần code khác.
function demo() {
let bian = "tôi sống trong hàm";
return bian;
}
// console.log(bian); // LỖI: bian không tồn tại ở đây
Vì sao function quan trọng đến vậy?
Có ba lý do cốt lõi:
- Tái sử dụng (Reusability) — viết một lần, dùng nhiều nơi.
- Dễ đọc, dễ bảo trì — chia nhỏ bài toán lớn thành các hàm nhỏ có tên rõ ràng.
- Tránh lặp lại (DRY — Don't Repeat Yourself) — khi cần sửa logic, bạn chỉ sửa một chỗ duy nhất.
Tình huống thực tế
Tình huống 1 — Tính phí ship của Tiki
Đội kỹ thuật của một sàn thương mại điện tử như Tiki cần tính phí giao hàng cho hàng triệu đơn mỗi ngày. Một bạn dev mới, chưa biết function, viết code tính phí lặp lại ở từng trang: trang giỏ hàng một đoạn, trang thanh toán một đoạn, trang xác nhận một đoạn. Đến khi công ty thay đổi chính sách "miễn phí ship cho đơn trên 300.000đ", bạn ấy phải đi tìm và sửa ở 8 chỗ khác nhau — và bỏ sót 2 chỗ, gây tính sai phí cho khách.
Giải pháp đúng là đóng gói logic vào một function duy nhất:
function tinhPhiShip(giaTriDon, khoangCachKm) {
if (giaTriDon >= 300000) return 0; // miễn phí
const phiCoBan = 15000;
const phiTheoKm = khoangCachKm * 2000;
return phiCoBan + phiTheoKm;
}
Bài học rút ra: Khi một quy tắc nghiệp vụ được tập trung vào một function, mọi nơi trong hệ thống đều gọi cùng một nguồn. Đổi chính sách? Sửa đúng một function, toàn bộ ứng dụng cập nhật theo. Đây chính là sức mạnh của nguyên tắc DRY.
Tình huống 2 — Định dạng giá tiền ở một startup fintech tại TP.HCM
Một startup ví điện tử ở Quận 1 hiển thị số tiền ở khắp nơi: số dư, lịch sử giao dịch, hóa đơn. Ban đầu mỗi màn hình tự nối chuỗi và thêm "đ" theo kiểu riêng, dẫn đến chỗ thì "1000000đ", chỗ thì "1,000,000 VND", chỗ thì "1.000.000đ" — trông rất thiếu chuyên nghiệp và khiến khách hàng nghi ngờ.
Lead engineer quyết định viết một function chuẩn hóa duy nhất:
function dinhDangTien(soTien) {
return soTien.toLocaleString("vi-VN") + "đ";
}
dinhDangTien(1000000); // "1.000.000đ"
dinhDangTien(50000); // "50.000đ"
Sau đó yêu cầu cả team: mọi nơi hiển thị tiền đều phải gọi dinhDangTien(). Chỉ trong một sprint, toàn bộ ứng dụng có giao diện tiền tệ nhất quán.
Bài học rút ra: Function không chỉ để tính toán — nó còn là công cụ chuẩn hóa. Khi một hành vi cần đồng nhất trên toàn hệ thống, hãy biến nó thành function và bắt mọi người dùng chung.
Tình huống 3 — Function như một "đối số" trong xử lý danh sách
Một bạn dev tại Đà Nẵng xây tính năng lọc danh sách khóa học theo mức giá. Bạn ấy phát hiện ra rằng trong JavaScript, function có thể được truyền vào function khác như một đối số (gọi là higher-order function):
const khoaHoc = [
{ ten: "JavaScript", gia: 499000 },
{ ten: "Python", gia: 299000 },
{ ten: "Scrum", gia: 799000 },
];
const khoaHocReGoc = khoaHoc.filter(function (kh) {
return kh.gia < 500000;
});
// Kết quả: JavaScript và Python
Hàm filter nhận vào một function nhỏ (callback) để quyết định giữ phần tử nào. Đây là cách viết hiện đại, súc tích, thay thế cho vòng lặp dài dòng.
Bài học rút ra: Trong JavaScript, function là "công dân hạng nhất" (first-class citizen) — có thể gán cho biến, truyền đi, trả về. Nắm được điều này mở ra cánh cửa tới phong cách lập trình hàm (functional programming) rất mạnh mẽ.
Hướng dẫn từng bước
Hãy cùng xây một function hoàn chỉnh: tính điểm trung bình của học viên và xếp loại.
Bước 1 — Xác định nhiệm vụ và đầu vào. Function nhận vào một mảng các điểm số, trả về điểm trung bình. Đặt tên rõ ràng: tinhDiemTrungBinh.
Bước 2 — Viết khung function.
function tinhDiemTrungBinh(dsDiem) {
}
Bước 3 — Xử lý trường hợp biên (edge case). Nếu mảng rỗng, đừng để chia cho 0.
function tinhDiemTrungBinh(dsDiem) {
if (dsDiem.length === 0) return 0;
}
Bước 4 — Tính tổng và trả về kết quả.
function tinhDiemTrungBinh(dsDiem) {
if (dsDiem.length === 0) return 0;
let tong = 0;
for (let diem of dsDiem) {
tong += diem;
}
return tong / dsDiem.length;
}
Bước 5 — Tạo một function thứ hai dùng kết quả của function thứ nhất. Đây là cách function "kết hợp" với nhau.
function xepLoai(dsDiem) {
const tb = tinhDiemTrungBinh(dsDiem);
if (tb >= 8) return "Giỏi";
if (tb >= 6.5) return "Khá";
if (tb >= 5) return "Trung bình";
return "Cần cố gắng";
}
console.log(xepLoai([9, 8, 7])); // "Khá" (tb = 8)
console.log(xepLoai([5, 6, 4])); // "Cần cố gắng"
Bước 6 — Kiểm thử với nhiều đầu vào khác nhau, bao gồm mảng rỗng [], để chắc chắn function hoạt động đúng trong mọi tình huống.
Bạn vừa thấy nguyên tắc vàng: mỗi function làm tốt một việc, rồi ghép chúng lại để giải bài toán lớn.
Lỗi thường gặp & mẹo
Lỗi 1 — Quên return. Rất nhiều người mới nhầm giữa console.log (in ra màn hình) và return (trả giá trị ra ngoài). Nếu function không có return, nó trả về undefined. Hãy nhớ: muốn dùng kết quả ở nơi khác thì phải return.
Lỗi 2 — Nhầm gọi hàm và tham chiếu hàm. chao là bản thân function, còn chao() là gọi nó. Viết const x = chao; khác hẳn const x = chao();. Nhầm lẫn này gây ra rất nhiều bug khó hiểu, đặc biệt với callback.
Lỗi 3 — Truyền sai số lượng đối số. JavaScript không báo lỗi nếu bạn truyền thiếu đối số — tham số bị thiếu sẽ là undefined, và phép tính có thể ra NaN. Hãy dùng giá trị mặc định để phòng ngừa.
Lỗi 4 — Hàm quá dài, làm quá nhiều việc. Nếu một function dài hơn 30–40 dòng và xử lý nhiều nhiệm vụ khác nhau, hãy tách nhỏ. Mỗi function nên có một trách nhiệm duy nhất.
Mẹo 1 — Đặt tên function bằng động từ. tinhPhiShip, kiemTraEmail, layDanhSach — tên nói rõ function làm gì giúp code tự giải thích chính nó.
Mẹo 2 — Giữ function "thuần" (pure) khi có thể. Một function thuần luôn trả về cùng kết quả với cùng đầu vào, và không thay đổi gì bên ngoài. Loại function này dễ kiểm thử và ít gây bug nhất.
Mẹo 3 — Dùng arrow function cho các callback ngắn, dùng function declaration cho các hàm tiện ích chính. Sự nhất quán giúp đồng đội đọc code dễ hơn.
Bài tập thực hành
Hãy tự code các bài sau (đừng chỉ đọc — gõ tay vào trình duyệt hoặc Node.js):
- Khởi động: Viết function
binhPhuong(n)trả vền * n. Gọi thử vớibinhPhuong(7).
- Giá trị mặc định: Viết function
tinhGiamGia(gia, phanTram = 10)trả về số tiền sau khi giảm. Kiểm thử vớitinhGiamGia(200000)vàtinhGiamGia(200000, 25).
- Trường hợp biên: Viết function
timMax(dsSo)trả về số lớn nhất trong một mảng. Đảm bảo xử lý đúng khi mảng rỗng (trả vềnull).
- Kết hợp function: Viết
dinhDangTien(soTien)(như tình huống 2) rồi viếthienThiHoaDon(dsGia)— nhận một mảng giá, tính tổng, và trả về chuỗi đã định dạng kèm "đ".
- Higher-order function (nâng cao): Cho mảng tên
["an", "binh", "cuong"], dùng.map()với một arrow function để biến mỗi tên thành chữ in hoa đầu ("An","Binh","Cuong").
Tóm tắt
Trong bài này, bạn đã học rằng function là khối code có thể tái sử dụng — viên gạch nền tảng để tổ chức chương trình JavaScript một cách chuyên nghiệp. Bạn đã nắm:
- Ba cách khai báo: function declaration (có hoisting), function expression, và arrow function (ngắn gọn, hiện đại).
- Cơ chế đầu vào/đầu ra: tham số, đối số, giá trị mặc định, và
return. - Scope: biến cục bộ chỉ sống bên trong function, giúp code an toàn và độc lập.
- Ba lý do dùng function: tái sử dụng, dễ bảo trì, và tránh lặp lại (DRY).
- Function là công dân hạng nhất: có thể truyền đi như đối số (higher-order function).
Hãy hoàn thành các bài tập trước khi sang bài tiếp theo — vì từ đây trở đi, gần như mọi đoạn code bạn viết đều sẽ nằm gọn trong một function nào đó.