Arrow Function vs Regular Function trong JavaScript: Phân biệt và Ứng dụng

Trong JavaScript, việc chọn sai giữa arrow functionregular function có thể khiến bạn gặp lỗi mất this, dẫn đến bug khó debug và ảnh hưởng đến logic ứng dụng. Hiểu rõ sự khác biệt sẽ giúp bạn viết code rõ ràng hơn, dễ bảo trì và tránh được nhiều “cạm bẫy” không đáng có.

#1. Khái niệm cơ bản

#1.1. Regular Function

1
2
3
4
5
6
7
8
9
greet('Paolo'); // Hello, Paolo
function greet(name) {
console.log('Hello, ' + name);
}

sayHi('Hannah'); // Arguments: Arguments ['Hannah', callee: ƒ, Symbol(Symbol.iterator): ƒ]
function sayHi(name) {
console.log('Arguments: ', arguments );
}

Regular function có những đặc điểm sau:

  • Có thể dùng để tạo constructor (bằng new).
  • Truy cập được biến arguments – chứa toàn bộ tham số truyền vào.
  • Được hoisting – có thể gọi trước khi định nghĩa.

#1.2. Arrow Function

1
2
const greet = (name) => console.log(`Hello, ${name}`);
greet('Paolo'); // Hello, Paolo

Arrow function sinh ra để viết function ngắn gọn và giữ nguyên this của ngữ cảnh bên ngoài (lexical this). Tuy nhiên, điều này dẫn đến vài khác biệt đáng lưu ý:

  1. Không có arguments riêng

Arrow function không tạo ra biến arguments. Nếu cần lấy danh sách tham số, bạn phải dùng cú pháp rest:

1
2
3
const fn = (...args) => {
console.log(args);
};
  1. Không có new.target

    new.target là một meta property chỉ xuất hiện khi hàm được gọi bằng từ khóa new, dùng để kiểm tra xem hàm có đang được khởi tạo như một constructor hay không.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function A() {
    if (!new.target) {
    throw new Error('Phải dùng new A() để khởi tạo');
    }
    console.log('Đang khởi tạo bằng new');
    }

    new A(); // OK
    A(); // Error: Phải dùng new A() để khởi tạo

    Tuy nhiên, arrow function không có new.target vì bản thân nó không được thiết kế để làm constructor, và không có ngữ cảnh khởi tạo (construct context) riêng.


    Nếu bạn cố gọi arrow function với new:

    1
    2
    3
    4
    5
    const Arrow = () => {
    console.log(new.target); // SyntaxError: new.target expression is not allowed here
    };

    new Arrow();

    Bạn sẽ nhận lỗi ngay: "new.target expression is not allowed here" – vì arrow function không có execution context riêng, nên không được phép truy cập new.target.


  1. Không thể làm constructor

    • Vì arrow function không có [[Construct]] và không có prototype, nên không thể dùng để tạo object:

      1
      2
      const F = () => {};
      new F(); // TypeError: F is not a constructor

#1.3. Cú pháp ngắn gọn và implicit return

1
const square = n => n * n;

Ghi nhớ: Nếu function chỉ có một biểu thức, bạn có thể bỏ {}return. Kết quả của biểu thức sẽ được trả về ngầm định.

#2. Hoisting và Temporal Dead Zone

1
2
3
4
5
6
7
console.log(fn);  // ƒ fn(){}
console.log(foo); // undefined
console.log(bar); // ReferenceError

function fn() {} // Hoisting hoàn toàn
var foo = function () {}; // Hoisting biến, không gán giá trị
const bar = () => {}; // Không hoisting, bị TDZ

Giải thích:

  • fn là function declaration → được hoisting đầy đủ cả tên và thân hàm.
  • foo khai báo bằng var → được hoisting biến, nhưng gán sau nên in ra undefined.
  • bar dùng const → nằm trong Temporal Dead Zone, truy cập trước khi định nghĩa sẽ lỗi.

#3. this hoạt động khác nhau như thế nào?

#3.1. Trong global context

1
2
3
4
5
function regularFunc() { console.log(this); }
const arrowFunc = () => { console.log(this); };

regularFunc(); // window (hoặc undefined nếu dùng strict mode)
arrowFunc(); // window (kế thừa outer scope)

Tóm lại:

  • regularFunc()this linh hoạt, phụ thuộc cách gọi.
  • arrowFunc() luôn giữ this của ngữ cảnh bên ngoài (ở đây là global).

#3.2. Callback & Promise

