Arrow function ในภาษา JavaScript

ในบทนี้ คุณจะได้เรียนรู้เกี่ยวกับ Arrow function เราจะมาดูอีกวิธีสำหรับการประกาศฟังก์ชันเพื่อใช้งานในภาษา JavaScript นี่เป็นเนื้อหาในบทนี้

  • การประกาศ Arrow function
  • การส่ง Arrow function เป็น Callback
  • คำสั่ง this กับ Arrow function
  • Losing this

การประกาศ Arrow function

Arrow function เป็นการประกาศฟังก์ชัน Expression ในรูปแบบที่สั้นและกระทัดกว่าการประกาศฟังก์ชันในรูปแบบปกติด้วยคำสั่ง function และการทำงานของ Arrow function นั้นไม่มี this, arguments, super หรือ new.target ให้ใช้งาน ซึ่งนี่อาจเป็นประโยชน์ในกรณีที่เราไม่ต้องการใช้มัน เราจะพูดถึงเรื่องนี้ในตอนท้ายของบทนี้

สำหรับการใช้งานทั่วไปของ Arrow function นั้นจะไม่แตกต่างจากการประกาศฟังก์ชันในรูปแบบปกติ นี่เป็นรูปแบบการประกาศ Arrow function ในภาษา JavaScript

(param1, param2, ...) => {
    // Statement
    return value;
}

ในรูปแบบการประกาศ Arrow function นั้นจะเริ่มต้นด้วยการกำหนดพารามิเตอร์ให้กับฟังก์ชันภายในวงเล็บ (...) และภายในวงเล็บปีกกา {...} นั้นเป็นการกำหนดคำสั่งการทำงานของฟังก์ชัน และเราสามารถส่งค่ากลับมาจากฟังก์ชันด้วยคำสั่ง return ถ้าหากมี

เนื่องจาก Arrow function นั้นเป็นฟังก์ชัน Expression ดังนั้นเพื่อที่จะใช้งานมันเหมือนกับฟังก์ชันปกติเราต้องกำหนดมันไว้ในตัวแปร ต่อไปเรามาลองประกาศ Arrow function ในภาษา JavaScript กัน

arrow_function.js
let sayHi = (name) => {
    console.log("Hi " + name);
};

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

sayHi("Metin");
let c = sum(1, 2);
console.log("1 + 2 = " + c);

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

Hi Metin
1 + 2 = 3

เราได้ประกาศสอง Arrow function และกำหนดค่าไว้ในตัวแปร sayHi และ sum นั่นจะทำให้เราสามารถใช้ตัวแปรทั้งสองเป็นฟังก์ชันได้ หรือกล่าวอีกนัยหนึ่ง นี่เป็นอีกวิธีสำหรับการประกาศฟังก์ชันในภาษา JavaScript ในรูปแบบที่สั้นกว่า สังเกตว่า Arrow function จะเหมือนกับฟังก์ชัน Expression แต่ต่างกันเพียงรูปแบบการประกาศ

นอกจากนี้ ถ้าหากจำนวนพารามิเตอร์ของฟังก์ชันมีเพียงหนึ่ง เราสามารถละเว้นวงเล็บ ( ) ได้ และถ้าหากคำสั่งการทำงานของฟังก์ชันมีเพียงหนึ่งคำสั่ง เราสามารถละเว้นวงเล็บปีกกา { } ได้เช่นกัน ยกตัวอย่างเช่น

let sayHi = name => console.log("Hi " + name);
let sum = (a, b) => a + b;

เนื่องจากฟังก์ชัน sayHi นั้นมีเพียงหนึ่งพารามิเตอร์คือ name ดังนั้นเราสามารละเว้นวงเล็บของพารามิเตอร์ได้ และเนื่องจากทั้งสองฟังก์ชันมีคำสั่งการทำงานเพียงคำสั่งเดียว เราสามารถละเว้นวงเล็บสำหรับตัวของฟังก์ชันได้

