Thứ sáu, 31/07/2020 | 00:00 GMT+7

Hiểu các hàm mũi tên trong JavaScript

Phiên bản năm 2015 của đặc tả ECMAScript (ES6) đã thêm các biểu thức hàm mũi tên vào ngôn ngữ JavaScript . Hàm mũi tên là một cách mới để viết các biểu thức hàm ẩn danh và tương tự như các hàm lambda trong một số ngôn ngữ lập trình khác, chẳng hạn như Python .

Các hàm mũi tên khác với các hàm truyền thống theo một số cách, bao gồm cách xác định phạm vi của chúng và cách thể hiện cú pháp của chúng. Do đó, các hàm mũi tên đặc biệt hữu ích khi truyền một hàm dưới dạng tham số cho một hàm bậc cao hơn, chẳng hạn như khi bạn lặp qua một mảng với các phương thức trình vòng lặp tích hợp sẵn . Viết tắt cú pháp của chúng cũng có thể cho phép bạn cải thiện khả năng đọc mã của bạn .

Trong bài viết này, bạn sẽ xem xét các khai báo và biểu thức hàm, tìm hiểu về sự khác biệt giữa biểu thức hàm truyền thống và biểu thức hàm mũi tên, tìm hiểu về phạm vi từ vựng vì nó liên quan đến các hàm mũi tên và khám phá một số viết tắt cú pháp được phép sử dụng với các hàm mũi tên.

Xác định chức năng

Trước khi đi sâu vào chi tiết cụ thể của các biểu thức hàm mũi tên, hướng dẫn này sẽ xem xét ngắn gọn các hàm JavaScript truyền thống để thể hiện rõ hơn các khía cạnh độc đáo của hàm mũi tên sau này.

Hướng dẫn Cách Định nghĩa Hàm trong JavaScript trước đó trong loạt bài này đã giới thiệu khái niệm về khai báo hàmbiểu thức hàm . Khai báo hàm là một hàm được đặt tên được viết bằng từ khóa function . Khai báo hàm tải vào ngữ cảnh thực thi trước khi chạy bất kỳ mã nào. Điều này được gọi là hoisting , nghĩa là bạn có thể sử dụng hàm trước khi khai báo nó.

Đây là một ví dụ về hàm sum trả về tổng của hai tham số:

function sum(a, b) {   return a + b } 

Bạn có thể thực thi hàm sum trước khi khai báo hàm do kéo:

sum(1, 2)  function sum(a, b) {   return a + b } 

Chạy mã này sẽ cho kết quả sau:

Output
3

Bạn có thể tìm thấy tên của hàm bằng cách ghi lại chính hàm đó:

console.log(sum) 

Kết quả sẽ trả về hàm, cùng với tên của nó:

Output
ƒ sum(a, b) { return a + b }

Một biểu thức hàm là một hàm không được tải trước vào ngữ cảnh thực thi và chỉ chạy khi mã gặp nó. Biểu thức hàm thường được gán cho một biến và có thể ẩn danh , nghĩa là hàm không có tên.

Trong ví dụ này, hãy viết hàm tính sum giống như một biểu thức hàm ẩn danh:

const sum = function (a, b) {   return a + b } 

Đến đây bạn đã gán hàm ẩn danh cho hằng sum . Cố gắng thực thi hàm trước khi nó được khai báo sẽ dẫn đến lỗi:

sum(1, 2)  const sum = function (a, b) {   return a + b } 

Chạy cái này sẽ cho:

Output
Uncaught ReferenceError: Cannot access 'sum' before initialization

Ngoài ra, lưu ý hàm không có định danh được đặt tên. Để minh họa điều này, hãy viết cùng một hàm ẩn danh được gán cho sum , sau đó ghi sum vào console :

const sum = function (a, b) {   return a + b }  console.log(sum) 

Điều này sẽ cho bạn thấy những điều sau:

Output
ƒ (a, b) { return a + b }

Giá trị của sum là một hàm ẩn danh, không phải là một hàm có tên.

Bạn có thể đặt tên cho các biểu thức hàm được viết bằng từ khóa function , nhưng điều này không phổ biến trong thực tế. Một lý do bạn có thể cần đặt tên cho một biểu thức hàm là làm cho các dấu vết ngăn xếp lỗi dễ gỡ lỗi hơn.

Hãy xem xét các chức năng sau đây, trong đó sử dụng một if tuyên bố để ném ra một lỗi nếu thông số chức năng đang thiếu:

const sum = function namedSumFunction(a, b) {   if (!a || !b) throw new Error('Parameters are required.')    return a + b }  sum(); 

Phần được tô sáng đặt tên cho hàm, sau đó hàm sử dụng dấu hoặc || toán tử để ném một đối tượng lỗi nếu một trong hai tham số bị thiếu.

