← Lập trình JavaScript cơ bản

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ớ

function declaration được hoisting - gọi chao() TRƯỚC dòng khai báo vẫn chạy. Còn function expression/arrow gán vào biến (const f = …) thì phải khai báo trước khi dùng, y như mọi biến khác.

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

Tạo 3 hàm trong vòng lặp, rồi gọi tất cả:
for (var i = 0; i < 3; i++)
fn[0]() → 3fn[1]() → 3fn[2]() → 3
Mọi closure dùng CHUNG một i → đều ra 3. 🐛
for (let i = 0; i < 3; i++)
fn[0]() → 0fn[1]() → 1fn[2]() → 2
Mỗi vòng có i RIÊNG → nhớ đúng 0…2. ✓

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

Bug “var trong vòng lặp” là một trong những lý do luôn dùng let/const (nối tiếp Bài 2). Với let, mỗi vòng lặp có một biến mới nên closure bắt đúng giá trị.

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ọithis
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

Đã làm chủ hàm, closure và this. Bài sau - “Mảng & object” - dùng hàm hạng nhất để xử lý dữ liệu: map/filter/reduce, destructuring, spread, và JSON.

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.

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.)

this KHÔNG cố định theo nơi viết hàm - nó phụ thuộc CÁCH HÀM ĐƯỢC GỌI. obj.fn()thisobj; gọi thẳng fn()thisundefined (strict mode / module); arrow lấy this từ scope bao ngoài (không có this riêng). call/apply/bind đặt this thủ công; new tạo this là object mới.

Dùng arrow cho callback ngắn và khi muốn GIỮ this của scope ngoài (vd setTimeout bên trong một method). Dùng function thường (hoặc method shorthand) khi cần this động - như method của object, hay handler cần this là phần tử DOM. ĐỪNG dùng arrow làm method nếu bạn cần this trỏ tới chính object đó.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

“Hàm là công dân hạng nhất (first-class)” nghĩa là gì?

  1. 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. 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ữ count riêng, không ảnh hưởng nhau (vd a=2, b=0).

  3. 3

    Bug var/let

    Dùng công cụ ở Bước 3 với n = 3: var ra gì, let ra gì? Giải thích.

    Hoàn thành khi: var[3, 3, 3] (chung một i); let[0, 1, 2] (mỗi vòng một i riêng).

  4. 4

    Lexical scope

    Viết hàm cha chứa biến x và hàm con in x. Gọi con ở nơi khác. Con thấy x không?

    Hoàn thành khi: Có - con thấy x theo NƠI VIẾT (trong cha), không phụ thuộc nơi gọi.

  5. 5

    this: method vs gọi thẳng

    Tạo object có method in this.ten. Gọi obj.m(); rồi const m = obj.m; m(). Khác gì?

    Hoàn thành khi: obj.m()thisobj (in đúng); m() gọi thẳng → thisundefined → lỗi/không có ten.

  6. 6

    arrow giữ this

    Trong một method, gọi setTimeout với arrow và với function thường, in this.ten. So sánh.

    Hoàn thành khi: arrow giữ this = object (in đúng); function thường mất ngữ cảnh (this undefined/global).