Nhiều chức năng mũi tên có nghĩa là gì trong javascript?


472

Tôi đã đọc một loạt reactmã và tôi thấy những thứ như thế này mà tôi không hiểu:

handleChange = field => e => {
  e.preventDefault();
  /// Do something here
}

11
Chỉ để cho vui, Kyle Simpson đưa tất cả các đường dẫn quyết định cho mũi tên vào biểu đồ dòng chảy này . Nguồn: Nhận xét của ông về bài đăng trên blog của Mozilla Hacks có tên ES6 In Depth: Arrow function
gfullam

Như có câu trả lời tuyệt vời và bây giờ là một tiền thưởng. Bạn có thể vui lòng giải thích những gì bạn không hiểu rằng các câu trả lời dưới đây không bao gồm.
Michael Warner

5
URL cho biểu đồ luồng chức năng mũi tên hiện bị hỏng vì có một phiên bản mới của cuốn sách. URL làm việc có tại raw.githubusercontent.com/getify/You-Dont-Know-JS/1st-ed/ gợi
Dhiraj Gupta

Câu trả lời:


831

Đó là một chức năng curried

Đầu tiên, kiểm tra chức năng này với hai tham số

const add = (x, y) => x + y
add(2, 3) //=> 5

Đây là một lần nữa ở dạng curried

const add = x => y => x + y

Đây là cùng 1 mã không có chức năng mũi tên

const add = function (x) {
  return function (y) {
    return x + y
  }
}

Tập trung vào return

Nó có thể giúp hình dung nó theo một cách khác. Chúng tôi biết rằng các hàm mũi tên hoạt động như thế này - chúng ta hãy chú ý đặc biệt đến giá trị trả về .

const f = someParam => returnValue

Vì vậy, addhàm của chúng ta trả về một hàm - chúng ta có thể sử dụng dấu ngoặc đơn để thêm rõ ràng. Các in đậm văn bản là giá trị trả về của hàm của chúng tôiadd

const add = x => (y => x + y)

Nói cách khác, addmột số số trả về một hàm

add(2) // returns (y => 2 + y)

Gọi chức năng curried

Vì vậy, để sử dụng chức năng curried của chúng tôi, chúng tôi phải gọi nó là một chút khác nhau

add(2)(3)  // returns 5

Điều này là do lệnh gọi hàm đầu tiên (bên ngoài) trả về hàm thứ hai (bên trong). Chỉ sau khi chúng ta gọi hàm thứ hai, chúng ta mới thực sự nhận được kết quả. Điều này càng rõ ràng hơn nếu chúng ta tách các cuộc gọi trên hai đường dây

const add2 = add(2) // returns function(y) { return 2 + y }
add2(3)             // returns 5

Áp dụng hiểu biết mới của chúng tôi vào mã của bạn

liên quan: Có gì khác biệt giữa ràng buộc, ứng dụng một phần và cà ri?

OK, bây giờ chúng tôi hiểu cách thức hoạt động, hãy xem mã của bạn

handleChange = field => e => {
  e.preventDefault()
  /// Do something here
}

Chúng ta sẽ bắt đầu bằng cách biểu diễn nó mà không sử dụng các hàm mũi tên

handleChange = function(field) {
  return function(e) {
    e.preventDefault()
    // Do something here
    // return ...
  };
};

Tuy nhiên, vì các chức năng mũi tên liên kết từ vựng this, nó thực sự sẽ trông giống như thế này

handleChange = function(field) {
  return function(e) {
    e.preventDefault()
    // Do something here
    // return ...
  }.bind(this)
}.bind(this)

Có lẽ bây giờ chúng ta có thể thấy những gì nó đang làm rõ ràng hơn. Các handleChangechức năng được tạo ra một chức năng cho một quy định field. Đây là một kỹ thuật React tiện dụng vì bạn bắt buộc phải thiết lập trình nghe của riêng mình trên mỗi đầu vào để cập nhật trạng thái ứng dụng của bạn. Bằng cách sử dụng handleChangechức năng, chúng tôi có thể loại bỏ tất cả các mã trùng lặp sẽ dẫn đến việc thiết lập trình changenghe cho từng trường. Mát mẻ!

1 Ở đây tôi không phải liên kết từ vựng thisaddhàm ban đầu không sử dụng bất kỳ ngữ cảnh nào, vì vậy việc bảo tồn nó trong trường hợp này không quan trọng.


Mũi tên nhiều hơn

Có thể sắp xếp nhiều hơn hai hàm mũi tên, nếu cần -

const three = a => b => c =>
  a + b + c

const four = a => b => c => d =>
  a + b + c + d