การส่ง Arrow function เป็น Callback

เนื่องจาก Arrow function นั้นมีรูปแบบการเขียนที่สั้นและกระทัดรัด นั่นทำให้โปรแกรมเมอร์เป็นจำนวนมากชอบใช้มันเพื่อส่งเป็นฟังก์ชัน Callback หรือใช้แทนฟังก์ชัน Expression เนื่องจากมันสะดวกและรวดเร็วกว่าในการเขียน

map_method.js
let names = ["Metin", "Mateo", "Tony", "Chris", "Leo"];
names = names.map((value) => {
    return value.toUpperCase()
});
console.log(names);

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

[ 'METIN', 'MATEO', 'TONY', 'CHRIS', 'LEO' ]

ในตัวอย่างเป็นการใช้เมธอด map ของอาเรย์สำหรับแปลงค่าในอาเรย์เป็นค่าอื่น เมธอดนี้รับพารามิเตอร์เป็นฟังก์ชัน Callback และทำการวนรอบสมาชิกแต่ละตัวในอาเรย์และส่งค่าเข้าไปยังฟังก์ชัน Callback สิ่งที่ส่งกลับมาจากฟังก์ชันจะกลายเป็นค่าใหม่ของอาเรย์ ในกรณีนี้เราเรียกใช้เมธอด toUpperCase เพื่อแปลงข้อความให้เป็นตัวพิมพ์ใหญ่

และนี่เป็นอีกตัวอย่างสำหรับการใช้ Arrow function เพื่อส่งเป็นฟังก์ชัน Callback ของฟังก์ชัน setTimeout เพื่อหน่วงเวลาการทำงานของโปรแกรมให้โค้ดในฟังก์ชันทำงานหลังจากเวลาที่กำหนด

settimeout.js
setTimeout(() => {
    console.log("This message will display");
    console.log("After 1 second");
}, 1000);

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

This message will display
After 1 second

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

คำสั่ง this กับ Arrow function

อย่างที่เราได้บอกไปในตอนต้นว่า Arrow function สำหรับการใช้งานทั่วไปนั้นให้ผลลัพธ์ที่ไม่แตกต่างกับฟังก์ชันในรูปแบบปกติ นั่นคือถ้าเราไม่ต้องการใช้งาน this หรือ super ในฟังก์ชัน แต่ถ้าหากเราต้องการ เราจะต้องใช้การประกาศฟังก์ชันในรูปแบบปกติแทน

ต่อไปมาดูความแตกต่างของการใช้งานระหว่างการประกาศฟังก์ชันในรูปแบบปกติและ Arrow function กับคำสั่ง this

binding_function.js
class User {
    constructor() {
        this.name = "Metin";
    }
}

let sayHi_Normal = function () {
    console.log("Hi " + this.name);
};

let sayHi_Arrow = () => {
    console.log("Hi " + this.name);
};

let user = new User();
let sayHi1 = sayHi_Normal.bind(user);
let sayHi2 = sayHi_Arrow.bind(user);

sayHi1();
sayHi2();

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

Hi Metin
Hi undefined

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

let sayHi_Normal = function () {
    console.log("Hi " + this.name);
};

let sayHi_Arrow = () => {
    console.log("Hi " + this.name);
};

เราประกาศสองฟังก์ชันในรูปแบบของฟังก์ชันปกติและ Arrow function สำหรับแสดงชื่อของออบเจ็คออกทางหน้าจอ สังเกตว่าภายในฟังก์ชันได้มีการเข้าถึง this.name ซึ่ง this เป็นตัวแปรที่อ้างถึงออบเจ็คปัจจุบันที่ฟังก์ชันกำลังทำงานอยู่

let user = new User();
let sayHi1 = sayHi_Normal.bind(user);
let sayHi2 = sayHi_Arrow.bind(user);

