Chương 03 · Functions, Scope & Closures

Hàm, phạm vi và bao đóng

Function là first-class citizen của JavaScript — có thể gán biến, truyền tham số, trả về từ function khác. Mỗi function "ghi nhớ" scope nơi nó được tạo, tạo ra closure — công cụ mạnh nhất nhưng cũng dễ gây bug nhất của ngôn ngữ. Chương này gỡ rối 4 cách viết function, 4 rule của this và scope chain.

Độ dài: ~950 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1-2
🎯 Mục tiêu chương
  • Biết 4 cách định nghĩa function và phân biệt rõ qua bảng so sánh: hoisting, this, arguments, new, prototype.
  • Hiểu "function là first-class" — gán biến, truyền argument, return, lưu trong cấu trúc dữ liệu.
  • Master parameters modern: default, rest ...args, destructuring params.
  • Phân biệt arrow function với function thường — 4 điểm khác biệt then chốt.
  • Master this binding 4 rule: default, implicit, explicit, new — và ngoại lệ arrow function.
  • Hiểu scope chain, hoisting và Temporal Dead Zone trên function declaration vs expression.
  • Định nghĩa closure chính xác và áp dụng 5 pattern: data privacy, factory, memoization, once, currying.
  • Biết IIFE — vẫn còn trong code legacy, đôi khi cần.

1. Bốn cách định nghĩa function

JavaScript cho phép viết function theo ít nhất 4 cú pháp, mỗi cú pháp có hành vi khác nhau. Hiểu khác biệt là điều kiện tiên quyết để đọc bất kỳ codebase nào.

1.1. Function declaration

Cú pháp truyền thống. Bắt đầu bằng keyword function ở vị trí statement.

Function declaration
function add(a, b) {
  return a + b;
}

add(2, 3);  // 5

// Hoisted toàn bộ — gọi trước dòng khai báo vẫn chạy:
sayHi();          // 'Xin chào!' ← vẫn chạy
function sayHi() {
  console.log('Xin chào!');
}

1.2. Function expression

Function nằm ở vị trí biểu thức (ví dụ vế phải của =). Có thể named hoặc anonymous. Không hoisted như declaration.

Function expression
// Anonymous
const add = function(a, b) {
  return a + b;
};

// Named (tên chỉ dùng được bên trong function — hữu ích cho recursion + debug)
const fact = function factorial(n) {
  return n <= 1 ? 1 : n * factorial(n - 1);
};

// KHÔNG hoisted — gọi trước dòng khai báo lỗi:
greet();  // ReferenceError (let/const) hoặc TypeError (var)
const greet = function() { console.log('hi'); };

1.3. Arrow function

Cú pháp ES6, ngắn gọn. Khác function thường ở 4 điểm: không có this riêng, không có arguments, không new được, không có prototype. Chi tiết ở mục 4.

Arrow function syntax
// Body block
const add = (a, b) => {
  return a + b;
};

// Body biểu thức — implicit return (không cần `return` keyword)
const add2 = (a, b) => a + b;

// 1 tham số — bỏ ngoặc
const square = x => x * x;

// 0 tham số — vẫn cần ngoặc rỗng
const hi = () => console.log('hi');

// Trả về object literal — phải bọc ngoặc tròn để JS không hiểu là block
const point = (x, y) => ({ x, y });

// Multi-line body block:
const processUser = (user) => {
  const name = user.name.trim();
  const age = user.age ?? 0;
  return { name, age };
};

1.4. Method shorthand (object & class)

Cú pháp ES6 viết method gọn trong object literal hoặc class body. Hành vi giống function expression thường, chỉ khác cách viết.

Method shorthand
// Cách cũ:
const user = {
  name: 'An',
  greet: function() { console.log(this.name); }
};

// Shorthand ES6:
const user2 = {
  name: 'An',
  greet() { console.log(this.name); }   // ← bỏ `: function`
};

// Trong class:
class User {
  constructor(name) { this.name = name; }
  greet() { console.log(this.name); }
}

1.5. Bảng so sánh 4 cách

