ฟังก์ชั่นลูกศรหลายความหมายใน javascript คืออะไร?


472

ฉันอ่านreactรหัสมาแล้วและฉันเห็นสิ่งต่าง ๆ ที่ฉันไม่เข้าใจ:

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

11
เพื่อความสนุกKyle Simpson ได้รวบรวมเส้นทางการตัดสินใจทั้งหมดสำหรับลูกศรในแผนภูมิการไหลนี้ ที่มา: ความคิดเห็นของเขาเกี่ยวกับการโพสต์บล็อก Mozilla Hacks ได้รับสิทธิES6 ในเชิงลึก: ฟังก์ชั่นลูกศร
gfullam

เนื่องจากมีคำตอบที่ดีและตอนนี้ความโปรดปราน คุณช่วยอธิบายรายละเอียดเกี่ยวกับสิ่งที่คุณไม่เข้าใจได้อย่างไรว่าคำตอบด้านล่างไม่ครอบคลุม
Michael Warner

5
ตอนนี้ URL สำหรับลูกศรฟังก์ชั่นแผนภูมิการทำงานของลูกศรขาดเนื่องจากมีหนังสือรุ่นใหม่ URL การทำงานอยู่ที่raw.githubusercontent.com/getify/You-Dont-Know-JS/1st-ed/ …
Dhiraj Gupta

คำตอบ:


831

นั่นคือฟังก์ชั่น curried

ก่อนอื่นให้ตรวจสอบฟังก์ชันนี้ด้วยพารามิเตอร์สองตัว ...

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

ที่นี่เป็นอีกครั้งในรูปแบบ curried ...

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

นี่คือรหัส1เดียวกันโดยไม่มีฟังก์ชั่นลูกศร ...

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

เน้นไปที่ return

มันอาจช่วยให้เห็นภาพอีกวิธีหนึ่ง เรารู้ว่าฟังก์ชั่นการทำงานของลูกศรเช่นนี้ - ความสนใจโดยเฉพาะให้ของกับค่าตอบแทน

const f = someParam => returnValue

ดังนั้นaddฟังก์ชั่นของเราส่งคืนฟังก์ชัน - เราสามารถใช้วงเล็บเพื่อเพิ่มความชัดเจน ตัวหนาข้อความเป็นค่าตอบแทนของฟังก์ชั่นของเราadd

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

ในคำอื่น ๆaddของบางตัวเลขส่งกลับฟังก์ชั่น

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

เรียกฟังก์ชั่น curried

ดังนั้นเพื่อที่จะใช้ฟังก์ชั่น curried ของเราเราต้องเรียกมันว่าแตกต่างออกไปเล็กน้อย ...

add(2)(3)  // returns 5

นี่เป็นเพราะการเรียกใช้ฟังก์ชันแรก (ด้านนอก) ส่งกลับฟังก์ชันที่สอง (ภายใน) หลังจากที่เราเรียกฟังก์ชั่นที่สองแล้วเราจะได้ผลลัพธ์ สิ่งนี้ชัดเจนมากขึ้นถ้าเราแยกการโทรออกเป็นสองบรรทัด ...

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

การใช้ความเข้าใจใหม่ของเรากับรหัสของคุณ

เกี่ยวข้อง: ” อะไรคือความแตกต่างระหว่างการผูกแอปพลิเคชั่นบางส่วนและการแกง?”

ตกลงตอนนี้เราเข้าใจวิธีการทำงานแล้วลองดูรหัสของคุณ

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

เราจะเริ่มต้นด้วยการแสดงมันโดยไม่ใช้ฟังก์ชั่นลูกศร ...

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

อย่างไรก็ตามเนื่องจากฟังก์ชั่นลูกศรผูกเข้าthisด้วยกันจริง ๆ แล้วมันจะมีลักษณะเช่นนี้ ...

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

บางทีตอนนี้เราสามารถเห็นสิ่งที่กำลังทำอยู่ชัดเจนยิ่งขึ้น ฟังก์ชั่นคือการสร้างฟังก์ชั่นสำหรับการระบุhandleChange fieldนี่เป็นเทคนิคการตอบโต้ที่สะดวกเพราะคุณต้องตั้งค่าผู้ฟังของคุณเองในแต่ละอินพุตเพื่ออัปเดตสถานะแอปพลิเคชันของคุณ โดยการใช้handleChangeฟังก์ชั่นเราสามารถกำจัดรหัสที่ซ้ำกันทั้งหมดซึ่งจะส่งผลให้การตั้งค่าchangeฟังสำหรับแต่ละฟิลด์ เย็น!