จากนั้นสร้างออบเจ็ค user เพื่อที่จะนำไปผูกเข้ากับฟังก์ชันทั้งสองด้วยเมธอด bind นั่นจะทำให้ this ในฟังก์ชันนั้นหมายถึงออบเจ็ค user และเราสามารถใช้มันในการอ้างถึงค่า Property ภายในออบเจ็คได้ ยกตัวอย่างเช่นในคำสั่ง this.name

sayHi1();
sayHi2();

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

user.sayHi1();
user.sayHi2();

แต่การเรียกใช้ในรูปแบบนี้จะต้องประกาศฟังก์ชันไว้ภายในคลาสซึ่งในตัวอย่างนี้เราไม่ได้ทำ

จะเห็นว่าการเรียกใช้งานฟังก์ชัน sayHi2 ที่สร้างมาจาก Arrow function ค่าของ this.name จะเป็น undefined นั่นหมายความว่าเราไม่สามารถผูก this ให้กับฟังก์ชันได้นั่นเอง เนื่องจาก this ของ Arrow function นั้นจะเป็นออบเจ็คในบริษที่ฟังก์ชันกำลังทำงานอยู่เสมอ

Losing this

จากการที่ Arrow function นั้นมีสภาวะที่สูญเสีย this นั่นทำให้ในบางครั้งมันมีประโยชน์และช่วยอำนวยความสะดวกในการเขียนโปรแกรมได้ ในตัวอย่างนี้จะเป็นการนำ Arrow function มาแก้ปัญหาในการเข้าถึง this ของออบเจ็คที่ฟังก์ชันกำลังทำงานอยู่ และนี่เป็นโปรแกรมนับถอยหลังโดยเริ่มจากเวลาที่กำหนดในหน่วยวินาที

timer1.js
class Timer {

    constructor(second) {
        this.second = second;
    }

    run() {
        let id = setInterval(function () {
            console.log(this.second);
            if (this.second == 0) {
                clearInterval(id);
            }
            this.second--;
        }, 1000);
    }
}

let c = new Timer(5);
c.run();

นี่เป็นผลลัพธ์การทำงานของโปรแกรม

undefined
NaN
NaN
NaN
NaN
...

ในตัวอย่าง เป็นโปรแกรมนับถอยหลังโดยเริ่มจาก 5 และลดค่าลงมาจนเหลือ 0 เราคาดหวังให้โปรแกรมทำงานเช่นนั้น แต่เมื่อรันโปรแกรมนี้ การทำงานของมันไม่เป็นไปตามที่เราคาดหวังและมันรันตลอดไป คุณสามารถกด CTRL+C เพื่อให้โปรแกรมหยุดทำงานได้

let id = setInterval(function () {
    console.log(this.second);
    if (this.second == 0) {
        clearInterval(id);
    }
    this.second--;
}, 1000);

เหตุผลที่ทำให้โค้ดนี้ทำงานไม่ได้ก็คือ เราได้กำหนดฟังก์ชัน Callback ให้กับฟังก์ชัน setInterval ในรูปแบบของฟังก์ชันปกติ ซึ่งในภาษา JavaScript นั้นฟังก์ชันสามารถผูกกับ this ซึ่งเป็นบริบทการทำงานของฟังก์ชันนั้นๆ ได้ และในกรณีนี้ this ในฟังก์ชัน Callback นั้นถูกผูกกับ global ออบเจ็คในตอนที่ฟังก์ชันถูกเรียกใช้งานในฟังก์ชัน setInterval

this.second

ดังนั้นเมื่อเราเข้าถึง this.second ภายในฟังก์ชัน Callback ค่าของ second ซึ่งเป็นการอ้างถึงค่าใน global ออบเจ็คนั้นไม่มีอยู่ มันไม่ได้เป็นค่าของออบเจ็ค Timer ที่เราต้องการนั่นเอง