Chạy mã này sẽ cung cấp cho bạn những điều sau:

Output
Uncaught Error: Parameters are required. at namedSumFunction (<anonymous>:3:23) at <anonymous>:1:1

Trong trường hợp này, đặt tên cho hàm sẽ giúp bạn biết nhanh lỗi ở đâu.

Biểu thức hàm mũi tên là một biểu thức hàm ẩn danh được viết bằng cú pháp “mũi tên béo” ( => ).

Viết lại hàm sum với cú pháp hàm mũi tên:

const sum = (a, b) => {   return a + b } 

Giống như các biểu thức hàm truyền thống, các hàm mũi tên không được kéo lên, và vì vậy bạn không thể gọi chúng trước khi khai báo. Chúng cũng luôn ẩn danh — không có cách nào để đặt tên cho một hàm mũi tên. Trong phần tiếp theo, bạn sẽ khám phá thêm sự khác biệt về cú pháp và thực tế giữa các hàm mũi tên và các hàm truyền thống.

Cú pháp và hành vi của hàm mũi tên

Các hàm mũi tên có một số điểm khác biệt quan trọng trong cách chúng hoạt động, phân biệt chúng với các hàm truyền thống, cũng như một số cải tiến về cú pháp. Sự khác biệt lớn nhất về chức năng là các hàm mũi tên không có ràng buộc hoặc nguyên mẫu riêng this và không thể được sử dụng như một phương thức khởi tạo. Các hàm mũi tên cũng có thể được viết như một sự thay thế nhỏ gọn hơn cho các hàm truyền thống, vì chúng cấp khả năng bỏ qua các dấu ngoặc đơn xung quanh các tham số và thêm khái niệm về một thân hàm súc tích với trả về ngầm định.

Trong phần này, bạn sẽ xem qua các ví dụ minh họa cho từng trường hợp này.

Lexical this

Từ khóa this thường được coi là một chủ đề phức tạp trong JavaScript. Bài viết Hiểu điều này, ràng buộc, gọi và áp dụng trong JavaScript giải thích cách hoạt động của this và cách this có thể được suy luận ngầm dựa trên việc chương trình sử dụng nó trong ngữ cảnh chung, như một phương thức trong một đối tượng, như một phương thức khởi tạo trên một hàm hoặc lớp, hoặc như một trình xử lý sự kiện DOM .

Các hàm mũi tên có từ vựng this , nghĩa là giá trị của this được xác định bởi phạm vi xung quanh (môi trường từ vựng).

Ví dụ tiếp theo sẽ chứng minh sự khác biệt giữa cách các hàm truyền thống và arrow xử lý this . Trong đối tượng printNumbers sau đây, có hai thuộc tính: phrasenumbers . Ngoài ra còn có một phương thức trên đối tượng, loop , sẽ in ra chuỗi phrase và giá trị hiện tại dưới dạng numbers :

const printNumbers = {   phrase: 'The current value is:',   numbers: [1, 2, 3, 4],    loop() {     this.numbers.forEach(function (number) {       console.log(this.phrase, number)     })   }, } 

Người ta có thể mong đợi hàm loop in ra chuỗi và số hiện tại trong vòng lặp trên mỗi lần lặp. Tuy nhiên, trong kết quả của việc chạy hàm, phrase thực sự undefined :

printNumbers.loop() 

Điều này sẽ cung cấp những điều sau:

Output
undefined 1 undefined 2 undefined 3 undefined 4

Như điều này cho thấy, this.phrase không được xác định, cho biết this trong hàm ẩn danh được truyền vào phương thức forEach không tham chiếu đến đối tượng printNumbers . Điều này là do một hàm truyền thống sẽ không xác định giá trị this của nó từ phạm vi của môi trường, đó là đối tượng printNumbers .

Trong các version JavaScript cũ hơn, bạn sẽ phải sử dụng phương thức bind , phương thức này sẽ đặt this một cách rõ ràng. Mẫu này có thể được tìm thấy thường xuyên trong một số version trước của khung công tác, như React , trước khi ES6 ra đời.

Sử dụng bind để sửa chức năng:

