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ể gây mất binding this, dẫn đến lỗi khó debug và ảnh hưởng tính năng. Hiểu rõ điểm khác biệt giúp bạn viết code sạch hơn, dễ bảo trì và tránh các “cạm bẫy” trong ứng dụng.

#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 cho phép bạn sử dụng để tạo constructor, sử dụng arguments để truy cập tất cả tham số, có thể thực thi trước khi định nghĩa nhờ hoisting.

#1.2. Arrow Function

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

Arrow function được thiết kế chủ yếu để làm function “nhỏ gọn” và giữ nguyên ngữ cảnh (lexical) của this. Chính cách thiết kế này dẫn đến việc:

  1. Không có arguments

    • Arrow function không khởi tạo arguments. Thay vào đó, nếu cần tham số động, bạn phải dùng rest parameter:
      1
      2
      3
      const fn = (...args) => {
      console.log(args);
      };
  2. Không có super

    • Trong class, khi gọi method thông thường, bạn có thể dùng super để gọi phương thức cha. Ví dụ:
      1
      2
      3
      4
      5
      6
      7
      8
      class A { method() { console.log('A'); } }
      class B extends A {
      method() {
      super.method(); // gọi A.method()
      }
      }

      new B().method()
    • Arrow function không có bản đồ prototype và không khởi tạo this hay new.target, nên cũng không có binding cho super.
  3. Không có new.target

    • new.target chỉ tồn tại trong một hàm được gọi bằng new, để biết hàm đó có bị khởi tạo làm constructor hay không.
    • Arrow function không thể dùng với new, nên không tạo binding new.target.
  4. Không thể làm constructor

    • Một hàm muốn được gọi bằng new thì phải có nội bộ method [[Construct]] và tạo ra đối tượng mới với prototype tương ứng.
    • Arrow function không có nội bộ [[Construct]], không có prototype property, nên khi bạn cố gắng new (() => {}) sẽ bị lỗi:
      1
      2
      const F = () => {};
      new F(); // TypeError: F is not a constructor

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

1
const square = n => n * n; // implicit return

Lưu ý: Khi arrow function chỉ chứa một biểu thức, bạn có thể bỏ dấu ngoặc nhọn và return; giá trị của biểu thức chính là giá trị trả về.

#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(){} // regular function declaration
var foo = function() {}; // function expression
const bar = () => {}; // arrow function

Lưu ý:

  • var foo được hoisted (đưa lên đầu scope) nhưng chỉ khởi tạo giá trị undefined trước khi gán hàm, nên console.log(foo) in ra undefined.
  • const bar nằm trong Temporal Dead Zone (TDZ) từ khi vào scope đến khi khởi tạo, nên truy cập bar trước khi định nghĩa gây ReferenceError.
  • Nhờ đó, bạn hiểu rằng regular function declaration được hoisted hoàn toàn, nhưng function expression hoặc arrow function với let/const thì không.

#3. Cách hoạt động của this

#3.1. Global Context

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

regularFunc(); // window (non-strict) hoặc undefined (strict)
arrowFunc(); // window (kế thừa global scope)

Lưu ý:

  • regularFunc(): this bên trong regular function ở non-strict mode là window, ở strict mode là undefined.
  • arrowFunc(): this bên trong arrow function luôn là this của outer scope, tức là window.

#3.2. Callback & Promise

#3.2.1. Callback

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'); // No name 1
}, 0);

setTimeout(() => {
console.log(this.name || 'No name 2'); // Hannah
}, 0);
}
};
user.show();

Lưu ý:

  • Trong setTimeout(function(){...}), this lúc này là window do ở non-strict mode, do đó this.name không phải user.name.
  • Trong setTimeout(()=>{...}), this lúc này là user, do đó this.name tương ứng với user.name.

#3.2.1. Promise

Callback của .then(function(){...})this riêng, không phải instance API.

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 arrow function sẽ giữ this là instance APIFixedthis bên trong arrow function luôn là this của outer scope, tức là APIFixed.

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();

#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, nên this trỏ đến window, do đó this.valueundefined, undefined++ cho NaN.

#3.4. DOM Event Handler

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

button.addEventListener('click', () => {
console.log(this); // không phải element, thường là global hoặc scope chứa nó
});

Lưu ý:

  • Regular function trong sự kiện clickthis là element gắn sự kiện.
  • Arrow function có this là outer scope, nên this trỏ đến window, không dùng được để thao tác trực tiếp với element.

#4. Bài tập tự kiểm tra

  1. Dự đ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();
  2. Viết lại foo để in ra 10:
    1
    2
    3
    4
    5
    const obj = {
    x: 10,
    foo: () => console.log(this.x)// undefined
    };
    obj.foo();
  3. Giải thích vì sao arrow function không có arguments, super, new.target và không thể làm constructor.

#5. Kết luận

  • Regular Function

    • Khi cần this linh hoạt, làm method trong object/class.
    • Khi cần sử dụng constructor, hoisting, arguments.
  • Arrow Function

    • Khi cần callback ngắn gọn, functional programming (map/filter/reduce).

    • Khi cần giữ nguyên this của outer scope, tránh phải dùng .bind(this):

      • Khi bạn truyền một method dùng this làm callback, ví dụ cho setTimeout hoặc event handler, regular function sẽ mất binding this, buộc bạn phải:
        1
        setTimeout(this.method.bind(this), 1000);
        Hoặc lưu biến trung gian:
        1
        2
        const self = this;
        setTimeout(function() { self.method(); }, 1000);
        Dùng arrow function thì không cần thêm .bind(this) vì arrow tự động mượn this từ nơi nó được định nghĩa:
        1
        setTimeout(() => this.method(), 1000);