Arrow Function trong JavaScript - 3 Lưu Ý Quan Trọng Khi Chuyển Từ ES5
Bạn quen viết function() {} trong ES5, giờ thấy code mới toàn () => {} nhưng chưa rõ nó khác gì? Thử dùng thì gặp lỗi this undefined, arguments không hoạt động, hoặc new báo lỗi. Đây là 3 điểm khác biệt cốt lõi giữa arrow function và function thông thường mà dev ES5 cần nắm rõ.
Bài này tập trung vào 3 lưu ý quan trọng nhất khi làm việc với arrow function: cách this binding hoạt động, sự khác biệt về arguments, và tại sao không dùng được với new. Hiểu rõ 3 điểm này sẽ giúp bạn tự tin áp dụng arrow function đúng chỗ và tránh bug khó debug.
#1. Arrow Function Là Gì?
Arrow function là cú pháp viết hàm ngắn gọn hơn, được giới thiệu từ ES6 (ES2015):
ES5 - Function thông thường:
1
2
3
var add = function(a, b) {
return a + b;
};ES6 - Arrow function:
1
const add = (a, b) => a + b;Cú pháp cơ bản:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Không có tham số
() => console.log('Hello');
// Một tham số (không cần dấu ngoặc)
name => console.log(name);
// Nhiều tham số
(a, b) => a + b;
// Nhiều dòng code (cần return)
(x, y) => {
const sum = x + y;
return sum * 2;
};
// Return object (cần dấu ngoặc)
id => ({ id: id, active: true });Trong thực tế: Cú pháp ngắn gọn của arrow function rất phổ biến trong code React, Vue và các dự án hiện đại. Nhưng đừng chỉ dùng vì nó ngắn - có 3 điểm khác biệt quan trọng về cách hoạt động mà bạn cần nắm rõ để tránh bug.
#2. this Binding - Khác Biệt Lớn Nhất
#2.1. Function Thông Thường: this Thay Đổi Theo Cách Gọi
Trong ES5, bạn quen với việc this phụ thuộc vào cách gọi hàm:
1
2
3
4
5
6
7
8
9
10
11
var user = {
name: 'Paolo',
greet: function() {
console.log(this.name);
}
};
user.greet(); // Paolo (this = user)
var greetFunc = user.greet;
greetFunc(); // undefined (this = window hoặc undefined trong strict mode)Giải thích:
user.greet()→this=uservì gọi qua objectgreetFunc()→thismất context vì gọi trực tiếp
Đây là hành vi bạn đã quen thuộc từ ES5.
Trong thực tế: Bug “mất this“ này rất phổ biến khi truyền method vào callback (setTimeout, addEventListener, Promise.then). Đây chính là lý do pattern var self = this xuất hiện nhiều trong code ES5.
#2.2. Arrow Function: this Cố Định Từ Outer Scope
Arrow function không tạo this riêng - nó “kế thừa” this từ hàm cha (lexical scope):
1
2
3
4
5
6
7
8
var user = {
name: 'Paolo',
greet: () => {
console.log(this.name);
}
};
user.greet(); // undefined (this = global scope, không phải user!)Tại sao lại undefined?
- Arrow function lấy
thistừ outer scope (ở đây là global scope) - Global scope không có
name, nênthis.namelàundefined - Dù gọi
user.greet(),thisvẫn không phảiuser
Quy tắc quan trọng:
Arrow function “chụp” this tại nơi nó được viết, không phải nơi nó được gọi.
#2.3. Khi Nào Arrow Function Hữu Ích?
Vấn đề phổ biến trong ES5 - Callback mất this:
1
2
3
4
5
6
7
8
9
10
function Timer() {
this.seconds = 0;
setInterval(function() {
this.seconds++; // Error: this là window, không phải Timer instance
console.log(this.seconds);
}, 1000);
}
new Timer(); // NaN, NaN, NaN...Cách fix trong ES5:
1
2
3
4
5
6
7
8
9
10
11
function Timer() {
this.seconds = 0;
var self = this; // Lưu this vào biến
setInterval(function() {
self.seconds++;
console.log(self.seconds);
}, 1000);
}
new Timer(); // 1, 2, 3...Cách fix trong ES6 với arrow function:
1
2
3
4
5
6
7
8
9
10
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // this tự động trỏ đúng instance
console.log(this.seconds);
}, 1000);
}
new Timer(); // 1, 2, 3...Giải thích:
- Arrow function không có
thisriêng, nên nó lấythistừTimer()constructor - Không cần dùng
var self = thisnữa - Đây chính là lý do arrow function được tạo ra - để xử lý callback mà không mất
this
#2.4. Khi Nào Không Nên Dùng Arrow Function?
❌ Object method:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Sai - this không phải obj
var obj = {
value: 42,
getValue: () => this.value // undefined
};
// Đúng - dùng function thông thường
var obj = {
value: 42,
getValue: function() {
return this.value; // 42
}
};❌ Prototype method:
1
2
3
4
5
6
7
8
9
10
11
12
13
function User(name) {
this.name = name;
}
// Sai
User.prototype.greet = () => {
console.log(this.name); // undefined
};
// Đúng
User.prototype.greet = function() {
console.log(this.name); // Hoạt động đúng
};❌ Event handler (nếu cần access element):
1
2
3
4
5
6
7
8
9
// Sai - this không phải button
button.addEventListener('click', () => {
this.classList.toggle('active'); // Error
});
// Đúng - this = button element
button.addEventListener('click', function() {
this.classList.toggle('active'); // OK
});Trong thực tế: Nếu bạn đang chuyển code jQuery sang vanilla JS, chú ý pattern $(this) trong event handler - bạn cần dùng regular function, không phải arrow function.
#2.5. So Sánh this Binding
View Mermaid diagram code
flowchart TD
Start([Gọi function]) --> Type{Loại function?}
Type -->|"Arrow Function<br/>() => {}"| Arrow["this = outer scope<br/>(cố định từ lúc viết code)"]
Type -->|"Regular Function<br/>function() {}"| Regular{Cách gọi?}
Regular -->|obj.method| Obj["this = obj"]
Regular -->|"Gọi trực tiếp<br/>func()"| Global["this = window<br/>(hoặc undefined)"]
Regular -->|new Function| New["this = instance mới"]
style Arrow fill:#e1f5ff
style Obj fill:#fff4e1
style Global fill:#ffebee
style New fill:#f1f8e9#3. arguments Object - Arrow Function Không Có
#3.1. Function Thông Thường Có arguments
Trong ES5, bạn có thể dùng arguments để truy cập tất cả tham số:
1
2
3
4
5
6
7
8
9
10
function sum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum(5, 10)); // 15#3.2. Arrow Function Không Có arguments
1
2
3
4
5
const sum = () => {
console.log(arguments); // ReferenceError: arguments is not defined
};
sum(1, 2, 3); // Error!Tại sao?
Arrow function không tạo arguments object riêng. Nếu bạn dùng arguments trong arrow function, nó sẽ lấy arguments từ outer function (nếu có):
1
2
3
4
5
6
7
8
function outer() {
const inner = () => {
console.log(arguments); // arguments của outer, không phải inner
};
inner(1, 2, 3);
}
outer('a', 'b'); // ['a', 'b'] - arguments của outer#3.3. Giải Pháp: Rest Parameters
ES6 cung cấp rest parameters (...args) - cách tốt hơn arguments:
1
2
3
4
5
6
const sum = (...numbers) => {
return numbers.reduce((total, n) => total + n, 0);
};
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum(5, 10)); // 15Ưu điểm của rest parameters:
- ✅ Là array thật (không phải array-like như
arguments) - ✅ Có đầy đủ array methods (map, filter, reduce…)
- ✅ Hoạt động với cả arrow function và regular function
- ✅ Rõ ràng hơn - tên tham số mô tả ý nghĩa
So sánh:
1
2
3
4
5
6
7
8
9
10
// ES5 - arguments (array-like object)
function oldWay() {
var args = Array.prototype.slice.call(arguments); // Phải convert
return args.map(function(x) { return x * 2; });
}
// ES6 - rest parameters (array thật)
const newWay = (...args) => {
return args.map(x => x * 2); // Dùng trực tiếp
};Trong thực tế:
Ngay cả với regular function, nên dùng rest parameters thay vì arguments vì code rõ ràng và dễ làm việc hơn.
#4. Constructor - Arrow Function Không Dùng Được new
#4.1. Function Thông Thường Có Thể Làm Constructor
Trong ES5, bạn dùng function với new để tạo object:
1
2
3
4
5
6
function Car(brand) {
this.brand = brand;
}
var myCar = new Car('Toyota');
console.log(myCar.brand); // Toyota#4.2. Arrow Function Không Thể Làm Constructor
1
2
3
4
5
const Car = (brand) => {
this.brand = brand;
};
const myCar = new Car('Toyota'); // TypeError: Car is not a constructorTại sao?
Arrow function không có:
thisriêng (không thể tạo instance mới)prototypeproperty (không thể kế thừa)- Internal
[[Construct]]method (không hỗ trợnew)
Trong thực tế: Lỗi này thường gặp khi refactor code - bạn thấy hàm dài, muốn rút ngắn thành arrow function, nhưng quên mất nó đang được dùng với new ở đâu đó trong codebase.
Minh họa:
1
2
3
4
5
6
7
// Regular function
function Regular() {}
console.log(Regular.prototype); // { constructor: Regular }
// Arrow function
const Arrow = () => {};
console.log(Arrow.prototype); // undefined#4.3. Thay Thế: Dùng Class hoặc Function
Cách 1: Class (ES6 - được khuyến khích):
1
2
3
4
5
6
7
8
9
10
11
12
class Car {
constructor(brand) {
this.brand = brand;
}
start() {
console.log(`${this.brand} is starting...`);
}
}
const myCar = new Car('Toyota');
myCar.start(); // Toyota is starting...Cách 2: Regular function:
1
2
3
4
5
6
7
8
9
10
function Car(brand) {
this.brand = brand;
}
Car.prototype.start = function() {
console.log(this.brand + ' is starting...');
};
const myCar = new Car('Toyota');
myCar.start(); // Toyota is starting...Trong thực tế:
- Dùng class cho constructor (code rõ ràng, chuẩn ES6+)
- Dùng arrow function cho callbacks, helper functions
- Không dùng arrow function làm constructor
#5. Use Cases - Khi Nào Dùng Gì?
#5.1. Dùng Arrow Function
✅ Callback functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// setTimeout, setInterval
setTimeout(() => {
console.log('Delayed message');
}, 1000);
// Array methods
var numbers = [1, 2, 3, 4];
var doubled = numbers.map(n => n * 2);
var evens = numbers.filter(n => n % 2 === 0);
// Promise chains
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));✅ Short, simple functions:
1
2
3
const square = x => x * x;
const greet = name => `Hello, ${name}`;
const add = (a, b) => a + b;✅ Callbacks cần giữ outer this:
1
2
3
4
5
6
7
8
9
10
11
function DataService() {
this.cache = {};
this.fetchData = function(id) {
fetch(`/api/${id}`)
.then(response => response.json())
.then(data => {
this.cache[id] = data; // this vẫn là DataService instance
});
};
}Trong thực tế: Promise chains là trường hợp phổ biến nhất để dùng arrow function. Nếu dùng function() trong .then(), bạn sẽ phải .bind(this) hoặc dùng var self = this - rất rườm rà.
#5.2. Dùng Regular Function
✅ Object methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var calculator = {
value: 0,
add: function(n) {
this.value += n;
return this;
},
multiply: function(n) {
this.value *= n;
return this;
}
};
calculator.add(5).multiply(2); // value = 10✅ Prototype methods:
1
2
3
4
5
6
7
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log('Hello, ' + this.name);
};✅ Constructor functions:
1
2
3
4
5
6
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person('Paolo', 30);✅ Event handlers (khi cần access element):
1
2
3
4
button.addEventListener('click', function() {
this.classList.toggle('active'); // this = button element
this.textContent = 'Clicked!';
});✅ Functions cần arguments:
1
2
3
4
5
6
7
function logAll() {
for (var i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
logAll('a', 'b', 'c'); // a, b, c#5.3. Bảng So Sánh Nhanh
| Tình huống | Dùng gì? | Lý do |
|---|---|---|
| Object method | function() {} | Cần this = object |
| Prototype method | function() {} | Cần this = instance |
| Constructor | function() {} hoặc class | Arrow không hỗ trợ new |
| Event handler | function() {} | this = element |
| Callback (setTimeout, Promise) | () => {} | Giữ outer this |
| Array methods (map, filter) | () => {} | Ngắn gọn, không cần this |
| Short utility function | () => {} | Cú pháp gọn hơn |
Function cần arguments | function() {} hoặc rest params | Arrow không có arguments |
#6. Ví Dụ Thực Tế - Class với Arrow Function
Arrow function rất hữu ích trong class khi dùng làm callback:
Vấn đề với regular method:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(this.count);
}
}
var counter = new Counter();
setTimeout(counter.increment, 1000); // Error: Cannot read 'count' of undefinedGiải thích:
counter.incrementđược truyền vàosetTimeoutmất context- Khi callback chạy,
thiskhông phảicounterinstance nữa
Cách fix 1: Dùng .bind():
1
setTimeout(counter.increment.bind(counter), 1000); // OKCách fix 2: Arrow function wrapper:
1
setTimeout(() => counter.increment(), 1000); // OKCách fix 3: Class field với arrow function (ES2022+):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Counter {
constructor() {
this.count = 0;
}
// Arrow function tự động bind this
increment = () => {
this.count++;
console.log(this.count);
}
}
var counter = new Counter();
setTimeout(counter.increment, 1000); // OK - 1Trong thực tế:
Class field arrow function rất phổ biến trong React component để handle events. Cách này tiện hơn .bind() trong constructor và tránh phải tạo wrapper function trong JSX.
1
2
3
4
5
6
7
8
9
10
11
12
class LoginForm extends React.Component {
state = { email: '' };
// Không cần .bind(this) trong constructor
handleChange = (e) => {
this.setState({ email: e.target.value });
}
render() {
return <input onChange={this.handleChange} />;
}
}#7. Checklist Nhanh - Arrow Function vs Regular Function
Khi viết code, tự hỏi:
Câu hỏi 1: Function này cần this không?
- ✅ Cần
thiscủa object/instance → Dùngfunction() {} - ✅ Cần giữ
thiscủa outer scope → Dùng() => {} - ✅ Không cần
this→ Dùng() => {}(ngắn gọn hơn)
Câu hỏi 2: Function này dùng làm gì?
- Object method →
function() {} - Callback (setTimeout, Promise, event) →
() => {}(nếu cần outerthis) - Constructor →
function() {}hoặcclass - Helper/utility →
() => {}(nếu ngắn gọn)
Câu hỏi 3: Có dùng new không?
- Có → Bắt buộc
function() {}hoặcclass - Không → Tùy theo câu 1 và 2
Quy tắc ngón tay cái:
Khi làm quen với arrow function, nhớ 3 điểm này:
thiscố định - lấy từ outer scope, không tạo mới- Không có
arguments- dùng rest parameters thay thế - Không dùng với
new- không phải constructor
Vậy là bạn đã hiểu 3 điểm khác biệt quan trọng giữa arrow function và function thông thường. Khi chuyển từ ES5 sang ES6, hãy chú ý this binding - đây là nguồn gốc phổ biến nhất của bug. Arrow function không phải để thay thế hoàn toàn regular function, mà để bổ sung cho những trường hợp cần giữ this của outer scope. Chúc bạn code ES6+ hiệu quả!