three (1) (2) (3) // 6

four (1) (2) (3) (4) // 10

Chức năng curried có khả năng những điều đáng ngạc nhiên. Dưới đây chúng ta thấy $được định nghĩa là một hàm được xử lý với hai tham số, nhưng tại trang web cuộc gọi, nó xuất hiện như thể chúng ta có thể cung cấp bất kỳ số lượng đối số nào. Curry là sự trừu tượng của arity -

const $ = x => k =>
  $ (k (x))
  
const add = x => y =>
  x + y

const mult = x => y =>
  x * y
  
$ (1)           // 1
  (add (2))     // + 2 = 3
  (mult (6))    // * 6 = 18
  (console.log) // 18
  
$ (7)            // 7
  (add (1))      // + 1 = 8
  (mult (8))     // * 8 = 64
  (mult (2))     // * 2 = 128
  (mult (2))     // * 2 = 256
  (console.log)  // 256

Ứng dụng một phần

Ứng dụng một phần là một khái niệm liên quan. Nó cho phép chúng ta áp dụng một phần các chức năng, tương tự như currying, ngoại trừ chức năng không phải được xác định ở dạng curried -

const partial = (f, ...a) => (...b) =>
  f (...a, ...b)

const add3 = (x, y, z) =>
  x + y + z

partial (add3) (1, 2, 3)   // 6

partial (add3, 1) (2, 3)   // 6

partial (add3, 1, 2) (3)   // 6

partial (add3, 1, 2, 3) () // 6

partial (add3, 1, 1, 1, 1) (1, 1, 1, 1, 1) // 3

Đây là bản demo hoạt động partialmà bạn có thể chơi trong trình duyệt của riêng mình -

const partial = (f, ...a) => (...b) =>
  f (...a, ...b)
  
const preventDefault = (f, event) =>
  ( event .preventDefault ()
  , f (event)
  )
  
const logKeypress = event =>
  console .log (event.which)
  
document
  .querySelector ('input[name=foo]')
  .addEventListener ('keydown', partial (preventDefault, logKeypress))
<input name="foo" placeholder="type here to see ascii codes" size="50">


2
Điều này là nổi bật! Bao lâu thì ai đó thực sự gán '$' mặc dù? Hay đó là một bí danh cho điều này trong phản ứng? Tha thứ cho sự thiếu hiểu biết của tôi cuối cùng, chỉ tò mò vì tôi không thấy một biểu tượng nhận được một bài tập quá thường xuyên trong các ngôn ngữ khác.
Caperneoignis

7
@Caperneoignis $đã được sử dụng để demo khái niệm này, nhưng bạn có thể đặt tên cho nó bất cứ điều gì bạn muốn. Trùng hợp nhưng hoàn toàn không liên quan, $ đã được sử dụng trong các thư viện phổ biến như jQuery, nơi $là điểm nhập cảnh toàn cầu cho toàn bộ thư viện hàm. Tôi nghĩ rằng nó cũng đã được sử dụng ở những người khác. Một thứ khác bạn sẽ thấy là _, phổ biến trong các thư viện như gạch dưới và lodash. Không một biểu tượng nào có ý nghĩa hơn biểu tượng khác; bạn gán ý nghĩa cho chương trình của bạn . Nó đơn giản là JavaScript hợp lệ: D
Cảm ơn bạn

1
Frijoli thánh, câu trả lời tốt đẹp. chúc các op sẽ chấp nhận
mtyson 26/03/19

2
@Blake Bạn có thể hiểu rõ hơn $bằng cách xem cách sử dụng nó. Nếu bạn đang hỏi về việc thực hiện chính nó, $là một hàm nhận giá trị xvà trả về một hàm mới k => .... Nhìn vào phần thân của hàm được trả về, chúng ta thấy k (x)chúng ta kcũng phải biết là một hàm và bất kể kết quả của k (x)nó được đưa vào là gì $ (...), mà chúng ta biết sẽ trả về một hàm khác k => ..., và nó sẽ đi ... Nếu bạn vẫn bị mắc kẹt, cho tôi biết.
Cảm ơn bạn

2
trong khi câu trả lời này giải thích cách thức hoạt động của nó và những mẫu nào có với kỹ thuật này. Tôi cảm thấy không có gì cụ thể về lý do tại sao đây thực sự là một giải pháp tốt hơn trong bất kỳ kịch bản nào. Trong tình huống nào, abc(1,2,3)là ít hơn lý tưởng hơn abc(1)(2)(3). Thật khó để lý luận về logic của mã và thật khó để đọc hàm abc và khó đọc lệnh gọi hàm hơn. Trước đây bạn chỉ cần biết abc làm gì, bây giờ bạn không chắc chắn những chức năng chưa được đặt tên mà abc sẽ trả về, và hai lần tại đó.
Muhammad Umer

