Bài 4 · Vận dụng · 24 phút· Cập nhật 11/06/2026
Hàm, closure & this
Biên soạn bởi Nguyễn Anh Tuấn
Hàm trong JavaScript là công dân hạng nhất (first-class): scope, closure giữ trạng thái riêng, arrow function, và cách xác định this theo cách gọi hàm.
Trong JavaScript, hàm là một giá trị như number hay string: gán vào biến, bỏ vào mảng, truyền làm đối số, trả về từ hàm khác. Có ba cách viết hàm:
ba cách khai báo hàm
// 1) Function declaration (duoc hoisting)
function chao(ten) { return "Chao " + ten; }
// 2) Function expression (gan vao bien)
const chao2 = function (ten) { return "Chao " + ten; };
// 3) Arrow function (gon nhat)
const chao3 = (ten) => "Chao " + ten;
// Ham la GIA TRI: bo ca ba vao mang roi goi
const ds = [chao, chao2, chao3];
console.log(ds[0]("Lan")); // Chao Lan Kết quả khi chạy
Chao Lan
- ▸Hàm là giá trị: gán biến, bỏ vào mảng, truyền đi, trả về.
- ▸declaration, expression, arrow - ba cách viết cùng một ý.
- ▸Hàm nhận/trả về hàm khác gọi là higher-order function (nền của callback).
Hoisting: một khác biệt cần nhớ
JavaScript dùng lexical scope (phạm vi theo NƠI VIẾT): một hàm thấy được các biến ở nơi nó được viết ra - kể cả biến của các hàm bao quanh - chứ không phải nơi nó được gọi.
hàm con thấy biến của hàm cha
const ngoai = "A";
function cha() {
const trong = "B";
function con() {
// con() thay CA "ngoai" lan "trong" - theo noi VIET
console.log(ngoai, trong);
}
con();
}
cha(); // A B Kết quả khi chạy
A B
- ▸Lexical scope: hàm thấy biến theo nơi VIẾT (lồng nhau), không theo nơi gọi.
- ▸Hàm trong thấy biến hàm ngoài; ngược lại thì không.
- ▸Đây chính là nền tảng để hiểu closure ở bước sau.
Closure là hàm “nhớ” được scope nơi nó sinh ra - kể cả khi hàm cha đã chạy xong. Nhờ vậy ta tạo được trạng thái riêng tư:
makeCounter - count riêng tư, được nhớ
function makeCounter() {
let count = 0; // rieng tu: chi ham ben trong thay
return {
inc: () => ++count,
get: () => count,
};
}
const a = makeCounter();
const b = makeCounter(); // doc lap, count rieng
a.inc();
a.inc();
console.log(a.get(), b.get()); // 2 0 Kết quả khi chạy
2 0
Closure cũng giải thích một bug kinh điển khi tạo hàm trong vòng lặp. Đổi số lượng hàm và xem khác biệt giữa var và let:
vì sao var sai, let đúng
// var: moi closure dung CHUNG mot i
var fns = [];
for (var i = 0; i < 3; i++) fns.push(() => i);
console.log(fns.map((f) => f())); // [3, 3, 3] (bug)
// let: moi vong mot i rieng
const gns = [];
for (let j = 0; j < 3; j++) gns.push(() => j);
console.log(gns.map((f) => f())); // [0, 1, 2] (dung) - ▸Closure = hàm + scope nơi nó sinh ra (vẫn sống sau khi hàm cha kết thúc).
- ▸Dùng để giấu trạng thái riêng tư (count trong makeCounter).
- ▸var trong vòng lặp dùng chung một biến → mọi closure ra giá trị cuối; let mỗi vòng một biến.
Thêm lý do dùng let
Arrow function viết ngắn hơn; nếu thân chỉ là một biểu thức thì tự return (không cần return). Điểm đặc biệt quan trọng: arrow không có this riêng - nó lấy this của scope bao ngoài (rất hữu ích, xem Bước 5).
cú pháp arrow
const binhPhuong = (x) => x * x; // implicit return
const tong = (a, b) => a + b;
const inHi = () => console.log("hi"); // khong tham so -> ()
// than nhieu dong thi can { } va return ro rang:
const phanLoai = (n) => {
if (n < 0) return "am";
return n === 0 ? "khong" : "duong";
};
console.log(binhPhuong(5), phanLoai(-2)); // 25 am Kết quả khi chạy
25 am
- ▸Một biểu thức → tự return; nhiều dòng → cần { } và return.
- ▸Không có this/arguments riêng - arrow lấy this từ scope ngoài.
- ▸Đừng dùng arrow làm method nếu cần this trỏ tới chính object.
Đây là điểm gây nhầm nhất. this KHÔNG cố định - nó được quyết định lúc gọi hàm, theo cách gọi:
cùng một hàm, this khác nhau theo cách gọi
const nguoi = {
ten: "Lan",
chao() { return "Toi la " + this.ten; },
};
console.log(nguoi.chao()); // "Toi la Lan" (this = nguoi)
const f = nguoi.chao; // tach method ra bien
console.log(f()); // loi/undefined (goi thang -> this = undefined)
// dat this thu cong:
console.log(nguoi.chao.call({ ten: "Hoa" })); // "Toi la Hoa" | Cách gọi | this là |
|---|---|
| obj.fn() | obj (object đứng trước dấu chấm) |
| fn() | undefined (strict mode / module) |
| fn.call(x) / fn.apply(x) | x (đặt thủ công) |
| fn.bind(x) | x (cố định vĩnh viễn) |
| new Fn() | object mới vừa tạo |
| () => … | this của scope bao ngoài (lexical) |
- ▸this được set lúc GỌI, không phải lúc viết hàm.
- ▸Tách method ra biến/callback hay làm “mất” this → dùng bind hoặc arrow để giữ.
- ▸Arrow không có this riêng: lấy this lexical - hợp cho callback bên trong method.
Tiếp theo
Câu hỏi thường gặp
Nghĩa là hàm cũng là một GIÁ TRỊ như number hay string: gán cho biến, bỏ vào mảng, truyền làm đối số, và trả về từ hàm khác - đều được. Đây là nền của callback, closure và higher-order function (hàm nhận/trả về hàm).
function declaration (function f() {}) được “hoisting”: gọi trước dòng khai báo vẫn chạy. function expression và arrow gán vào biến (const f = …) thì phải khai báo TRƯỚC khi dùng. Arrow ngắn gọn hơn và KHÔNG có this/arguments riêng (xem Bước 4-5).
Closure là một hàm “nhớ” được scope nơi nó SINH RA - kể cả khi hàm cha đã chạy xong. Nhờ đó hàm con vẫn truy cập biến của hàm cha (vd makeCounter giữ một count riêng tư). Closure không phải cú pháp đặc biệt - nó là hệ quả tự nhiên của lexical scope.
Vì var phạm vi theo HÀM: mọi closure tạo trong vòng lặp dùng CHUNG một biến i; sau vòng lặp i = n nên gọi cái nào cũng ra n. Dùng let (phạm vi block) thì mỗi vòng có một i RIÊNG → mỗi closure nhớ đúng giá trị của nó. (Thêm một lý do nên dùng let, nối tiếp Bài 2.)
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
“Hàm là công dân hạng nhất (first-class)” nghĩa là gì?
- 1
Hàm là giá trị
Gán một hàm vào biến, rồi bỏ 2 hàm vào một mảng và gọi qua mảng (vd
ds[0]("Lan")).Hoàn thành khi: Gọi được hàm qua biến và qua phần tử mảng - chứng tỏ hàm là một giá trị.
- 2
Closure đếm
Viết
makeCounter()trả về{ inc, get }. Tạo 2 counter và tăng độc lập.Hoàn thành khi: Hai counter giữ
countriêng, không ảnh hưởng nhau (vda=2, b=0). - 3
Bug var/let
Dùng công cụ ở Bước 3 với
n = 3:varra gì,letra gì? Giải thích.Hoàn thành khi:
var→[3, 3, 3](chung mộti);let→[0, 1, 2](mỗi vòng mộtiriêng). - 4
Lexical scope
Viết hàm cha chứa biến
xvà hàm con inx. Gọi con ở nơi khác. Con thấyxkhông?Hoàn thành khi: Có - con thấy
xtheo NƠI VIẾT (trong cha), không phụ thuộc nơi gọi. - 5
this: method vs gọi thẳng
Tạo object có method in
this.ten. Gọiobj.m(); rồiconst m = obj.m; m(). Khác gì?Hoàn thành khi:
obj.m()→thislàobj(in đúng);m()gọi thẳng →thislàundefined→ lỗi/không cóten. - 6
arrow giữ this
Trong một method, gọi
setTimeoutvới arrow và với function thường, inthis.ten. So sánh.Hoàn thành khi: arrow giữ
this= object (in đúng); function thường mất ngữ cảnh (thisundefined/global).