#3.2.1. Callback với setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
const user = {
name: 'Hannah',
show() {
setTimeout(function () {
console.log(this.name || 'No name 1');
}, 0);

setTimeout(() => {
console.log(this.name || 'No name 2');
}, 0);
}
};
user.show();
  • function() {}thiswindow → in No name 1.
  • () => {} giữ thisuser → in Hannah.

#3.2.2. Callback trong Promise

1
2
3
4
5
6
7
8
9
10
11
12
class API {
constructor() {
this.data = 42;
}

fetch() {
Promise.resolve().then(function () {
console.log(this?.data); // undefined
});
}
}
new API().fetch();

Callback bằng regular function không giữ được this, nên in ra undefined.

1
2
3
4
5
6
7
8
9
10
11
12
class APIFixed {
constructor() {
this.data = 42;
}

fetch() {
Promise.resolve().then(() => {
console.log(this?.data); // 42
});
}
}
new APIFixed().fetch();

Dùng arrow function → this là instance của APIFixed.

#3.3. Phương thức (Method) của Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const counter = {
value: 0,
incrementRegular() {
this.value++;
console.log(this.value);
},
incrementArrow: () => {
this.value++;
console.log(this.value);
}
};

counter.incrementRegular(); // 1
counter.incrementArrow(); // NaN

Lưu ý:

  • incrementRegular được gọi qua counter.incrementRegular(), nên this trỏ đến counter, cập nhật value.
  • incrementArrowthis là outer scope (không phải object counter), nên this.valueundefined, undefined++ sẽ cho kết quả NaN.

#3.4. Phương thức trong Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
name = 'Paolo';

regular() {
console.log('regular:', this.name);
}

arrow = () => {
console.log('arrow:', this.name);
}
}

const a = new A();
a.regular(); // regular: Paolo
a.arrow(); // arrow: Paolo

Tuy nhiên, sự khác biệt chỉ thể hiện rõ khi bạn truyền các method này làm callback:

1
2
setTimeout(a.regular, 100); // undefined (mất `this`)
setTimeout(a.arrow, 100); // Paolo (giữ nguyên `this`)

Giải thích:

  • regular() là method nằm trên A.prototype. Khi truyền vào callback, this không còn trỏ tới instance nữa trừ khi dùng .bind(this).
  • arrow() là instance property, được tạo mới mỗi khi khởi tạo object, và giữ nguyên this từ nơi định nghĩa (tức là instance a).

#3.5. DOM Event Handler

1
2
3
4
5
6
7
button.addEventListener('click', function () {
console.log(this); // element
});

button.addEventListener('click', () => {
console.log(this); // outer scope (window)
});
  • Dùng regular function → this là element đang click.
  • Dùng arrow function → this không phải element, thường là window.

#4. Tự luyện và kiểm tra

  1. Đoán kết quả:
1
2
3
4
5
6
7
const obj = {
x: 10,
foo: () => console.log(this.x),
bar() { console.log(this.x); }
};
obj.foo(); // ???
obj.bar(); // ???
  1. Sửa foo để in ra 10:
1
2
3
4
5
const obj = {
x: 10,
foo() { console.log(this.x); }
};
obj.foo(); // 10
  1. Vì sao arrow function không có arguments, new.target và không dùng để làm constructor?
    Trả lời: Vì arrow function không tạo ra execution context riêng. Nó dùng lại context của scope chứa nó nên không khởi tạo this, arguments, hay new.target.

#5. Kết luận

Tiêu chíRegular FunctionArrow Function
this riêng✅ Có, thay đổi theo cách gọi❌ Không, kế thừa từ outer scope
arguments✅ Có❌ Không
super✅ Có❌ Không hỗ trợ super
prototype (class)✅ Có, nằm trên prototype❌ Không, là property của instance
Dùng làm constructor (new)✅ Có thể❌ Không
Dùng làm method✅ Chuẩn ES6❌ Không phải method thực sự
Gọi làm callback (setTimeout, map)❌ Dễ mất this nếu không bind✅ Tự giữ this
Hiệu suất (class nhiều instance)✅ Tốt (dùng chung prototype)❌ Tốn bộ nhớ (tạo mỗi instance)

#Khi nào nên dùng?

Trường hợpNên dùng
Cần super, arguments, hoặc kế thừa logic✅ Regular Function
Dùng làm method thật sự, tối ưu hiệu suất✅ Regular Function
Callback hoặc truyền function ra ngoài class✅ Arrow Function
Tránh .bind(this) thủ công khi làm callback✅ Arrow Function