ข้อแตกต่างของ bind, apply และ call ใน JavaScript กับการใช้งาน

Nuttavut Thongjor

ผมเชื่อว่าหลายคนในที่นี้รู้จัก bind apply และ call ใน JavaScript เป็นอย่างดี แต่ไม่ใช่สำหรับบางคน และคนกลุ่มนั้นหละครับคือเป้าหมายของบทความนี้

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

JavaScript
1const obj = {
2 firstName: 'Nuttavut',
3 lastName: 'Thongjor',
4 getFullName() {
5 // firstName และ lastName มาจาก obj
6 return `${this.firstName} ${this.lastName}`
7 },
8}
9
10console.log(obj.getFullName()) // Nuttavut Thongjor

ในตัวอย่างนี้ดูตรงไปตรงมามากครับ obj เป็นผู้เรียกใช้ฟังก์ชัน getFullName ดังนั้น this ในที่นี้จึงหมายถึงตัว obj เอง ทำให้เมื่อเรากล่าวถึง this.firstName จึงหมายถึง firstName ของ obj

แล้วถ้าเป็นตัวอย่างนี้หละ?

JavaScript
1const print = (fn) => {
2 // เรียกใช้งาน getFullName ที่ส่งเข้ามา
3 console.log(fn())
4}
5
6const obj = {
7 firstName: 'Nuttavut',
8 lastName: 'Thongjor',
9 getFullName() {
10 return `${this.firstName} ${this.lastName}`
11 },
12}
13
14print(obj.getFullName) // Cannot set property 'firstName' of undefined

เราสร้างฟังก์ชันชื่อ print ขึ้นมาเพื่อรับฟังก์ชั่นเข้ามาอีกทีในชื่อของ fn จากนั้นจึงทำการเรียกฟังก์ชันที่ส่งเข้ามา พบว่าการเรียกฟังก์ชันนี้ไม่ได้อ้างอิงจากอ็อบเจ็กต์อื่นใดเหมือนกับตัวอย่างบน ตัวอย่างบนเราเรียก getFullName ผ่าน obj แต่ตัวอย่างนี้เราเรียก fn ที่ส่งเข้ามาลอยๆ จึงมีผลทำให้ this ว่างเปล่า

เธอคือแสงสว่างในยามที่ฉันหลงทาง

เพื่อให้ทุกอย่างถูกต้อง เราจึงต้องบอกใบ้ให้ JavaScript รู้ว่าเราจะใช้ this จากไหน ตัวอย่างข้างบนเราต้องการให้ this มีค่าเป็น obj เพื่อที่จะได้เรียกใช้ firstName และ lastName ของ obj ได้ เธอผู้เป็นดังแสงเทียนคอยชี้ทาง this ให้กับเรานั้นคือ bind นั่นเองครับ เราอยากให้ this หมายถึงใครก็ bind เข้ากับสิ่งนั้น

แก้ไขตัวอย่างนิดหน่อยเพื่อให้ทำงานได้ดังนี้

JavaScript
1const print = (fn) => {
2 console.log(fn())
3}
4
5const obj = {
6 firstName: 'Nuttavut',
7 lastName: 'Thongjor',
8 getFullName() {
9 console.log(this)
10 return `${this.firstName} ${this.lastName}`
11 },
12}
13
14// bind this เข้ากับ obj
15print(obj.getFullName.bind(obj))

bind ผู้ตาบอดสอดตาเห็น

เพราะความที่ bind นั้นใช้จับคู่ this เราจึงใช้ bind ทำสิ่งต่างๆต่อไปนี้ได้ครับ

หยิบยืมฟังก์ชันคนอื่นมาใช้

สมมติเรามีอ็อบเจ็กที่หมายถึงเครื่องปริ้นยี่ห้อ Canon โดยในอ็อบเจ็กต์นี้มีฟังก์ชัน print สำหรับปริ้นข้อความอยู่ ต่อมาเรามีอ็อบเจ็กต์ HP ที่สามารถปริ้นข้อความได้เหมือนกัน เราไม่อยากให้ HP ต้องมีฟังก์ชัน print อีกแล้ว ขอยืม print จาก Canon หน่อยนะตัวเอง ก็สามารถทำได้ครับ ดังนี้ (จะได้ค่าโฆษณาสินค้าไหมเนี่ย)

JavaScript
1const canon = {
2 text: 'Canon Text',
3 print() {
4 console.log(this.text)
5 },
6}
7
8const hp = {
9 text: 'HP InkJet',
10}
11
12console.log(canon.print()) // Canon Text
13// bind hp ให้เป็น this ดังนั้น this.text จึงหมายถึง text ของ hp
14console.log(canon.print.bind(hp)()) // HP InkJet

Partial function application

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

JavaScript
1const sayHi = (gender, age, name) => {
2 console.log(`Hey guys, my name is ${name}. I'm a ${age} year old ${gender}`)
3}
4
5// นักเรียนคนที่ 1
6sayHi('male', 12, 'Somkiat')
7// นักเรียนคนที่ 2
8sayHi('male', 12, 'Somset')
9// นักเรียนคนที่ 3
10sayHi('male', 12, 'Somtum')

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