Declaration Expression Arrow Method shorthand
HoistingFull (cả tên + body)Chỉ tên biến (var = undefined; let/const TDZ)Như expressionN/A (nằm trong object)
this riêngKhông (lexical)
argumentsKhông
new đượcKhông
prototypeKhông
Có tên cho stack traceKhi named hoặc gán biếnKhi gán biến
🧠 Mental model — chọn cú pháp nào
  • Top-level helper dùng nhiều chỗ → function declaration (hoisted, dễ debug).
  • Callback ngắn truyền vào map/filter/setTimeout → arrow.
  • Method của object/class cần this → method shorthand.
  • Function gán điều kiện (vd: const fn = cond ? a : b) → expression.

2. Function là first-class citizen

Trong JS, function là giá trị như string hay number. Có thể:

  • Gán cho biến.
  • Truyền làm argument cho function khác.
  • Trả về từ function (higher-order function).
  • Lưu trong array, object, Map.
First-class examples
// 1. Gán cho biến
const double = x => x * 2;

// 2. Truyền làm argument — callback
[1, 2, 3].map(double);   // [2, 4, 6]

// 3. Return function — higher-order function
function multiplier(n) {
  return x => x * n;     // trả về function
}
const triple = multiplier(3);
triple(10);  // 30

// 4. Lưu trong array/object
const ops = [
  x => x + 1,
  x => x * 2,
  x => x - 3,
];
const result = ops.reduce((acc, fn) => fn(acc), 10);
// (10+1)*2 - 3 = 19

const handlers = {
  onClick: () => console.log('click'),
  onHover: () => console.log('hover'),
};
handlers.onClick();
🔀 So sánh với Dart & Python

Dart cũng coi function là first-class — có type Function và arrow (x) => x * 2. Tuy nhiên Dart phân biệt rõ tham số kiểu num Function(num).

Python tương tự (def hoặc lambda), function là object — có thể gán, truyền, trả về. Nhưng lambda Python chỉ chứa 1 biểu thức.

3. Parameters: default, rest, destructuring

ES6 thêm 3 cú pháp parameter rất tiện. Cần master cả 3.

3.1. Default parameter

Gán giá trị mặc định khi argument là undefined.

Default parameter
function greet(name = 'khách', greeting = 'Xin chào') {
  return `${greeting}, ${name}!`;
}

greet();                // 'Xin chào, khách!'
greet('An');            // 'Xin chào, An!'
greet('An', 'Chào');    // 'Chào, An!'

// Quan trọng: default chỉ áp dụng với `undefined`, KHÔNG với `null`
greet(null);            // 'Xin chào, null!' 🔥 (null không trigger default)
greet(undefined);       // 'Xin chào, khách!'

// Default có thể là biểu thức, thậm chí gọi function khác:
function log(msg, time = Date.now()) {
  console.log(time, msg);
}

3.2. Rest parameter ...args

Gom các argument còn lại thành 1 array. Khác arguments cũ (không phải array thực, không có trong arrow function).

Rest parameter
function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}

sum(1, 2, 3);          // 6
sum(1, 2, 3, 4, 5);    // 15

// Kết hợp tham số thường + rest (rest phải ở CUỐI):
function tag(label, ...items) {
  return `${label}: ${items.join(', ')}`;
}
tag('fruits', 'apple', 'banana');  // 'fruits: apple, banana'

// So với arguments cũ:
function oldSum() {
  // arguments là array-like, không có .reduce/.map
  const arr = Array.from(arguments);
  return arr.reduce((a, b) => a + b, 0);
}

3.3. Destructuring parameter

"Tháo dỡ" object/array ngay tại signature. Cực kỳ tiện cho config object.

Destructuring parameter
// Object destructuring
function createUser({ name, age = 18, role = 'user' }) {
  return { name, age, role };
}

createUser({ name: 'An', age: 25 });
// { name: 'An', age: 25, role: 'user' }

// Default cho cả object (tránh lỗi destructure undefined)
function init({ debug = false } = {}) {
  return debug;
}
init();           // false (không lỗi nhờ `= {}`)

// Array destructuring
function swap([a, b]) {
  return [b, a];
}
swap([1, 2]);   // [2, 1]

// Rename khi destructure
function printName({ name: fullName }) {
  console.log(fullName);
}

4. Arrow function vs function thường

Arrow function trông giống "function viết gọn" nhưng khác về 4 điểm hành vi. Hiểu kỹ là điều bắt buộc.

4.1. Không có this riêng — lexical this

Arrow function không tạo binding this mới. Nó "thừa kế" this từ scope bao ngoài tại thời điểm định nghĩa (lexical).

