Arrow function expressions
Baseline
Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2016.
Biểu thức hàm mũi tên (arrow function) là một cách viết hàm gọn hơn so với biểu thức hàm truyền thống, với một số điểm khác biệt về ngữ nghĩa và những hạn chế nhất định trong cách sử dụng:
- Hàm mũi tên không có ràng buộc riêng với
this,arguments, haysuper, và không nên được dùng làm phương thức. - Hàm mũi tên không thể dùng làm hàm tạo (constructor). Gọi chúng với
newsẽ ném ra lỗiTypeError. Chúng cũng không có quyền truy cập vào từ khóanew.target. - Hàm mũi tên không thể dùng
yieldbên trong thân hàm và không thể được tạo ra dưới dạng hàm generator.
Try it
const materials = ["Hydrogen", "Helium", "Lithium", "Beryllium"];
console.log(materials.map((material) => material.length));
// Expected output: Array [8, 6, 7, 9]
Cú pháp
() => expression
param => expression
(param) => expression
(param1, paramN) => expression
() => {
statements
}
param => {
statements
}
(param1, paramN) => {
statements
}
Tham số rest, tham số mặc định, và destructuring trong danh sách tham số đều được hỗ trợ, nhưng luôn cần có dấu ngoặc đơn:
(a, b, ...r) => expression
(a = 400, b = 20, c) => expression
([a, b] = [10, 20]) => expression
({ a, b } = { a: 10, b: 20 }) => expression
Hàm mũi tên có thể là async bằng cách thêm từ khóa async vào trước biểu thức.
async param => expression
async (param1, param2, ...paramN) => {
statements
}
Mô tả
Hãy cùng phân tích một hàm ẩn danh truyền thống xuống dạng hàm mũi tên đơn giản nhất từng bước một. Mỗi bước đều là một hàm mũi tên hợp lệ.
Note: Biểu thức hàm truyền thống và hàm mũi tên có nhiều điểm khác biệt hơn chỉ là cú pháp. Chúng ta sẽ giới thiệu chi tiết hơn về sự khác biệt trong hành vi ở các mục tiếp theo.
// Hàm ẩn danh truyền thống
(function (a) {
return a + 100;
});
// 1. Xóa từ khóa "function" và đặt mũi tên giữa tham số và dấu ngoặc nhọn mở thân hàm
(a) => {
return a + 100;
};
// 2. Xóa dấu ngoặc nhọn thân hàm và từ khóa "return" — giá trị trả về là ẩn
(a) => a + 100;
// 3. Xóa dấu ngoặc đơn quanh tham số
a => a + 100;
Trong ví dụ trên, cả dấu ngoặc đơn quanh tham số lẫn dấu ngoặc nhọn quanh thân hàm đều có thể bỏ qua. Tuy nhiên, chúng chỉ có thể bỏ qua trong một số trường hợp nhất định.
Dấu ngoặc đơn chỉ có thể bỏ qua khi hàm có đúng một tham số đơn giản. Nếu có nhiều tham số, không có tham số, hoặc dùng tham số mặc định, destructuring hay rest, thì dấu ngoặc đơn quanh danh sách tham số là bắt buộc.
// Hàm ẩn danh truyền thống
(function (a, b) {
return a + b + 100;
});
// Hàm mũi tên
(a, b) => a + b + 100;
const a = 4;
const b = 2;
// Hàm ẩn danh truyền thống (không có tham số)
(function () {
return a + b + 100;
});
// Hàm mũi tên (không có tham số)
() => a + b + 100;
Dấu ngoặc nhọn chỉ có thể bỏ qua khi hàm trực tiếp trả về một biểu thức. Nếu thân hàm có nhiều câu lệnh, dấu ngoặc nhọn là bắt buộc. Trong trường hợp đó, giá trị trả về phải được khai báo tường minh bằng từ khóa return. Hàm mũi tên không thể tự đoán bạn muốn trả về gì hay khi nào.
// Hàm ẩn danh truyền thống
(function (a, b) {
const chuck = 42;
return a + b + chuck;
});
// Hàm mũi tên
(a, b) => {
const chuck = 42;
return a + b + chuck;
};
Hàm mũi tên vốn không gắn liền với một tên cụ thể. Nếu hàm mũi tên cần tự gọi chính nó, hãy dùng biểu thức hàm có tên thay thế. Bạn cũng có thể gán hàm mũi tên cho một biến để tham chiếu đến nó qua biến đó.
// Hàm thông thường
function bob(a) {
return a + 100;
}
// Hàm mũi tên
const bob2 = (a) => a + 100;
Thân hàm
Hàm mũi tên có thể có thân biểu thức hoặc thân khối thông thường.
Trong thân biểu thức, chỉ có một biểu thức duy nhất, và đây sẽ là giá trị trả về ẩn. Thân khối tương tự như thân hàm truyền thống, nơi giá trị trả về phải được khai báo tường minh bằng từ khóa return. Hàm mũi tên không bắt buộc phải trả về giá trị. Nếu luồng thực thi trong thân khối kết thúc mà không gặp câu lệnh return, hàm sẽ trả về undefined như các hàm thông thường khác.
// Thân biểu thức
const add = (a, b) => a + b; // Trả về ẩn a + b
// Thân khối
const add2 = (a, b) => {
console.log(a, b);
return a + b; // Phải trả về tường minh
};
// Không có giá trị trả về
const add3 = (b) => {
a += b;
// Không có câu lệnh return, nên trả về undefined
};
Trả về object literal dùng cú pháp thân biểu thức (params) => { object: literal } không hoạt động như mong đợi.
const func = () => { foo: 1 };
// Gọi func() trả về undefined!
const func2 = () => { foo: function () {} };
// SyntaxError: function statement requires a name
const func3 = () => { foo() {} };
// SyntaxError: Unexpected token '{'
Đây là vì JavaScript chỉ coi hàm mũi tên có thân biểu thức khi token theo sau mũi tên không phải dấu ngoặc nhọn mở, nên đoạn code trong dấu ngoặc nhọn ({}) sẽ được phân tích cú pháp như một chuỗi câu lệnh, trong đó foo là một nhãn (label) chứ không phải khóa trong object literal.
Để khắc phục điều này, hãy bao object literal trong dấu ngoặc đơn:
const func = () => ({ foo: 1 });
Không thể dùng làm phương thức
Biểu thức hàm mũi tên chỉ nên được dùng cho các hàm không phải phương thức vì chúng không có this riêng. Hãy xem điều gì xảy ra khi ta thử dùng chúng làm phương thức:
"use strict";
const obj = {
i: 10,
b: () => console.log(this.i, this),
c() {
console.log(this.i, this);
},
};
obj.b(); // logs undefined, Window { /* … */ } (hoặc đối tượng toàn cục)
obj.c(); // logs 10, Object { /* … */ }
Một ví dụ khác liên quan đến Object.defineProperty():
"use strict";
const obj = {
a: 10,
};
Object.defineProperty(obj, "b", {
get: () => {
console.log(this.a, typeof this.a, this); // undefined 'undefined' Window { /* … */ } (hoặc đối tượng toàn cục)
return this.a + 10; // đại diện cho đối tượng toàn cục 'Window', do đó 'this.a' trả về 'undefined'
},
});
Vì thân class có ngữ cảnh this, hàm mũi tên dùng làm class field sẽ đóng gói (close over) ngữ cảnh this của class, và this bên trong thân hàm mũi tên sẽ trỏ đúng đến instance (hoặc bản thân class, với static field). Tuy nhiên, vì đây là một closure, chứ không phải ràng buộc riêng của hàm, giá trị của this sẽ không thay đổi theo ngữ cảnh thực thi.
class C {
a = 1;
autoBoundMethod = () => {
console.log(this.a);
};
}
const c = new C();
c.autoBoundMethod(); // 1
const { autoBoundMethod } = c;
autoBoundMethod(); // 1
// Nếu là phương thức thông thường, kết quả sẽ là undefined trong trường hợp này
Thuộc tính hàm mũi tên thường được gọi là "phương thức tự ràng buộc" (auto-bound methods), vì cách tương đương với phương thức thông thường là:
class C {
a = 1;
constructor() {
this.method = this.method.bind(this);
}
method() {
console.log(this.a);
}
}
Note: Class field được định nghĩa trên instance, không phải trên prototype, nên mỗi lần tạo instance sẽ tạo ra một tham chiếu hàm mới và cấp phát một closure mới, có thể dẫn đến tốn nhiều bộ nhớ hơn so với phương thức thông thường không ràng buộc.
Vì lý do tương tự, các phương thức call(), apply(), và bind() không có tác dụng khi gọi trên hàm mũi tên, vì hàm mũi tên xác định this dựa trên phạm vi nơi nó được định nghĩa, và giá trị this không thay đổi theo cách gọi hàm.
Không ràng buộc arguments
Hàm mũi tên không có đối tượng arguments riêng. Vì vậy, trong ví dụ này, arguments là tham chiếu đến arguments của phạm vi bao ngoài:
function foo(n) {
const f = () => arguments[0] + n; // ràng buộc arguments ẩn của foo. arguments[0] là n
return f();
}
foo(3); // 3 + 3 = 6
Trong hầu hết các trường hợp, sử dụng tham số rest là một giải pháp thay thế tốt cho việc dùng đối tượng arguments.
function foo(n) {
const f = (...args) => args[0] + n;
return f(10);
}
foo(1); // 11
Không thể dùng làm constructor
Hàm mũi tên không thể dùng làm constructor và sẽ ném ra lỗi khi gọi với new. Chúng cũng không có thuộc tính prototype.
const Foo = () => {};
const foo = new Foo(); // TypeError: Foo is not a constructor
console.log("prototype" in Foo); // false
Không thể dùng làm generator
Từ khóa yield không thể dùng trong thân hàm mũi tên (trừ khi dùng trong các hàm generator lồng bên trong hàm mũi tên). Do đó, hàm mũi tên không thể dùng làm generator.
Ngắt dòng trước mũi tên
Hàm mũi tên không thể có ngắt dòng giữa các tham số và mũi tên.
const func = (a, b, c)
=> 1;
// SyntaxError: Unexpected token '=>'
Để định dạng mã nguồn, bạn có thể đặt ngắt dòng sau mũi tên hoặc dùng dấu ngoặc đơn/ngoặc nhọn quanh thân hàm, như minh họa dưới đây. Bạn cũng có thể đặt ngắt dòng giữa các tham số.
const func = (a, b, c) =>
1;
const func2 = (a, b, c) => (
1
);
const func3 = (a, b, c) => {
return 1;
};
const func4 = (
a,
b,
c,
) => 1;
Độ ưu tiên của mũi tên
Mặc dù mũi tên trong hàm mũi tên không phải là toán tử, hàm mũi tên có các quy tắc phân tích cú pháp đặc biệt tương tác khác với độ ưu tiên toán tử so với hàm thông thường.
let callback;
callback = callback || () => {};
// SyntaxError: invalid arrow-function arguments
Vì => có độ ưu tiên thấp hơn hầu hết các toán tử, cần có dấu ngoặc đơn để tránh callback || () bị phân tích cú pháp như danh sách đối số của hàm mũi tên.
callback = callback || (() => {});
Ví dụ
>Sử dụng hàm mũi tên
// Hàm mũi tên rỗng trả về undefined
const empty = () => {};
(() => "foobar")();
// Trả về "foobar"
// (đây là Biểu thức Hàm Được Gọi Ngay lập tức - IIFE)
const simple = (a) => (a > 15 ? 15 : a);
simple(16); // 15
simple(10); // 10
const max = (a, b) => (a > b ? a : b);
// Lọc, ánh xạ mảng dễ dàng, v.v.
const arr = [5, 6, 13, 0, 1, 18, 23];
const sum = arr.reduce((a, b) => a + b);
// 66
const even = arr.filter((v) => v % 2 === 0);
// [6, 0, 18]
const double = arr.map((v) => v * 2);
// [10, 12, 26, 0, 2, 36, 46]
// Chuỗi promise gọn gàng hơn
promise
.then((a) => {
// …
})
.then((b) => {
// …
});
// Hàm mũi tên không có tham số
setTimeout(() => {
console.log("I happen sooner");
setTimeout(() => {
// deeper code
console.log("I happen later");
}, 1);
}, 1);
Sử dụng call, bind, và apply
Các phương thức call(), apply(), và bind() hoạt động như mong đợi với hàm truyền thống, vì ta xác định phạm vi cho mỗi phương thức:
const obj = {
num: 100,
};
// Đặt "num" trên globalThis để thấy nó KHÔNG được dùng.
globalThis.num = 42;
// Hàm truyền thống để thao tác trên "this"
function add(a, b, c) {
return this.num + a + b + c;
}
console.log(add.call(obj, 1, 2, 3)); // 106
console.log(add.apply(obj, [1, 2, 3])); // 106
const boundAdd = add.bind(obj);
console.log(boundAdd(1, 2, 3)); // 106
Với hàm mũi tên, vì hàm add được tạo ra thực chất trong phạm vi globalThis (toàn cục), nó sẽ coi this là globalThis.
const obj = {
num: 100,
};
// Đặt "num" trên globalThis để thấy nó được lấy ra.
globalThis.num = 42;
// Hàm mũi tên
const add = (a, b, c) => this.num + a + b + c;
console.log(add.call(obj, 1, 2, 3)); // 48
console.log(add.apply(obj, [1, 2, 3])); // 48
const boundAdd = add.bind(obj);
console.log(boundAdd(1, 2, 3)); // 48
Lợi ích lớn nhất của việc dùng hàm mũi tên có lẽ là với các phương thức như setTimeout() và EventTarget.prototype.addEventListener() — những phương thức thường cần một loại closure, call(), apply(), hoặc bind() nào đó để đảm bảo hàm được thực thi trong phạm vi đúng.
Với biểu thức hàm truyền thống, đoạn code như này không hoạt động như mong đợi:
const obj = {
count: 10,
doSomethingLater() {
setTimeout(function () {
// hàm thực thi trong phạm vi window
this.count++;
console.log(this.count);
}, 300);
},
};
obj.doSomethingLater(); // logs "NaN", vì thuộc tính "count" không có trong phạm vi window.
Với hàm mũi tên, phạm vi this được giữ nguyên dễ dàng hơn:
const obj = {
count: 10,
doSomethingLater() {
// Cú pháp phương thức ràng buộc "this" với ngữ cảnh "obj".
setTimeout(() => {
// Vì hàm mũi tên không có ràng buộc riêng và
// setTimeout (khi gọi như một hàm) cũng không tạo ràng buộc,
// ngữ cảnh "obj" của phương thức bên ngoài được sử dụng.
this.count++;
console.log(this.count);
}, 300);
},
};
obj.doSomethingLater(); // logs 11
Thông số kỹ thuật
| Specification |
|---|
| ECMAScript® 2027 Language Specification> # sec-arrow-function-definitions> |
Khả năng tương thích với trình duyệt
Xem thêm
- Hướng dẫn Functions
- Functions
function- Biểu thức
function - ES6 In Depth: Arrow functions trên hacks.mozilla.org (2015)