JavaScript
1const sayHi = (gender, age, name) => {
2 console.log(`Hey guys, my name is ${name}. I'm a ${age} year old ${gender}`)
3}
4
5const male12SayHi = sayHi.bind(null, 'male', 12)
6
7male12SayHi('Somkiat')
8male12SayHi('Somset')
9male12SayHi('Somtum')

บรรทัดที่5เราใช้ bind เพื่อผูกค่า gender เป็น male และผูกค่า age เป็น 12 สังเกตสิ่งแรกที่เราส่งเข้าไปคือ null เป็นการบอกว่าเราไม่ได้ต้องการผูก this เข้ากับสิ่งใดเพราะเราไม่ได้ใช้ this นั่นเอง นี่หละครับคือสิ่งที่เราเรียกว่า Function Currying หรือ Partial function application

เรียกใช้งานฟังก์ชันพร้อมตั้งค่า this ด้วย apply และ call

ถ้าเรามีฟังก์ชัน sayHello เราสามารถเรียกฟังก์ชันนี้อย่างง่ายดายด้วยการใส่วงเล็บเป็น sayHello() แต่ถ้าต้องการติดตั้ง this ให้เรียกใช้งานได้ถูกได้ควรเหมือนการใช้ bind จะทำอย่างไร?

เราสามารถใช้ apply และ call ในการเรียกฟังก์ชันโดยส่งผ่าน this เข้าไปเป็นพารามิเตอร์ตัวแรกได้ สิ่งที่แตกต่างระหว่าง apply และ call คือในพารามิเตอร์ตัวถัดไปของ apply จะเป็นอาร์เรย์แต่ไม่ใช่สำหรับ call ดังนี้

JavaScript
1const obj = {
2 name: 'Nuttavut Thongjor',
3 sayHello(day) {
4 console.log(`Hi, ${this.name}. Today is ${day}.`)
5 },
6}
7
8const obj2 = {
9 name: 'Somkiat',
10}
11
12obj.sayHello('Mon') // Hi, Nuttavut Thongjor. Today is Mon.
13obj.sayHello.call(obj2, 'Wed') // Hi, Somkiat. Today is Wed.
14
15// ต้องส่งเป็นอาร์เรย์
16obj.sayHello.apply(obj2, ['Fri']) // Hi, Somkiat. Today is Fri.

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

JavaScript
1const numbers = [1, 2, 33, 99, 77, 4]
2
3// ส่ง null เป็นค่าแรกเพราะไม่ต้องการใช้ this
4// ปกติ Math.min ไม่รับอาร์เรย์เราต้องส่งแบบนี้ Math.min(1, 2, 3)
5// แต่เราต้องการใช้อาร์เรย์ เลยต้องอาศัย apply ช่วย
6console.log(Math.min.apply(null, numbers)) //1

Array-like object

array-like object คืออ็อบเจ็กต์ที่มีโครงสร้างเหมือนอาร์เรย์ใน JavaScript หน้าตาของมันก็ประมาณนี้ละฮะ

JavaScript
1const arrLike = {
2 // มี index บอกว่าเป็นช่องที่เท่าไหร่
3 0: 'Somkiat',
4 1: 'Somsree',
5 2: 'Somtum',
6 // มี length บอกจำนวนช่องชองอาร์เรย์
7 length: 3,
8}

เราสามารถใช้ call และ bind ช่วยจัดการกับสิ่งนี้ได้ดังนี้

JavaScript
1const arrLike = {
2 0: 'Somkiat',
3 1: 'Somsree',
4 2: 'Somtum',
5 length: 3,
6}
7
8// ใช้ฟังก์ชัน indexOf ของอาร์เรย์เพื่อหา Somtum ใน arrLike
9console.log(Array.prototype.indexOf.call(arrLike, 'Somtum'))

array-like อ็อบเจ็กต์ตัวสำคัญที่ผมเชื่อว่าหลายคนคงเคยเห็นก็คือ arguments ที่เป็นตัวบอกว่ามีพารามิเตอร์อะไรส่งเข้ามาในฟังก์ชันบ้าง

JavaScript
1function fn(a, b, c) {
2 console.log(arguments)
3}
4
5// หน้าตาเป็น array-like เลยเห็นไหม
6fn(1, 2, 3) // {"0":1,"1":2,"2":3}

เมื่อเป็นเช่นนี้เราจึงสามารถยำมันได้ตามแบบฉบับของการใช้ call และ apply เช่นเราต้องการละเว้นพารามิเตอร์ที่ส่งเข้ามาทุกตัว ต้องการเฉพาะตัวสุดท้าย เราสามารถเขียนได้ดังนี้

JavaScript
1function fn(a, b, c) {
2 console.log(Array.prototype.slice.call(arguments, -1))
3}
4
5fn(1, 2, 3) // [3]

เป็นยังไงบ้างครับหวังว่าบทความนี้จะมีส่วนช่วยให้เพื่อนๆที่ยังสับสนกับการใช้ bind apply และ call ได้เข้าใจความหมายของ this กับการประยุกต์ใช้ด้วยสามฟังก์ชันนี้มากขึ้นนะครับ

สารบัญ

สารบัญ

  • เธอคือแสงสว่างในยามที่ฉันหลงทาง
  • bind ผู้ตาบอดสอดตาเห็น
  • เรียกใช้งานฟังก์ชันพร้อมตั้งค่า this ด้วย apply และ call
  • Array-like object