Lexical this
const user = {
  name: 'An',
  hobbies: ['code', 'gym'],

  // ❌ Function thường: this = undefined trong callback
  printBad() {
    this.hobbies.forEach(function(h) {
      console.log(this.name, h);  // `this` ở đây ≠ user
    });
  },

  // ✅ Arrow: kế thừa this từ printGood
  printGood() {
    this.hobbies.forEach(h => {
      console.log(this.name, h);  // `this` = user
    });
  },
};

4.2. Không có arguments

function normal() {
  console.log(arguments);  // Arguments(3) [1, 2, 3]
}
normal(1, 2, 3);

const arrow = () => {
  console.log(arguments);  // ReferenceError (hoặc kế thừa từ scope ngoài nếu có)
};

// Thay thế: dùng rest params
const arrowFix = (...args) => console.log(args);
arrowFix(1, 2, 3);  // [1, 2, 3] (array thực)

4.3. Không new được

function Point(x, y) {
  this.x = x;
  this.y = y;
}
const p = new Point(1, 2);  // OK

const ArrowPoint = (x, y) => {
  this.x = x;
};
new ArrowPoint(1, 2);  // ❌ TypeError: ArrowPoint is not a constructor

4.4. Không có prototype

function foo() {}
console.log(foo.prototype);    // { constructor: foo }

const bar = () => {};
console.log(bar.prototype);    // undefined
💡 Khi nào dùng arrow, khi nào dùng function
  • Arrow: callback ngắn (map, filter, setTimeout), khi muốn giữ this của scope ngoài, function không cần new.
  • Function thường: method của object/class (cần this riêng), constructor, khi cần hoisting, khi cần arguments.

5. this binding — 4 rule

this trong JavaScript không phải "instance của class hiện tại" như Java. Nó được quyết định tại thời điểm gọi function, dựa trên cách gọi. Có 4 rule theo thứ tự ưu tiên:

Ưu tiênRuleCú phápthis là gì
1 (cao nhất)new bindingnew Foo()Object mới được tạo
2Explicit bindingfn.call(obj), fn.apply(obj), fn.bind(obj)()Object truyền vào
3Implicit bindingobj.fn()Object trước dấu chấm
4 (mặc định)Default bindingfn() độc lậpundefined (strict mode) hoặc globalThis (sloppy)
Ngoại lệArrow functionKhông tạo binding mớiKế thừa từ scope bao ngoài (lexical)

5.1. Default binding

Default binding
'use strict';

function show() {
  console.log(this);
}

show();   // strict: undefined; sloppy: window/global

// Hidden case — vẫn là default binding:
const obj = {
  name: 'An',
  greet() { console.log(this); }
};
const fn = obj.greet;
fn();    // undefined — vì không có obj. trước fn()

5.2. Implicit binding

Khi gọi với cú pháp obj.method(), this = obj (object trước dấu chấm).

const user = {
  name: 'An',
  greet() { console.log(this.name); }
};

user.greet();   // 'An' (this = user)

// Chain — `this` luôn là object cuối cùng trước dấu chấm:
const a = { name: 'A', fn: user.greet };
const b = { name: 'B', inner: a };

b.inner.fn();  // 'A' (this = a, không phải b)

5.3. Explicit binding — call/apply/bind

call / apply / bind
function greet(greeting, punct) {
  console.log(`${greeting}, ${this.name}${punct}`);
}

const user = { name: 'An' };

// call(thisArg, ...args)
greet.call(user, 'Xin chào', '!');  // 'Xin chào, An!'

// apply(thisArg, [args])
greet.apply(user, ['Chào', '.']);    // 'Chào, An.'

// bind(thisArg, ...args) — trả về function MỚI, không gọi ngay
const greetAn = greet.bind(user, 'Hi');
greetAn('?');   // 'Hi, An?'
greetAn('!');   // 'Hi, An!' (this đã bị "khóa" với user vĩnh viễn)

5.4. new binding

Khi gọi với new, JS thực hiện 4 bước:

  1. Tạo object mới rỗng.
  2. Link __proto__ của object mới tới Foo.prototype.
  3. Gán this = object mới, chạy body function.
  4. Nếu function không return object, tự động return object mới.
function User(name) {
  // this = {} mới
  this.name = name;
  // return this ngầm
}

const u = new User('An');
console.log(u);  // User { name: 'An' }