const printNumbers = {   phrase: 'The current value is:',   numbers: [1, 2, 3, 4],    loop() {     // Bind the `this` from printNumbers to the inner forEach function     this.numbers.forEach(       function (number) {         console.log(this.phrase, number)       }.bind(this),     )   }, }  printNumbers.loop() 

Điều này sẽ cho kết quả mong đợi:

Output
The current value is: 1 The current value is: 2 The current value is: 3 The current value is: 4

Các hàm mũi tên cung cấp một cách trực tiếp hơn để giải quyết vấn đề này. Vì giá trị this của chúng được xác định dựa trên phạm vi từ vựng, nên hàm bên trong được gọi trong forEach giờ đây có thể truy cập các thuộc tính của đối tượng printNumbers bên ngoài, như được minh họa:

const printNumbers = {   phrase: 'The current value is:',   numbers: [1, 2, 3, 4],    loop() {     this.numbers.forEach((number) => {       console.log(this.phrase, number)     })   }, }  printNumbers.loop() 

Điều này sẽ cho kết quả mong đợi:

Output
The current value is: 1 The current value is: 2 The current value is: 3 The current value is: 4

Những ví dụ này cho thấy rằng việc sử dụng các hàm mũi tên trong các phương thức mảng tích hợp sẵn như forEach , map , filterreduce có thể trực quan hơn và dễ đọc hơn, giúp chiến lược này có nhiều khả năng đáp ứng kỳ vọng hơn.

Hàm mũi tên làm phương thức đối tượng

Mặc dù các hàm mũi tên là tuyệt vời như các hàm tham số được truyền vào các phương thức mảng, chúng không hiệu quả như các phương thức đối tượng vì cách chúng sử dụng phạm vi từ vựng cho việc this . Sử dụng cùng một ví dụ như trước, hãy sử dụng phương thức loop và biến nó thành một hàm mũi tên để khám phá cách nó sẽ thực thi:

const printNumbers = {   phrase: 'The current value is:',   numbers: [1, 2, 3, 4],    loop: () => {     this.numbers.forEach((number) => {       console.log(this.phrase, number)     })   }, } 

Trong trường hợp này là một phương thức đối tượng, this sẽ tham chiếu đến các thuộc tính và phương thức của đối tượng printNumbers . Tuy nhiên, vì một đối tượng không tạo phạm vi từ vựng mới, một hàm mũi tên sẽ nhìn ra ngoài đối tượng cho giá trị của đối tượng this .

Gọi phương thức loop() :

printNumbers.loop() 

Điều này sẽ cung cấp những điều sau:

Output
Uncaught TypeError: Cannot read property 'forEach' of undefined

Vì đối tượng không tạo phạm vi từ vựng, phương thức hàm mũi tên tìm kiếm this trong phạm vi bên ngoài– Window trong ví dụ này. Vì thuộc tính numbers không tồn tại trên đối tượng Window nên nó sẽ tạo ra một lỗi. Theo nguyên tắc chung, sẽ an toàn hơn khi sử dụng các hàm truyền thống làm phương thức đối tượng theo mặc định.

Hàm mũi tên Không có hàm constructor hoặc prototype

Hướng dẫn Hiểu về Nguyên mẫu và Kế thừa trong JavaScript trước đó trong loạt bài này đã giải thích rằng các hàm và lớp có thuộc tính prototype , đó là những gì JavaScript sử dụng làm bản thiết kế để sao chép và kế thừa.

Để minh họa điều này, hãy tạo một hàm và ghi lại thuộc tính prototype được gán tự động:

function myFunction() {   this.value = 5 }  // Log the prototype property of myFunction console.log(myFunction.prototype) 

Thao tác này sẽ in nội dung sau vào console :

Output
{constructor: ƒ}

Điều này cho thấy rằng trong thuộc tính prototype có một đối tượng với một phương thức constructor . Điều này cho phép bạn sử dụng từ khóa new để tạo một version của hàm:

const instance = new myFunction()  console.log(instance.value) 

Điều này sẽ mang lại giá trị của thuộc tính value mà bạn đã xác định khi lần đầu tiên khai báo hàm:

Output
5

Ngược lại, các hàm mũi tên không có thuộc tính prototype . Tạo một hàm mũi tên mới và cố gắng ghi lại nguyên mẫu của nó:

const myArrowFunction = () => {}  // Attempt to log the prototype property of myArrowFunction console.log(myArrowFunction.prototype) 

Điều này sẽ cung cấp những điều sau:

Output
undefined

Do thuộc tính prototype bị thiếu, từ khóa new không có sẵn và bạn không thể tạo một version từ hàm mũi tên:

const arrowInstance = new myArrowFunction()  console.log(arrowInstance) 

Điều này sẽ gây ra lỗi sau:

Output
Uncaught TypeError: myArrowFunction is not a constructor

Điều này phù hợp với ví dụ trước đó của ta : Vì các hàm mũi tên không có giá trị this của riêng chúng, nên bạn sẽ không thể sử dụng hàm mũi tên làm hàm tạo.

Như được hiển thị ở đây, các chức năng mũi tên có rất nhiều thay đổi tinh tế khiến chúng hoạt động khác với các chức năng truyền thống trong ES5 trở về trước. Ngoài ra còn có một số thay đổi cú pháp tùy chọn giúp việc viết các hàm mũi tên nhanh hơn và ít dài dòng hơn. Phần tiếp theo sẽ hiển thị các ví dụ về những thay đổi cú pháp này.

Lợi nhuận ngầm định

Phần thân của một hàm truyền thống được chứa trong một khối sử dụng dấu ngoặc nhọn {} và kết thúc khi mã gặp một từ khóa return . Sau Đây là kết quả triển khai này trông giống như một hàm mũi tên:

const sum = (a, b) => {   return a + b } 

Hàm mũi tên giới thiệu cú pháp nội dung ngắn gọn hoặc trả về ngầm định . Điều này cho phép bỏ qua dấu ngoặc nhọn và từ khóa return .

const sum = (a, b) => a + b 

Trả về tiềm ẩn rất hữu ích để tạo các hoạt động một dòng ngắn gọn trong map , filter và các phương thức mảng phổ biến khác. Lưu ý cả dấu ngoặc và từ khóa return đều phải được bỏ qua. Nếu bạn không thể viết phần thân dưới dạng câu lệnh trả về một dòng, thì bạn sẽ phải sử dụng cú pháp phần thân khối thông thường.

Trong trường hợp trả về một đối tượng, cú pháp yêu cầu bạn phải bọc đối tượng theo nghĩa đen trong dấu ngoặc đơn. Nếu không, các dấu ngoặc sẽ được coi như một phần thân của hàm và sẽ không tính giá trị return .

Để minh họa điều này, hãy tìm ví dụ sau:

const sum = (a, b) => ({result: a + b})  sum(1, 2) 

Điều này sẽ cho kết quả sau:

Output
{result: 3}

Bỏ qua dấu ngoặc đơn xung quanh một tham số

Một cải tiến cú pháp hữu ích khác là khả năng loại bỏ dấu ngoặc đơn xung quanh một tham số duy nhất trong một hàm. Trong ví dụ sau, hàm square chỉ hoạt động trên một tham số, x :

const square = (x) => x * x 

Do đó, bạn có thể bỏ qua các dấu ngoặc quanh tham số và nó sẽ hoạt động giống nhau:

const square = x => x * x  square(10) 

Điều này sẽ cung cấp những điều sau:

Output
100

Lưu ý nếu một hàm không có tham số, dấu ngoặc đơn sẽ được yêu cầu:

const greet = () => 'Hello!'  greet() 

Lệnh gọi greet() sẽ hoạt động như sau:

Output
'Hello!'

Một số cơ sở mã chọn bỏ dấu ngoặc đơn nếu có thể, và những cơ sở khác chọn luôn giữ dấu ngoặc đơn xung quanh các tham số dù điều gì, đặc biệt là trong các cơ sở mã sử dụng TypeScript và yêu cầu thêm thông tin về từng biến và tham số. Khi quyết định cách viết các hàm mũi tên của bạn, hãy kiểm tra hướng dẫn kiểu của dự án mà bạn đang đóng góp.

Kết luận

Trong bài viết này, bạn đã xem xét các hàm truyền thống và sự khác biệt giữa khai báo hàm và biểu thức hàm. Bạn đã biết rằng các hàm mũi tên luôn ẩn danh, không có prototype hoặc hàm constructor , không thể sử dụng với từ khóa new và xác định giá trị của từ khóa this thông qua phạm vi từ vựng. Cuối cùng, bạn đã khám phá các cải tiến cú pháp mới có sẵn cho các hàm mũi tên, chẳng hạn như trả về ngầm định và bỏ qua dấu ngoặc đơn cho các hàm tham số đơn.

Để xem lại các hàm cơ bản, hãy đọc Cách xác định hàm trong JavaScript . Để đọc thêm về khái niệm phạm vi và lưu trữ trong JavaScript, hãy đọc Tìm hiểu biến, phạm vi và lưu trữ trong JavaScript .


Tags:

Các tin liên quan

Cách tạo phần tử kéo và thả với Vanilla JavaScript và HTML
2020-07-27
Hiểu các chữ mẫu trong JavaScript
2020-06-30
Cách sử dụng .map () để lặp lại thông qua các mục mảng trong JavaScript
2020-05-19
Hiểu về cấu trúc hủy, tham số khôi phục và cú pháp trải rộng trong JavaScript
2020-05-12
Cách gỡ lỗi JavaScript với Google Chrome DevTools và Visual Studio Code
2020-05-08
Thanh tiến trình trang với các biến JavaScript và CSS
2020-04-16
Xem xét API JavaScript của trình quan sát thay đổi kích thước
2020-04-16
Xem xét API control panel JavaScript
2020-04-16
Xem xét Đề xuất Nhà điều hành Đường ống JavaScript
2020-04-16
Cách triển khai các phương thức mảng JavaScript từ Scratch
2020-04-09