57

Hiểu các cú pháp có sẵn của các hàm mũi tên sẽ cho bạn hiểu về hành vi mà chúng đang giới thiệu khi 'xâu chuỗi' như trong các ví dụ bạn cung cấp.

Khi một hàm mũi tên được viết mà không có dấu ngoặc khối, có hoặc không có nhiều tham số, biểu thức cấu thành cơ thể của hàm sẽ được trả về hoàn toàn . Trong ví dụ của bạn, biểu thức đó là một hàm mũi tên khác.

No arrow funcs              Implicitly return `e=>{…}`    Explicitly return `e=>{…}` 
---------------------------------------------------------------------------------
function (field) {         |  field => e => {            |  field => {
  return function (e) {    |                             |    return e => {
      e.preventDefault()   |    e.preventDefault()       |      e.preventDefault()
  }                        |                             |    }
}                          |  }                          |  }

Một lợi thế khác của việc viết các hàm ẩn danh bằng cú pháp mũi tên là chúng bị ràng buộc từ vựng theo phạm vi mà chúng được xác định. Từ 'Hàm mũi tên' trên MDN :

Một biểu hiện mũi tên chức năng có một cú pháp ngắn hơn so với biểu hiện chức năng và giải nghĩa từ vựng liên kết với các này giá trị. Các chức năng mũi tên luôn ẩn danh .

Điều này đặc biệt thích hợp trong ví dụ của bạn khi xem xét rằng nó được lấy từ một ứng dụng. Như @naomik đã chỉ ra, trong React, bạn thường truy cập các hàm thành viên của một thành phần bằng cách sử dụng this. Ví dụ:

Unbound                     Explicitly bound            Implicitly bound 
------------------------------------------------------------------------------
function (field) {         |  function (field) {       |  field => e => {
  return function (e) {    |    return function (e) {  |    
      this.setState(...)   |      this.setState(...)   |    this.setState(...)
  }                        |    }.bind(this)           |    
}                          |  }.bind(this)             |  }

53

Một mẹo chung, nếu bạn bị nhầm lẫn bởi bất kỳ cú pháp JS mới nào và cách nó sẽ biên dịch, bạn có thể kiểm tra babel . Ví dụ: sao chép mã của bạn trong babel và chọn cài đặt trước es2015 sẽ cho đầu ra như thế này

handleChange = function handleChange(field) {
 return function (e) {
 e.preventDefault();
  // Do something here
   };
 };

khỉ đầu chó


42

Hãy nghĩ về nó như thế này, mỗi khi bạn nhìn thấy một mũi tên, bạn thay thế nó bằng function.
function parametersđược xác định trước mũi tên.
Vì vậy, trong ví dụ của bạn:

field => // function(field){}
e => { e.preventDefault(); } // function(e){e.preventDefault();}

và sau đó cùng nhau:

function (field) { 
    return function (e) { 
        e.preventDefault(); 
    };
}

Từ các tài liệu :

// Basic syntax:
(param1, param2, paramN) => { statements }
(param1, param2, paramN) => expression
   // equivalent to:  => { return expression; }

// Parentheses are optional when there's only one argument:
singleParam => { statements }
singleParam => expression

6
Đừng quên đề cập đến các ràng buộc từ vựng this.
Cảm ơn bạn

30

Ngắn gọn và đơn giản

Đây là một hàm trả về một hàm khác được viết theo cách ngắn gọn.

const handleChange = field => e => {
  e.preventDefault()
  // Do something here
}

// is equal to 
function handleChange(field) {
  return function(e) {
    e.preventDefault()
    // Do something here
  }
}

Tại sao mọi người làm điều đó

Bạn đã phải đối mặt khi bạn cần phải viết một chức năng có thể được tùy chỉnh? Hoặc bạn phải viết một hàm gọi lại có các tham số (đối số) cố định, nhưng bạn cần truyền nhiều biến hơn cho hàm nhưng tránh các biến toàn cục? Nếu câu trả lời của bạn là " " thì đó là cách làm.

Ví dụ: chúng tôi có một buttoncuộc gọi lại với onClick. Và chúng ta cần truyền idvào hàm, nhưng onClickchỉ chấp nhận một tham số event, chúng ta không thể truyền các tham số phụ trong phạm vi như thế này:

const handleClick = (event, id) {
  event.preventDefault()
  // Dispatch some delete action by passing record id
}

Nó sẽ không làm việc!