เพื่อเข้าถึง this.second ของออบเจ็ค Timer ภายในฟังก์ชัน Callback นั้นมีหลายวิธีที่เราสามารถทำได้ วิธีแรกคือการเก็บค่า this ไว้ในตัวแปรเพื่อใช้งานภายในฟังก์ชัน

// Change method implementation to
run() {
    let currentThis = this
    let id = setInterval(function () {
        console.log(currentThis.second);
        if (currentThis.second == 0) {
            clearInterval(id);
        }
        currentThis.second--;
    }, 1000);
}

ก่อนเรียกใช้งานฟังก์ชัน setInterval เราเก็บค่า this ของออบเจ็คปัจจุบันไว้ในตัวแปร currentThis ก่อนเพื่อที่จะนำไปใช้ในฟังก์ชัน นี่เป็นวิธีที่ง่ายที่สุดที่เราสามารถทำได้เพื่อป้องกันการสูญเสีย this เมื่อโปรแกรมทำงานในฟังก์ชัน Callback

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

// Change method implementation to
run() {
    let callback = function () {
        console.log(this.second);
        if (this.second == 0) {
            clearInterval(id);
        }
        this.second--;
    };
    let id = setInterval(callback.bind(this), 1000);
}

ในวิธีนี้ เป็นการใช้งานเมธอด bind เพื่อผูกฟังก์ชัน callback เข้ากับ this ของออบเจ็คปัจจุบันซึ่งก็คือ Timer เพื่อใช้เป็นฟังก์ชัน Callback ของฟังก์ชัน setInterval ตอนนี้ไม่สำคัญว่าฟังก์ชัน callback จะถูกนำไปเรียกใช้งานที่ไหน this ของมันจะเป็นของออบเจ็ค Timer เสมอ

สำหรับวิธีสุดท้าย เราสามารถแก้ปัญหานี้ได้โดยการใช้ Arrow function เนื่องจากมันไม่มี this เป็นของมันเอง และนี่เป็นโค้ดสุดท้ายที่สามารถทำงานได้โดยการใช้ Arrow function

timer2.js
class Timer {

    constructor(second) {
        this.second = second;
    }

    run() {
        let id = setInterval(() => {
            console.log(this.second);
            if (this.second == 0) {
                clearInterval(id);
            }
            this.second--;
        }, 1000);
    }
}

let c = new Timer(5);
c.run();

ในตัวอย่าง ทั้งหมดที่เราทำก็คือเปลี่ยนจากการประกาศฟังก์ชัน Callback ด้วยฟังก์ชันรูปแบบปกติเป็น Arrow function และเนื่องจาก Arrow function ไม่มี this เป็นของมันเอง ดังนั้นเมื่อเราเข้าถึง this.second มันเป็นการเข้าถึงตัวแปรของออบเจ็คที่อยู่เหนือกว่า ซึ่งก็คือ Timer นั่นเอง และนี่เป็นผลลัพธ์เมื่อเราลองรันโปรแกรมอีกครั้ง

5
4
3
2
1
0

ตอนนี้ตัวนับถอยหลังสามารถทำงานได้ตามที่ต้องการแล้ว ในตัวอย่างนี้แสดงให้เห็นว่าในบางปัญหา เราสามารถใช้ Arrow function เข้าช่วยได้ ซึ่งจะง่ายและรวดเร็วกว่า อย่างไรก็ตามโปรดจำไว้ว่าถ้าหากคุณยังคงต้องการใช้ this ในฟังก์ชัน คุณก็ยังคงต้องใช้ฟังก์ชันในรูปแบบปกติเช่นเดิม

ในบทนี้ เราได้แนะนำให้คุณรู้จักกับ Arrow function ซึ่งเป็นรูปแบบการประกาศฟังก์ชันที่สั้นและกระทัดรัดกว่าการประกาศฟังก์ชันในรูปแบบปกติ และเราได้พูดถึงการทำงานของ Arrow function กับ this