1 ที่นี่ฉันไม่จำเป็นต้องผูกคำศัพท์thisเพราะaddฟังก์ชั่นดั้งเดิมไม่ได้ใช้บริบทใด ๆ ดังนั้นจึงไม่สำคัญที่จะต้องรักษามันไว้ในกรณีนี้


ลูกศรมากยิ่งขึ้น

ฟังก์ชั่นลูกศรมากกว่าสองรายการสามารถจัดลำดับได้หากจำเป็น -

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

ฟังก์ชั่น Curried มีความสามารถในสิ่งที่น่าแปลกใจ ด้านล่างเราเห็น$ว่าฟังก์ชั่น curried มีสองพารามิเตอร์ แต่ที่ไซต์การโทรดูเหมือนว่าเราสามารถระบุอาร์กิวเมนต์จำนวนเท่าใดก็ได้ ความดีความชอบเป็นนามธรรมของ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

แอปพลิเคชั่นบางส่วน

แอปพลิเคชันบางส่วนเป็นแนวคิดที่เกี่ยวข้อง มันช่วยให้เราสามารถใช้ฟังก์ชั่นบางส่วนคล้ายกับการแกงยกเว้นฟังก์ชั่นไม่จำเป็นต้องกำหนดในรูปแบบ 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

นี่คือตัวอย่างการทำงานที่partialคุณสามารถเล่นกับเบราว์เซอร์ของคุณเอง -

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
นี่มันยอดเยี่ยม! มีใครบางคนไม่ได้กำหนด '$' จริงหรือ? หรือมันเป็นนามแฝงสำหรับเรื่องนี้ในการตอบสนอง? ให้อภัยความไม่รู้ของฉันในที่สุดเพียงแค่อยากรู้อยากเห็นเพราะฉันไม่เห็นสัญลักษณ์ที่ได้รับมอบหมายบ่อยเกินไปในภาษาอื่น ๆ
Caperneoignis

7
@Caperneoignis $ถูกใช้เพื่อสาธิตแนวคิด แต่คุณสามารถตั้งชื่อสิ่งที่คุณต้องการ บังเอิญ แต่ที่ไม่เกี่ยวข้องอย่างสมบูรณ์$ ได้ถูกนำมาใช้ในห้องสมุดได้รับความนิยมเช่น jQuery ซึ่ง$เป็นประเภทของจุดเข้าทั่วโลกไปยังห้องสมุดทั้งหมดของฟังก์ชั่น ฉันคิดว่ามันถูกใช้ในคนอื่นด้วย อีกสิ่งที่คุณจะเห็นคือเป็นที่_นิยมในห้องสมุดเช่นขีดล่างและ lodash ไม่มีใครมีความหมายมากกว่าสัญลักษณ์อื่น; คุณกำหนดความหมายสำหรับโปรแกรมของคุณ มันเป็นเพียง JavaScript ที่ถูกต้อง: D
ขอบคุณ

1
ศักดิ์สิทธิ์ frijoli คำตอบที่ดี หวังว่า op จะยอมรับ
mtyson

2
@Blake คุณสามารถทำความเข้าใจ$กับการใช้งานได้ดีขึ้น หากคุณกำลังถามเกี่ยวกับการดำเนินงานของตัวเอง$เป็นฟังก์ชั่นที่ได้รับความคุ้มค่าและผลตอบแทนฟังก์ชั่นใหม่x k => ...มองไปที่ร่างของฟังก์ชั่นกลับมาเราจะเห็นk (x)เพื่อให้เรารู้kยังต้องฟังก์ชั่นและสิ่งผลมาจากการk (x)ถูกนำกลับมาได้รับในการ$ (...)ที่เรารู้ว่าผลตอบแทนอื่นk => ...และมันจะไป ... หากคุณยังคง ติดขัดให้ฉันรู้
ขอบคุณ

2
ในขณะที่คำตอบนี้อธิบายวิธีการทำงานและรูปแบบที่มีกับเทคนิคนี้ ฉันรู้สึกว่าไม่มีอะไรที่เฉพาะเจาะจงว่าทำไมนี่จึงเป็นทางออกที่ดีกว่าในทุกสถานการณ์ ในสิ่งที่สถานการณ์น้อยกว่าเหมาะกว่าabc(1,2,3) abc(1)(2)(3)มันยากที่จะให้เหตุผลเกี่ยวกับตรรกะของรหัสและมันยากที่จะอ่านฟังก์ชั่น abc และมันยากที่จะอ่านการเรียกใช้ฟังก์ชัน ก่อนที่คุณจะต้องการเพียงรู้ว่า abc ทำอะไรตอนนี้คุณไม่แน่ใจว่าฟังก์ชัน abc ที่ไม่มีชื่อกลับมาทำอะไร
มูฮัมหมัด