5.5. Ngoại lệ: arrow function

Arrow function bỏ qua mọi rule trên. this luôn lấy từ scope bao ngoài tại thời điểm định nghĩa.

const obj = {
  name: 'An',
  arrow: () => console.log(this?.name),
  regular() { console.log(this.name); }
};

obj.arrow();    // undefined (this kế thừa từ global, không phải obj)
obj.regular();  // 'An'

// Cả bind cũng KHÔNG đổi được this của arrow:
const bound = obj.arrow.bind({ name: 'B' });
bound();           // vẫn undefined
🔥 Gotcha — tách method khỏi object

Mọi lúc gán const fn = obj.method rồi gọi fn(), bạn mất implicit binding. Đây là nguồn bug kinh điển.

3 cách fix:

  1. const fn = obj.method.bind(obj)
  2. const fn = () => obj.method() (closure)
  3. Trong class, gán method trong constructor: this.method = this.method.bind(this)

6. Scope: global, function, block

"Scope" = vùng nhìn thấy biến. JavaScript có 3 loại:

ScopeTạo bởiÁp dụng cho
GlobalTop-level của script/moduleMọi khai báo top-level
FunctionMỗi function bodyvar, function declaration, parameters, this, arguments
Block{ } — if, for, while, block thườngChỉ let, const, class
Scope demo
const g = 'global';          // global scope

function outer() {
  const f = 'function';      // function scope

  if (true) {
    var   v = 'var-in-block';  // FUNCTION scope (var ignore block!)
    let   l = 'let-in-block';  // block scope
    const c = 'const-in-block'; // block scope
  }

  console.log(v);  // 'var-in-block' ← var leak ra function
  console.log(l);  // ReferenceError
  console.log(c);  // ReferenceError
}
🧠 Mental model — scope chain

Mỗi scope có "tham chiếu" tới scope cha. Khi truy cập biến, JS tìm theo chuỗi: local scope → parent scope → ... → global scope. Tìm thấy ở đâu thì dùng ở đó, không tìm thấy thì throw ReferenceError.

Quan trọng: scope chain xác định tại thời điểm định nghĩa, không phải khi gọi (lexical scoping).

Scope chain
const a = 1;          // global

function outer() {
  const b = 2;        // outer

  function inner() {
    const c = 3;      // inner
    console.log(a, b, c);  // 1 2 3 — chain: inner → outer → global
  }
  inner();
}
outer();

7. Scope chain & hoisting

Hoisting = JS engine "đẩy" khai báo lên đầu scope trước khi chạy code. Nhưng cách hoist khác nhau giữa các loại khai báo.

Khai báoHoistedInit value khi hoistTruy cập trước dòng khai báo
function fn() {}Full (cả body)function objectOK, chạy được
var xundefinedTrả undefined (không lỗi)
let x / const x(không init)ReferenceError (TDZ)
var fn = function(){}Chỉ tên varundefinedTypeError: fn is not a function
const fn = () => {}Chỉ tên const, TDZ(không init)ReferenceError
Hoisting demo
// ✅ Function declaration — hoist full
sayHi();   // 'hi'
function sayHi() { console.log('hi'); }

// ⚠️  var + function expression — chỉ hoist tên
sayHo();   // ❌ TypeError: sayHo is not a function
var sayHo = function() { console.log('ho'); };
// Lúc gọi: sayHo là undefined → undefined() → TypeError

// ❌ const/let arrow — TDZ
sayHey();  // ❌ ReferenceError: Cannot access 'sayHey' before initialization
const sayHey = () => console.log('hey');
💡 Quy tắc thực dụng

Khai báo trước khi dùng — luôn đúng dù ngôn ngữ nào. Đừng dựa vào function hoisting để viết code "gọi trước, định nghĩa sau", rất khó đọc.

8. Closure — function nhớ scope nó được tạo

Closure = function + reference tới scope nơi nó được tạo ra. Khi function được trả ra khỏi scope cha, scope đó không bị thu hồi — function vẫn truy cập được các biến bên trong.

Ví dụ kinh điển — counter:

makeCounter
function makeCounter() {
  let n = 0;             // biến local của makeCounter
  return () => ++n;     // inner function "đóng gói" biến n
}

const c1 = makeCounter();
c1();  // 1
c1();  // 2
c1();  // 3 — n vẫn tồn tại sau khi makeCounter return!