Do đó, chúng ta tạo một hàm sẽ trả về hàm khác với phạm vi biến riêng mà không có biến toàn cục, bởi vì biến toàn cục là xấu.

Bên dưới hàm handleClick(props.id)}sẽ được gọi và trả về một hàm và nó sẽ có idtrong phạm vi của nó! Cho dù nhấn bao nhiêu lần, các id sẽ không ảnh hưởng hoặc thay đổi lẫn nhau, chúng hoàn toàn bị cô lập.

const handleClick = id => event {
  event.preventDefault()
  // Dispatch some delete action by passing record id
}

const Confirm = props => (
  <div>
    <h1>Are you sure to delete?</h1>
    <button onClick={handleClick(props.id)}>
      Delete
    </button>
  </div
)


2

Ví dụ trong câu hỏi của bạn là câu hỏi curried functionsử dụng arrow functionvà có một implicit returnđối số đầu tiên.

Hàm mũi tên liên kết từ vựng này tức là chúng không có thisđối số riêng nhưng lấy thisgiá trị từ phạm vi kèm theo

Một mã tương đương ở trên sẽ là

const handleChange = (field) {
  return function(e) {
     e.preventDefault();
     /// Do something here
  }.bind(this);
}.bind(this);

Một điều nữa cần lưu ý về ví dụ của bạn là định nghĩa handleChangelà const hoặc hàm. Có lẽ bạn đang sử dụng nó như là một phần của phương thức lớp và nó sử dụng mộtclass fields syntax

vì vậy thay vì liên kết trực tiếp hàm ngoài, bạn sẽ liên kết nó trong hàm tạo của lớp

class Something{
    constructor(props) {
       super(props);
       this.handleChange = this.handleChange.bind(this);
    }
    handleChange(field) {
        return function(e) {
           e.preventDefault();
           // do something
        }
    }
}

Một điều khác cần lưu ý trong ví dụ là sự khác biệt giữa lợi nhuận ngầm và rõ ràng.

const abc = (field) => field * 2;

Trên đây là một ví dụ về lợi nhuận ngầm. nó lấy trường giá trị làm đối số và trả về kết quả field*2chỉ định rõ ràng hàm trả về

Để trả về rõ ràng, bạn sẽ nói rõ phương thức trả về giá trị

const abc = () => { return field*2; }

Một điều khác cần lưu ý về các chức năng mũi tên là chúng không có riêng argumentsnhưng cũng thừa hưởng điều đó từ phạm vi cha mẹ.

Ví dụ: nếu bạn chỉ định nghĩa một hàm mũi tên như

const handleChange = () => {
   console.log(arguments) // would give an error on running since arguments in undefined
}

Là một hàm mũi tên thay thế cung cấp các tham số còn lại mà bạn có thể sử dụng

const handleChange = (...args) => {
   console.log(args);
}

1

Nó có thể không hoàn toàn liên quan, nhưng vì câu hỏi được đề cập đến phản ứng sử dụng trường hợp (và tôi tiếp tục va vào luồng SO này): Có một khía cạnh quan trọng của chức năng mũi tên kép không được đề cập rõ ràng ở đây. Chỉ mũi tên 'đầu tiên' (chức năng) được đặt tên (và do đó 'có thể phân biệt' theo thời gian chạy), bất kỳ mũi tên nào sau đây đều ẩn danh và từ quan điểm React được tính là một đối tượng 'mới' trên mỗi kết xuất.

Do đó, chức năng mũi tên kép sẽ khiến bất kỳ PureComponent nào cũng phải chạy lại mọi lúc.

Thí dụ

Bạn có một thành phần cha mẹ với trình xử lý thay đổi là:

handleChange = task => event => { ... operations which uses both task and event... };

và với một kết xuất như:

{ tasks.map(task => <MyTask handleChange={this.handleChange(task)}/> }

xử lýChange sau đó được sử dụng trên một đầu vào hoặc nhấp chuột. Và tất cả điều này hoạt động và trông rất đẹp. NHƯNG điều đó có nghĩa là bất kỳ thay đổi nào sẽ khiến cha mẹ đăng ký lại (như thay đổi trạng thái hoàn toàn không liên quan) cũng sẽ hiển thị lại TẤT CẢ MyTask của bạn mặc dù chúng là PureComponents.

Điều này có thể được giảm bớt bằng nhiều cách như vượt qua mũi tên 'ngoài cùng' và đối tượng bạn sẽ cung cấp cho nó hoặc viết một hàm nênUpdate tùy chỉnh hoặc quay lại các vấn đề cơ bản như viết các hàm có tên (và ràng buộc điều này bằng tay ...)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.