57

การทำความเข้าใจไวยากรณ์ที่มีอยู่ของฟังก์ชั่นลูกศรจะทำให้คุณเข้าใจถึงพฤติกรรมที่พวกเขาแนะนำเมื่อ 'ผูกมัด' เหมือนในตัวอย่างที่คุณให้ไว้

เมื่อเขียนฟังก์ชันลูกศรโดยไม่มีเครื่องหมายปีกกาบล็อกโดยมีหรือไม่มีพารามิเตอร์หลายตัวนิพจน์ที่ถือเป็นเนื้อความของฟังก์ชันจะถูกส่งกลับโดยปริยาย ในตัวอย่างของคุณนิพจน์นั้นเป็นฟังก์ชันลูกศรอื่น

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

ข้อดีอีกอย่างของการเขียนฟังก์ชั่นที่ไม่ระบุชื่อโดยใช้ไวยากรณ์ลูกศรก็คือมันจะถูกผูกเข้ากับขอบเขตที่กำหนดไว้ จาก'ฟังก์ชั่นลูกศร' บน MDN :

การแสดงออกของฟังก์ชั่นลูกศรมีไวยากรณ์ที่สั้นกว่าเมื่อเทียบกับการแสดงออกของฟังก์ชั่นและผูกค่านี้ศัพท์ ฟังก์ชั่นลูกศรอยู่เสมอที่ไม่ระบุชื่อ

นี่เป็นตัวอย่างที่เกี่ยวข้องโดยเฉพาะอย่างยิ่งเมื่อพิจารณาว่านำมาจาก ใบสมัคร เท่าที่ออกมาชี้โดย @naomik ในการตอบสนองคุณมักจะเข้าถึงองค์ประกอบของฟังก์ชั่นสมาชิกthisใช้ ตัวอย่างเช่น:

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

เคล็ดลับทั่วไปถ้าคุณได้รับสับสนใด ๆ ของไวยากรณ์ JS ใหม่และวิธีการที่จะรวบรวมคุณสามารถตรวจสอบBabel ตัวอย่างเช่นการคัดลอกรหัสของคุณใน Babel และเลือกการตั้งค่า es2015 จะให้ผลลัพธ์เช่นนี้

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

ที่จอแจอึกทึก


42

functionคิดว่ามันเป็นแบบนี้ทุกครั้งที่คุณเห็นลูกศรทุกท่านแทนที่ด้วย
function parametersถูกกำหนดไว้ก่อนลูกศร
ดังนั้นในตัวอย่างของคุณ:

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

และจากนั้นเข้าด้วยกัน:

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

จากเอกสาร :

// 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
thisอย่าลืมที่จะกล่าวถึงขอบเขต lexically
ขอบคุณ

30

สั้น ๆ และเรียบง่าย 🎈

มันเป็นฟังก์ชั่นที่ส่งกลับฟังก์ชั่นอื่นที่เขียนในทางสั้น ๆ

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

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

ทำไมคนทำมัน

คุณเคยประสบเมื่อต้องเขียนฟังก์ชั่นที่สามารถปรับแต่งได้หรือไม่? หรือคุณต้องเขียนฟังก์ชั่นการโทรกลับซึ่งมีพารามิเตอร์คงที่ (อาร์กิวเมนต์) แต่คุณต้องส่งตัวแปรเพิ่มเติมไปยังฟังก์ชัน แต่หลีกเลี่ยงตัวแปรทั่วโลก หากคำตอบของคุณคือ " ใช่ " นั่นเป็นวิธีที่จะทำ

ตัวอย่างเช่นเรามีbuttonwith onClick callback และเราจำเป็นต้องส่งผ่านidไปยังฟังก์ชั่น แต่onClickยอมรับเพียงหนึ่งพารามิเตอร์eventเท่านั้นเราไม่สามารถส่งพารามิเตอร์เพิ่มเติมภายในดังนี้:

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

มันจะไม่ทำงาน!

ดังนั้นเราจึงสร้างฟังก์ชั่นที่จะคืนค่าฟังก์ชันอื่นด้วยขอบเขตของตัวแปรที่ไม่มีตัวแปรทั่วโลกเพราะตัวแปรทั่วโลกเป็น evil ที่ชั่วร้าย