const c2 = makeCounter();  // scope mới, n mới
c2();  // 1 — c1 và c2 độc lập
c1();  // 4
🧠 Mental model — vì sao closure tồn tại

Trong garbage-collected language, biến bị thu hồi khi không còn ai tham chiếu. Khi makeCounter return, biến n vẫn được inner function tham chiếu — nên không bị thu hồi.

Đây không phải "leak". Khi biến c1 bị gán giá trị khác hoặc ra khỏi scope, inner function bị thu hồi → n mới được thu hồi.

9. Closure patterns

9.1. Data privacy — biến private

Trước class field #private (ES2022), closure là cách duy nhất tạo state thật sự private.

Data privacy
function createAccount(initialBalance) {
  let balance = initialBalance;   // private — không cách nào truy cập trực tiếp

  return {
    deposit(amount) { balance += amount; },
    withdraw(amount) {
      if (amount > balance) throw new Error('Insufficient');
      balance -= amount;
    },
    getBalance() { return balance; }
  };
}

const acc = createAccount(100);
acc.deposit(50);
acc.getBalance();   // 150
acc.balance;        // undefined — không có property này

9.2. Factory function

Tạo nhiều function "biến thể" từ 1 template.

function multiplier(factor) {
  return x => x * factor;
}

const double = multiplier(2);
const triple = multiplier(3);
const half   = multiplier(0.5);

[1, 2, 3].map(double);  // [2, 4, 6]
[10, 20].map(half);     // [5, 10]

9.3. Memoization — cache kết quả

memoize
function memoize(fn) {
  const cache = new Map();   // private cache, sống cùng wrapped fn
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const slowSquare = (n) => {
  for (let i = 0; i < 1e7; i++) {}  // giả lập chậm
  return n * n;
};

const fastSquare = memoize(slowSquare);
fastSquare(5);   // chậm lần đầu
fastSquare(5);   // instant — đọc cache

9.4. Once — gọi 1 lần duy nhất

function once(fn) {
  let called = false;
  let result;
  return (...args) => {
    if (!called) {
      result = fn(...args);
      called = true;
    }
    return result;
  };
}

const init = once(() => console.log('Khởi tạo'));
init();   // 'Khởi tạo'
init();   // (không in gì, trả lại undefined cached)

9.5. Currying

Chuyển fn(a, b, c) thành fn(a)(b)(c). Cho phép partial application.

curry
// Curry thủ công cho 3 tham số:
const add = a => b => c => a + b + c;
add(1)(2)(3);   // 6

// Curry tự động cho bất kỳ arity:
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return (...next) => curried(...args, ...next);
  };
}

const sum3 = (a, b, c) => a + b + c;
const curriedSum = curry(sum3);

curriedSum(1)(2)(3);   // 6
curriedSum(1, 2)(3);   // 6
curriedSum(1)(2, 3);   // 6
curriedSum(1, 2, 3);   // 6

10. IIFE — Immediately Invoked Function Expression

Function được "định nghĩa và gọi ngay lập tức". Trước ES6 module, đây là cách phổ biến để tạo scope riêng.

IIFE
// Cú pháp truyền thống — bọc trong ngoặc
(function() {
  const secret = 42;   // scope riêng, không leak ra global
  console.log(secret);
})();

// Arrow IIFE
(() => {
  const x = 1;
  console.log(x);
})();

// Truyền argument vào IIFE
((name) => {
  console.log(`Hello ${name}`);
})('JS');

// Trả về giá trị
const config = (() => {
  const env = process.env.NODE_ENV;
  return { debug: env !== 'production' };
})();
📘 IIFE thời đại module

Trước ES6, JS không có module — tất cả script chia sẻ global scope. IIFE giải quyết bằng cách tạo scope riêng. Bây giờ với import/export (chương 8), mỗi module đã có scope riêng → IIFE ít cần thiết. Tuy vậy vẫn hữu ích cho:

  • Async top-level cũ: (async () => { await ... })() (trước top-level await).
  • Khởi tạo có logic: const value = (() => { ... return ...; })();
  • Code phải chạy trong <script> tag không có type="module".

Bài tập

Bài 1 — 3 cách viết add + hoisting

Viết function add(a, b) theo cả 3 cách: function declaration, function expression (gán const), arrow function. Trong mỗi file/snippet, thử gọi trước dòng khai báo và quan sát:

  • Cách nào chạy được, cách nào throw, throw lỗi gì?
