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 = user vì gọi qua object
  • greetFunc()this mấ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 this từ outer scope (ở đây là global scope)
  • Global scope không có name, nên this.nameundefined
  • Dù gọi user.greet(), this vẫn không phải user

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ó this riêng, nên nó lấy this từ Timer() constructor
  • Không cần dùng var self = this nữ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 constructor

Tại sao?

Arrow function không có:

  • this riêng (không thể tạo instance mới)
  • prototype property (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ốngDùng gì?Lý do
Object methodfunction() {}Cần this = object
Prototype methodfunction() {}Cần this = instance
Constructorfunction() {} hoặc classArrow không hỗ trợ new
Event handlerfunction() {}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 argumentsfunction() {} hoặc rest paramsArrow 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 undefined

Giải thích:

  • counter.increment được truyền vào setTimeout mất context
  • Khi callback chạy, this không phải counter instance nữa

Cách fix 1: Dùng .bind():

1
setTimeout(counter.increment.bind(counter), 1000); // OK

Cách fix 2: Arrow function wrapper:

1
setTimeout(() => counter.increment(), 1000); // OK

Cá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 - 1

Trong 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 this của object/instance → Dùng function() {}
  • ✅ Cần giữ this củ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 outer this)
  • Constructor → function() {} hoặc class
  • 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ặc class
  • 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:

  1. this cố định - lấy từ outer scope, không tạo mới
  2. Không có arguments - dùng rest parameters thay thế
  3. 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ả!