ฟังก์ชั่นด้านล่างhandleClick(props.id)}จะถูกเรียกและส่งคืนฟังก์ชั่นและมันจะมีidอยู่ในขอบเขต! ไม่ว่าจะกดกี่ครั้งรหัสจะไม่ส่งผลหรือเปลี่ยนแปลงซึ่งกันและกัน

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

ตัวอย่างในคำถามของคุณคือตัวอย่างcurried functionที่ใช้arrow functionและมีimplicit returnอาร์กิวเมนต์แรก

ฟังก์ชั่นลูกศรผูกอย่างนี้คือพวกเขาไม่มีthisข้อโต้แย้งของตัวเองแต่รับthisค่าจากขอบเขตการปิดล้อม

เทียบเท่ากับรหัสข้างต้นจะเป็น

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

อีกสิ่งหนึ่งที่ควรทราบเกี่ยวกับตัวอย่างของคุณคือการกำหนดhandleChangeเป็น const หรือฟังก์ชัน อาจเป็นเพราะคุณใช้มันเป็นส่วนหนึ่งของวิธีการเรียนและใช้class fields syntax

ดังนั้นแทนที่จะผูกฟังก์ชั่นด้านนอกโดยตรงคุณจะผูกมันไว้ในตัวสร้างคลาส

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

อีกสิ่งที่ควรทราบในตัวอย่างคือความแตกต่างระหว่างผลตอบแทนโดยนัยและชัดเจน

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

ด้านบนเป็นตัวอย่างของการส่งคืนโดยนัยคือ ใช้ฟิลด์ค่าเป็นอาร์กิวเมนต์และส่งคืนผลลัพธ์field*2ซึ่งระบุฟังก์ชันที่จะส่งคืนอย่างชัดเจน

สำหรับการส่งคืนอย่างชัดเจนคุณจะบอกวิธีการคืนค่าอย่างชัดเจน

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

สิ่งที่ควรทราบเกี่ยวกับฟังก์ชั่นลูกศรก็คือพวกเขาไม่มีของตัวเองargumentsแต่สืบทอดมาจากขอบเขตผู้ปกครองเช่นกัน

ตัวอย่างเช่นหากคุณเพิ่งกำหนดฟังก์ชั่นลูกศรเช่น

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

ในฐานะที่เป็นฟังก์ชั่นลูกศรทางเลือกให้พารามิเตอร์ที่เหลือที่คุณสามารถใช้

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

1

มันอาจจะไม่เกี่ยวข้องกันโดยสิ้นเชิง แต่เนื่องจากคำถามที่กล่าวถึงปฏิกิริยาใช้ case (และฉันเก็บไว้ในเธรด SO นี้): มีสิ่งสำคัญอย่างหนึ่งของฟังก์ชั่น double arrow ซึ่งไม่ได้กล่าวถึงอย่างชัดเจนที่นี่ เฉพาะลูกศร 'แรก' (ฟังก์ชัน) เท่านั้นที่ได้รับการตั้งชื่อ (และทำให้ 'แยกแยะได้' ในขณะใช้งาน) ลูกศรต่อไปนี้ไม่ระบุชื่อและจากมุมมองการตอบสนองนับเป็นวัตถุ 'ใหม่' ในทุกการแสดงผล

ดังนั้นฟังก์ชั่นลูกศรคู่จะทำให้ PureComponent ใด ๆ แสดงผลตลอดเวลา

ตัวอย่าง

คุณมีองค์ประกอบหลักที่มีตัวจัดการการเปลี่ยนแปลงดังนี้:

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

และด้วยการแสดงผลเช่น:

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

handleChange จากนั้นใช้กับอินพุตหรือคลิก ทั้งหมดนี้ใช้งานได้และดูดีมาก แต่หมายความว่าการเปลี่ยนแปลงใด ๆ ที่จะทำให้ผู้ปกครองแสดงผล (เช่นการเปลี่ยนแปลงสถานะที่ไม่เกี่ยวข้องอย่างสมบูรณ์) จะทำให้ MyTask ของคุณแสดงผลทั้งหมดอีกครั้งแม้ว่าพวกเขาจะเป็น PureComponents ก็ตาม

สิ่งนี้สามารถบรรเทาได้หลายวิธีเช่นการส่งลูกศร 'นอกสุด' และวัตถุที่คุณจะป้อนหรือเขียนฟังก์ชัน shouldUpdate ที่กำหนดเองหรือกลับไปสู่พื้นฐานเช่นการเขียนฟังก์ชันที่มีชื่อ (และผูกสิ่งนี้ด้วยตนเอง ... )

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.