Đáp án
// 1. Declaration — hoisted full, gọi trước OK
console.log(add1(2, 3));   // 5
function add1(a, b) { return a + b; }

// 2. Expression với const — TDZ, gọi trước → ReferenceError
console.log(add2(2, 3));   // ReferenceError: Cannot access 'add2' before initialization
const add2 = function(a, b) { return a + b; };

// 3. Arrow với const — cũng TDZ → ReferenceError
console.log(add3(2, 3));   // ReferenceError
const add3 = (a, b) => a + b;

Mẹo: nếu thay const bằng var ở case 2 và 3, lỗi sẽ thành TypeError: add2 is not a function (vì var hoist tên với value undefined, gọi undefined(2,3) sinh TypeError).

Bài 2 — Mất this và 3 cách fix

Cho:

const user = {
  name: 'An',
  greet() { console.log(`Hi, ${this.name}`); }
};

const fn = user.greet;
fn();   // ?

Hỏi: this trong fn() là gì? Fix bằng 3 cách độc lập: bind, arrow wrapper, closure capture.

Đáp án

this = undefined (strict mode) hoặc globalThis (sloppy). Khi tách method khỏi object, mất implicit binding.

Fix 1 — bind:

const fn = user.greet.bind(user);
fn();  // 'Hi, An'

Fix 2 — arrow wrapper:

const fn = () => user.greet();
fn();  // 'Hi, An' (arrow giữ this=user qua implicit call)

Fix 3 — closure capture:

const fn = function() {
  const self = user;       // capture qua closure
  self.greet();
};
fn();  // 'Hi, An'

Bài 3 — once(fn)

Viết once(fn): trả function chỉ chạy fn lần đầu, các lần sau trả kết quả đã cache. Test với:

const init = once(() => {
  console.log('init!');
  return { ready: true };
});

init();  // in 'init!', trả { ready: true }
init();  // KHÔNG in, trả { ready: true } đã cache
init();  // idem
Đáp án
function once(fn) {
  let called = false;
  let result;
  return function(...args) {
    if (!called) {
      result = fn.apply(this, args);
      called = true;
    }
    return result;
  };
}

Mẹo: dùng function thường + fn.apply(this, args) để pass-through this nếu caller gọi như method. Nếu không cần, arrow + fn(...args) đủ.

Bài 4 — curry(fn)

Viết curry(fn) chuyển fn(a, b, c) thành dạng có thể gọi fn(a)(b)(c), fn(a, b)(c), fn(a)(b, c) hay fn(a, b, c) đều ra kết quả.

Hint: dùng fn.length (arity) và đệ quy.

Đáp án
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...next) => curried.apply(this, [...args, ...next]);
  };
}

const sum = (a, b, c) => a + b + c;
const c = curry(sum);

c(1)(2)(3);    // 6
c(1, 2)(3);    // 6
c(1)(2, 3);    // 6
c(1, 2, 3);    // 6

Ý tưởng: gom args qua mỗi lần gọi, khi nào đủ arity thì gọi fn.

Bài 5 — Fix bug var i trong setTimeout

Code dưới in 3 3 3 thay vì 0 1 2. Giải thích vì sao và fix bằng 2 cách:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Giải thích + 2 cách fix

Vì sao: var i là function scope (hoặc global), không phải block. Cả 3 callback setTimeout chia sẻ cùng 1 binding i. Khi loop kết thúc, i === 3. Sau đó callback chạy, đọc i hiện tại → in 3 3 3.

Fix 1 — đổi var sang let:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2 — let tạo binding MỚI mỗi iteration

Lý do: let trong header for được spec đặc biệt — mỗi iteration tạo binding mới. Closure capture binding riêng → in đúng.

Fix 2 — IIFE tạo scope riêng:

for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 0);
  })(i);
}
// 0 1 2 — mỗi iteration IIFE tạo `j` mới

Cách 3 dùng setTimeout(fn, ms, ...extraArgs): setTimeout((j) => console.log(j), 0, i) — capture qua tham số runtime.

Quiz

Q1

Function declaration và function expression khác nhau gì về hoisting?

Xem đáp án
✓ Đáp án

Function declaration được hoist toàn bộ (cả tên + body) — có thể gọi trước dòng khai báo.

