ติดตามอ่านบทความเกี่ยวกับ JavaScript อื่นๆ ได้ที่
บทความชุด: JavaScript/Fundamental for beginner
รวมเนื้อหาการใช้ภาษา JavaScript สำหรับมือใหม่ ตั้งแต่หลักการ แนวคิด การทำงานกับเว็บทั้งฝั่งclient-server library&frameworkที่น่าสนใจ จนถึงมาตราฐานการเขียนแบบใหม่ ES6 (ECMAScript6)
สำหรับคนที่เคยเขียนโปรแกรมแบบ OOP (ใครไม่รู้จักไปอ่านเพิ่มเติมได้ที่นี่) น่าจะคุ้นเคยกับตัวแปรที่ชื่อว่า this ซึ่งโดยปกติแล้วจะหมายถึงตัว object ที่กำลังอ้างถึงอยู่
เช่น
class Dummy { int x; function set(y){ this.x = y; } } a = new Dummy(); a.set(10); b = new Dummy(); b.set(20);
ตัวอย่างข้างบน คลาสมีการประกาศตัวแปรชื่อ x เอาไว้ เมื่อเราจะสั่งงานใน method ให้ทำอะไรกับตัวแปร x นี่เราจะต้องขึ้นด้วยการบอกว่า this.x เพื่อเป็นการบอกว่าเราจะยุ่งกับ x ที่เป็นตัวแปรของคลาสข้างนอกโน้นนะ (ในบางภาษาเช่น Java สามารถละคีย์เวิร์ด this ได้แต่ตามหลักคือมันก็จะใส่ this ให้เองนั่นแหละ)
แต่มีอยู่ภาษาหนึ่งที่คีย์เวิร์ด this ทำตัวไม่ค่อยเหมือนภาษาชาวบ้านคนอื่นเขาเท่าไหร่ นั่นคือ JavaScript
ทวนพื้นฐานฟังก์ชั่นใน js กันนิดนึง
การสร้างฟังก์ชั่นใน js นั้นทำได้ง่ายมากมาย ประมาณนี้
function add(x, y){ return x + y; } //หรือ var add = function(x, y){ return x + y; }
สร้างง่ายมาก ไม่ยากเลย ส่วนเวลาเรียกใช้งานก็
var ans = add(10, 20);
แต่ถ้าเข้าใจ js เราจะรู้ว่าชนิดตัวแปรหรือ data type ใน js น่ะแบ่งออกเป็น 2 ประเภทหลักๆ คือ primitive (ประกอบด้วย number, string, boolean, null, undefined) และอีกประภทคือ object (พวก Object, Date, Array, RegExp, และอื่นๆ รวมไปถึง Function)
หมายความการที่เราสร้าง function ขึ้นมาตัวหนึ่งน่ะ js มันจะมองฟังก์ชั่นของเราเป็น object ล่ะ!
นั่นคือเราสามารถสร้างฟังก์ชันขึ้นมาด้วยการเขียนแบบนี้ก็ได้
var add = new Function('x', 'y', 'return x + y;');
แต่ก็นะ .. คนปกติทั่วไปคนไม่สร้างฟังก์ชันขึ้นมาด้วย syntax แบบนี้หรอก
ในเมื่อ js มอง Function เป็นตัวแปรหนึ่งในประเภท object การที่เราเรียกใช้งาน add(10, 20) น่ะ เจ้า js มันจะแปลงโค้ดเราเป็นแบบนี้
var ans = add.call(???, 10, 20);
คำสั่ง .call เป็นคำสั่งที่เอาไว้เรียกใช้งานฟังก์ชั่นว่าแกน่ะ เริ่มทำงานได้แล้วนะ ส่วนพารามิเตอร์ตัวแรกที่เป็น ??? นั่นคืออะไรเดี๋ยวเราค่อยมาพูดถึงกันต่อไปนะ
ตัวแปรลึกลับ this
ในภาษาอื่นๆ ที่สร้างขึ้นมาตามหลักการของ OOP เต็มรูปแบบ this จะโผล่มาก็ต่อเมื่อเราทำงานอยู่ใน method หรือใน class เท่านั้น แต่สำหรับ js (เวอร์ชั่นมาตราฐานที่เบราเซอร์ทั่วไปรันได้ตอนนี้คือ ES5 เราจะยังไม่พูดถึง ES6 ในบทความนี้นะ) มันไม่มี class จริงๆ ตัวแปร this จึงไปโผล่ใน function แทน
function dummy(){ console.log(this); }
ดูตัวอย่างโค้ดชุดนี้แล้วลองรันดูสิ ผลที่ได้ถ้าคุณกดรันในเบราเซอร์ธรรมดาคือ [Object Window] หรือสำหรับในบางระบบอาจจะได้ undefined (แต่ไม่ error นะ)
แบบนี้แปลว่าอะไร? แสดงว่าตัวแปร this น่ะมันมีตัวตนถึงแม้เราจะไม่ได้ประกาศตัวแปรชื่อนี้ไว้ตรงไหนเลยยังไงล่ะ (การเรียกใช้ตัวแปรที่ไม่เคยประกาศมาก่อนใน js เมื่อกดรันมันจะแจ้ง error ว่า "ReferenceError: xxx is not defined")
แล้วในเมื่อเราไม่เคยประกาศตัวแปร this มาก่อน แล้วมันไปสร้างเอาตอนไหนน่ะ?
คือตอบคือเมื่อเราประกาศ function ขึ้นมาสักตัวแล้ว js จะแอบเติมพารามิเตอร์ในตำแหน่งแรกให้เราโดยอัตโนมัติ และตัวแปรในพารามิเตอร์แรกที่มันแอบเติมเข้าไปนั่นแหละ มันตั้งชื่อว่า this
งั้นย้อนกลับไปดูฟังก์ชัน add ที่เราสร้างกันไว้ตอนแรกอีกทีซิ
function add(x, y){ return x + y; } //แต่เวลา js มันเอาไปใช้จริงๆ มันจะเห็นแบบนี้ function add(this, x, y){ return x + y; }
ส่วนสำหรับฟังก์ชัน dummy เมื่อก็จะกลายเป็น
function dummy(this){ console.log(this); }
โอเค นั่นเป็นเหตุผลว่าทำไมในฟังก์ชันเราถึงสามารถใช้ตัวแปรชื่อ this ได้นั่นเอง
แต่มันมีอะไรซับซ้อนกว่านั้นนะ
ถ้าทุกอย่างมันจบแค่การเพิ่มตัวแปรพิเศษมาให้ในพารามิเตอร์ตัวแรกสุดก็คงจบ ไม่มีอะไรมากกว่านั้น แต่ปัญหาคือ มันไม่จบแค่นี้น่ะสิ
มาลองคิดอะไรเล่นๆ กันดูหน่อยนะ จากโค้ดฟังก์ชัน dummy ข้างบนน่ะ คิดว่า this อ้างถึงอะไรอยู่ บางคนอาจจะตอบว่ามันก็ต้องอ้างถึง dummy สิ เพราะเป็นฟังก์ชันหลักที่ครอบ block มันอยู่นี่นา? จริงรึเปล่าก็ต้องลองเอาไปรันดูล่ะ ... เป็นไง ไม่ได้สินะ ผลที่ console.log ให้ออกมาดันไม่ใช่ function dummy แต่เป็น Window (หรือบางครั้งอาจจะเจอ undefined)
งั้นลองพิสูจน์อันด้วยตัวอย่างอีกอันนึงกัน เพื่อแสดงให้ดูว่า this ในโค้ดนี้มันไม่ได้อ้างถึงฟังก์ชัน dummy จริงๆ นะ
function dummy(){ console.log(this.x); } dummy.x = 10;
สร้างฟังก์ชัน dummy ขึ้นมาเหมือนเดิมนั่นแหละ แต่ตรง console.log เราสั่งให้มันล็อกค่า this.x ของมันออกมา ขั้นต่อมาเมื่อเราสร้างฟังก์ชันเสร็จ เราก็ทำการเซ็ตค่าให้ dummy เพิ่มด้วยคำสั่ง dummy.x = 10 แปลว่าตอนนี้เจ้า dummy จะมี property เพิ่มมาหนึ่งตัวชื่อว่า x โอเคนะ
Note: ภาษาตระกูลสคริปนั้น โดยปกติแล้วจะสามารถเพิ่ม properties เข้าไปกี่ตัวก็ได้หลังทำการสร้าง object ขึ้นมาแล้ว ใครที่เขียนคลาสเป็นแต่ภาษาพวก type-sensitive เช่น Java ที่ต้องระบุ properties ก่อนใช้งานเท่านั้นอาจจะยังไม่ชิน
เอาล่ะ ไหนลองเอาไปรันก่อนเลยว่า dummy น่ะมีค่า x เก็บไว้แล้วด้วยคำสั่ง
console.log(dummy.x);
จะพบว่าผลที่ได้คือค่า 10 ซึ่งตรงนี้แสดงให้เห็นแล้วนะว่าค่า x ที่เราสั่งไปเมื่อกี้เข้าไปอยู่ใน dummy แล้วจริงๆ แต่ถ้าเราสั่งให้ dummy ด้วยคำสั่ง dummy() ทำงานมันจะแจ้งว่าค่า this.x มีค่าเป็น undefined ก็เลยสรุปได้ว่า
this น่ะมันโผล่มาในฟังก์ชัน (หรือ object)
แต่มันไม่ได้อ้างอิงไปถึงตัวฟังก์ชัน (หรือ object) นั้นหรอกนะ!
เพราะว่า this ในภาษา JavaScript นั้นอ้างอิงถึง context หรือบริบทในขณะนั้นตั้งหากล่ะ
this จะเป็นอะไรขึ้นกับคนเรียกใช้
เรื่องนี้ถือเป็นความแปลกประหลาดของ js ที่ใครไม่คุ้นมักจะงงทุกคนเพราะในภาษาทั่วไป this มักจะอ้างอิงตัว object ของมันเอง คนนอกไม่สามารถไปแทรกแทรงได้ แต่กับ js นั้น this จะเปลี่ยนแปลงตาม context (หรือ บริบท) ที่คนนอกเป็นคนส่งไปให้
var obj = { x: 100, printX: function(){ console.log(this.x); }, printIt: function(){ console.log('it!'); } } //call obj.printX(); //100 obj.printIt(); //it!
ตัวอย่างข้างบนสร้าง object ขึ้นมาหนึ่งตัว มี method 2 ตัวคือ printX กับ printIt .. ง่ายๆ ไม่มีอะไรมากโดยเจ้า printX จะมีการแสดงค่า x ซึ่งเป็น property ของ object ออกมา (เรียกใช้ด้วย this.x) ส่วน printIt นี่ง่ายกว่าคือแสดงค่าคำว่า "it!" ออกมาตรงๆ เลย
อ่ะ ลองเอาไปรันดู ทั้ง printX และ printIt ก็จะแสดงค่า 100 และ "it!" ตามลำดับ อันนี้ไม่น่าแปลกใจอะไรเนอะ (ถ้าแปลกใจลองไปศึกษาเรื่อง object ใน js เพิ่มนะ)
ทีนี้ ... มาลองเปลี่ยนใหม่กันนิดหน่อย โดยการสร้างฟังก์ชันชื่อ caller ขึ้นมา
var obj = { x: 100, printX: function(){ console.log(this.x); }, printIt: function(){ console.log('it!'); } } function caller(fn){ fn(); } //call caller(obj.printX); //undefined caller(obj.printIt); //it!
หน้าที่ของ caller ไม่มีอะไร คือรับพารามิเตอร์ 1 ตัวเป็นฟังก์ชันไปและสั่งให้ฟังก์ชันตัวนั้นทำงานทันที (ใครยังไม่รู้จัก First-Class Function และ High-Order Function อ่านได้ที่นี่)
ต่อมาเราจะเปลี่ยนการเรียกใช้ printX และ printIt โดยการเรียกใช้ผ่าน caller ผลที่ได้น่าจะเหมือนเดิม แต่มันไม่เหมือนล่ะ!
สาเหตุก็คือ เมื่อเราส่ง printX เข้าไปใน caller ในนามของ fn แล้วจึงสั่งให้ fn ทำงาน ลองดูดีๆ จะพบว่ารูปแบบการเรียกใช้งานไม่เหมือนกัน ระหว่าง obj.printX() กับ fn() ... สิ่งที่ต่างกันก็คือการเรียกแบบแรกนั้นทำงานตัวแปร object ส่วนวิธีที่สองเป็นการเรียกใช้ตรงๆโดยไม่ผ่านใครเลย
- การเรียกผ่าน obj.printX() นั้น js มองว่า context ของคำสั่งนี้คือ obj (เพราะเป็นคนที่เรียกให้มันทำงาน) ดังนั้น this ในคำสั่ง console.log(this.x) จึงอ้างอิงถึง obj ได้ถูกต้องอย่างที่มันควรจะเป็น
- แต่การที่เราส่ง obj.printX ผ่านฟังก์ชันในนามของ fn แล้วเรียกใช้โดยสั่งแค่ fn() เฉยๆ นั้นทำให้ js ไม่รู้ว่าตกลง context ของคำสั่งนี้เป็นใคร มันเลยเดาเอาเองว่า window เป็นคนเรียกใช้มันละกันนะ ... this ในคำสั่ง console.log(this.x) จึงอ้างอิงถึงอ๊อบเจ็คของ window แทนซะงั้น และในเมื่อ window ไม่มีตัวแปร x ประกาศมาก่อนเลย ผลที่ได้จึงเป็น undefined ยังไงล่ะ
- ในเคสนี้จะเป็นว่าถ้าเราไม่ได้มีการใช้ this ใน method เลยก็จะรันได้ตามปกติเช่นเดียวกับ printIt
แล้วแก้ยังไงล่ะ
อัญเชิญ context ประทับร่างด้วยคำสั่ง .bind()
ปัญหาของเราเมื่อกี้เกิดขึ้นเมื่อเราส่ง method ผ่านพารามิเตอร์ แต่ context (อ๊อบเจ็คของมัน) ไม่ตามไปด้วยทำให้ this ผิดความหมาย วิธีการแก้คือใช้คำสั่ง .bind ในการแนบ context ติดไปด้วย
caller(obj.printX.bind(obj));
ไม่ได้ส่ง printX ไปเปล่าๆ แต่บอกว่า context ของฟังก์ชันตัวนี้น่ะ คือ obj นะ ทีนี้ถ้าเอาไปรันผลที่ได้ออกมาจะเป็น 100 แล้วล่ะ
ยุทธการ เปลี่ยนขื่อสลับเสา
ถ้าสังเกตรูปแบบการใช้ .bind ดีๆ จะพบว่าพารามิเตอร์ของมันไม่จำเป็นตัวใส่ object ตัวเดียวกันลงไปก็ได้นะ เช่นตัวอย่างที่แล้ว printX เป็น method ของ obj แล้วจะเกิดอะไรขึ้นถ้าเราไม่สั่ง bind มันด้วย obj นะ
เพื่อแสดงให้ดูขอสร้างตัวแปรใหม่ขึ้นมาตัวนึงให้ชื่อว่า faker โดยจะมี property หนึ่งตัวชื่อว่า x (ให้กำหนดค่าเป็น 200 แทนนะ)
var faker = { x: 200 };
จากนั้นแทนที่จะสั่ง bind ด้วย obj เหมือนเดิมก็เปลี่ยนไป bind ด้วย faker แทนซะแบบนี้
caller(obj.printX.bind(faker));
คราวนี้ผลที่ได้คือ 200 แทนซะล่ะ นั่นเพราะ faker กลายมาเป็น context ของโค้ดส่วนนี้แทนซะแล้ว this.x จึงหมายถึง x ของ faker ซึ่งมีค่า 200
จะเห็นว่าการใช้ this ใน js นั้นตอนเขียนน่ะอาจจะไม่ต้องระวังอะไรหรอก แต่ถ้าตอนเรียกใช้เรียกผิดตัวไปล่ะก็เป็นเรื่องได้เลยเพราะมันจะทำให้ context ที่ส่วนใหญ่เราจะชอบนึกว่ามันคือตัว object ที่ทำงานอยู่ตรงนี้เปลี่ยนเป็นอีกคนที่ทำโค้ดระเบิดได้ทันที
ใช้กับ callback pattern ก็ได้นะ
สำหรับโปรแกรมเมอร์สาย JavaScript เชื่อว่าทุกคนเคยเขียนโค้ดในรูปแบบ asynchronous (เช่น Ajax) โดยหน้าตาโค้ดมักจะเป็นอะไรประมาณนี้
var request = { call: function(){ var me = this; ajax(function(res){ me.response(res); }); }, response: function(res){ //..do something.. } }
ในโค้ดนี้มีการเรียกใช้ฟังก์ชัน ajax (ขอติ๊ต่างว่ามันคือตัวแทนของฟังก์ชัน asynchronous ตัวนึงละกัน) แล้วเมื่อผลตอนกลับมาแล้วเราก็ต้องการให้มันเรียก method ที่ชื่อว่า response ให้ทำงาน แต่เราไม่สามารถสั่ง this.response(res) ได้เพราะมันจะหลายเป็นว่าเราอ้าง context ถึง callback function ตัวในแทน ท่าปกติที่ส่วนใหญ่จะใช้กันคือสร้างตัวแปรขึ้นมาข้างนอกก่อน ชื่อมาตราฐานที่ชอบใช้กันก็มี me, self, _this อะไรประมาณนี้ แต่ชื่อไม่สำคัญ ประเด็นคือเราจะใช้มันอ้างถึงตัวแปร this ข้างนอก จะได้เอาไปเรียกใช้ใน callback function ได้ยังไงล่ะ
แต่เมื่อเรารู้จัก bind แล้ว ทำให้เราสามารถเปลี่ยน context ของ callback ตัวนี้ได้แล้ว
var request = { call: function(){ ajax( function(res){ this.response(res); }.bind(this) ); }, response: function(res){ //..do something.. } }
หลังจากสร้าง callback function (จริงๆ ต้องเรียกว่า anonymous function นะ ) ขึ้นมาเราก็จับ this ของฟังก์ชันตอนนอก bind ให้มันไปด้วย .. หลังจากนี้การเรียก this ในฟังก์ชันตัวในจึงจะหมายถึง context ของฟังก์ชันตัวนอกแทน เก็ทม๊ะ หรืองงกว่าเดิม (ฮา)
เตรียมค่าของพารามิเตอร์บางตัวไว้ก่อนด้วย Partial Function
บางครั้งเราเขียนฟังก์ชันขึ้นมาโดยมีพารามิเตอร์บางตัวที่ค่ามันค่อนข้างจะตายตัว แล้วขี้เกียจเรียกค่าเดิมๆ นี้หลายๆ ครั้งเช่น
function taxCalculator(tax, price){ return tax * price; } console.log( taxCalculator(0.07, 100) ); console.log( taxCalculator(0.07, 250) ); console.log( taxCalculator(0.07, 520) );
มีฟังก์ชันสำหรับคิดภาษีอยู่ รับพารามิเตอร์เป็นอัตราภาษีกับราคาของ ทีนี้อัตราภาษีของเราดันกำหนดเป็น 7% ทุกครั้ง .. ทุกครั้งที่เราเรียกใช้เราเลยต้องใส่ค่า 0.07 ลงไปทุกครั้ง ในเคสแบบนี้ถ้าอยากจะละ 0.07 ทิ้งไปเราสามารถเอา bind มาใช้ได้แบบนี้
function taxCalculator(tax, price){ return tax * price; } var tax7 = taxCalculator.bind(null, 0.07); console.log( tax7(100) ); console.log( tax7(250) ); console.log( tax7(520) );
bind เจ้าฟังก์ชัน taxCalculator ด้วยค่า null (จริงๆ จะ bind ด้วยค่าอะไรก็ได้ เพราะในเคสนี้ไม่ได้สน context มัน) หลังจากนั้นให้ลองคิดว่าพารามิเตอร์แรกเป็นพารามิเตอร์ที่เรารู้ค่าอยู่แล้ว ก็ให้ใส่ 0.07 ลงต่อไปเลย ผลที่ได้เอาไปสร้างตัวแปรชื่อ tax7 ซะ
ในตอนนี้ tax7 จะทำหน้าที่เหมือนกับ taxCalculator ทุกอย่างยกเว้นตอนที่เราจะเรียกใช้มัน เราจะสามารถละพารามิเตอร์ตัวแรกไปได้ เพราะตอน bind เรากำหนดให้มันไปแล้ว .. แต่อย่างไรก็ตามนะ พารามิเตอร์ตัวต่อไปที่ยังไม่ได้กำหนดลงไปตอน bind ก็ยังต้องใส่อยู่นะ อย่าลืม
จงทำงาน ณ บัดนี้ ... ด้วย .call() และ .apply()
ตอนที่เราใช้ bind ผลที่ได้ก็แค่การแนบ context ให้ติดไปกับฟังก์ชันด้วยทำนั้น แต่ยังไม่ได้สั่งให้มันทำงานนะ แต่ในบางครั้งเราก็อยากให้มันทำงานเลย js ก็ได้เตรียมคำสั่งชื่อว่า .call ไว้ให้ใช้เรียบร้อยแล้ว
call = bind ที่แนบ context เสร็จก็ทำงานเลย
function printName(){ console.log(this.name); } var people1 = { name: 'John' } var people2 = { name: 'Smith' } printName.call(people1); //John printName.call(people2); //Smith
การสั่งงานจะคล้ายๆ กับ bind ในตัวอย่างก่อนๆ แต่ครั้งนี้ไม่ได้แค่แนบ context ลงไปเท่านั้น มันจะเริ่มการทำงานเลยด้วย
เพื่อความเข้าใจ เอาไปอีกตัวอย่างละกั
function printInfo(gender, age){ console.log(this.name, gender, age); } var people1 = { name: 'John' } var people2 = { name: 'Smith' } printInfo.call(people1, 'm', 40); //John m 40 printInfo.call(people2, 'm', 32); //Smith m 32
ลำดับพารามิเตอร์ของ call กับ bind จะเหมือนกันเลย คือ
- ตัวแรกเป็น object ที่อยากแนบไปเป็น context
- ตัวต่อไปเป็นพารามิเตอร์ที่อยากส่งไปยังฟังก์ชันปลายทาง
แต่จุดอ่อนของ call คือถ้าพารามิเตอร์ที่เราจะส่งไปอยู่ในรูปของ array เราจะไม่สามารถเขียนได้ เช่น
var params = [1, 2, 3, .... ]; fn.call(null, params[0], params[1], params[2], .... );
เขียนไม่ได้ล่ะ เพราะไม่รู้ว่าจะมี params ทั้งหมดกี่ตัว เมื่อเจอแบบนี้เราจะเป็นไปใช้ apply แทน
apply = call ที่รับค่าเป็น array
var params = [10, 20, 30, 40]; function dummy(x, y, z, w){ // x is 10 // y is 20 // z is 30 // w is 40 } dummy.call(null, params[0], params[1], params[2], params[3]); //หรือถ้าเปลี่ยนไปใช้ apply dummy.apply(null, params);
จบแล้ว!
มาสรุปทิ้งท้ายกันนิดนึงละกัน
- this ใน js จะเปลี่ยนค่าไปเรื่อยๆ อ้างอิงจาก context ตอนที่เรียกใช้ซึ่งสามารถเซ็ตได้ด้วยคำสั่ง bind
- ถ้าอยาก bind แล้วทำงานทันทีให้เปลี่ยนไปใช้ call
- ถ้าพารามิเตอร์ที่จะส่งให้ call เป็น array ให้เปลี่ยนไปใช้ apply
- ผลที่ได้จาก .bind คือ function ที่แนบ context แล้ว
- ผลที่ได้จาก .call และ .apply คือ ผลจากการรันฟังก์ชัน
เข้าใจที่มาที่ไปแล้วครับ อธิบายได้เคลียร์คัทหมดจรดดีทีเดียว ขอบคุณครับ