Function expression chỉ hoist tên biến (theo rule của var/let/const). Body chưa tồn tại. Gọi trước dòng khai báo: varTypeError (undefined không phải function), let/constReferenceError (TDZ).

Q2

Arrow function có this riêng không?

Xem đáp án
✓ Đáp án

Không. Arrow function không tạo binding this mới — nó dùng this của scope bao ngoài tại thời điểm định nghĩa (lexical this).

Hệ quả: call/apply/bind không thay đổi được this của arrow. Cũng không thể dùng new với arrow.

Q3

Code sau in gì (strict mode)?

function foo() { console.log(this); }
foo();
Xem đáp án
✓ Đáp án

undefined. Đây là default binding — gọi function độc lập (không qua obj.fn(), new, call/apply/bind) trong strict mode thì this = undefined.

Trong sloppy mode (không có 'use strict'), this = globalThis (window trong browser).

Q4

Closure có giải phóng biến của scope cha sau khi function cha trả về không?

Xem đáp án
✓ Đáp án

Không — chừng nào inner function vẫn còn được tham chiếu, các biến trong scope cha mà nó "đóng gói" vẫn còn sống. Đó chính là cơ chế closure.

Khi inner function bị thu hồi (mất hết reference), scope cha mới được garbage collector dọn dẹp. Engine hiện đại (V8) còn tối ưu hơn: chỉ giữ các biến thực sự được closure tham chiếu, không giữ toàn bộ scope.

Q5

Code sau in gì?

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Xem đáp án
✓ Đáp án

0 1 2. let trong header for tạo binding mới mỗi iteration — mỗi callback closure capture binding riêng → in giá trị đúng.

Đổi sang var sẽ in 3 3 3 vì cả 3 callback chia sẻ cùng 1 biến.

Q6

Dùng new với arrow function được không?

Xem đáp án
✓ Đáp án

Không. Arrow function không có [[Construct]] internal slot, không có prototype property, không tạo this binding. new trên arrow throw TypeError: X is not a constructor.

Để tạo constructor, dùng function declaration/expression hoặc class (chương 4).

Q7

const fn = obj.method; fn.call(obj)obj.method() khác nhau gì?

Xem đáp án
✓ Đáp án

Về kết quả: giống nhau — this trong cả 2 đều là obj.

Về cơ chế: obj.method() dùng implicit binding (rule 3) — JS tự set this = object trước dấu chấm. fn.call(obj) dùng explicit binding (rule 2) — bạn truyền tay obj vào.

Khác biệt thực tế: nếu chỉ gọi fn() (không có .call), bạn sẽ mất this → default binding (undefined trong strict mode). Đây là lý do const fn = obj.method; fn() hay sinh bug.

Tổng kết

Sau chương 3, bạn nên đã master:

  • 4 cách định nghĩa function: declaration, expression, arrow, method shorthand — và bảng so sánh hoisting/this/arguments/new/prototype.
  • First-class function: gán biến, truyền arg, return, lưu trong cấu trúc.
  • Parameters modern: default (chỉ áp dụng cho undefined), rest ...args (mảng thực), destructuring trực tiếp ở signature.
  • Arrow vs regular: 4 khác biệt — không this, không arguments, không new, không prototype.
  • 4 rule của this: new > explicit > implicit > default; arrow là ngoại lệ (lexical).
  • Scope: global / function / block; var ignore block scope.
  • Hoisting: declaration hoist full; expression chỉ hoist tên; let/const ở TDZ.
  • Closure = function + scope nó được tạo. Counter, data privacy, memoize, once, curry.
  • IIFE — dù module hiện đại đã giảm vai trò, vẫn xuất hiện trong code base lớn.

Kết nối

  • Chương 2 (Variables, Types) — củng cố hiểu biết về var/let/const và TDZ trước khi đào sâu hoisting ở đây.
  • Chương 4 (Objects, Prototypes & Classes)this binding áp dụng vào method của class; new binding kết nối với prototype chain.
  • Chương 6 (Async) — callback và Promise chain dựa hoàn toàn vào closure và arrow function để giữ this.
  • Chương 9 (TypeScript) — type signature của function ((a: number) => number) và overload phụ thuộc cú pháp ở đây.
  • Dart Chương 3 — Dart cũng có closure và arrow, nhưng this đơn giản hơn (không có 4 rule). So sánh để thấy JS phức tạp ở đâu và vì sao.