Metaprogramming คืออะไร? เรียนรู้การใช้งานเบื้องต้นผ่าน JavaScript
ก่อนอื่นเลยต้องแจ้งไว้ก่อนครับว่าเพื่อนๆคนไหนที่ยังไม่สันทัดกับ JavaScript มากนักควรหลีกเลี่ยงบทความนี้ เพื่อนๆคนไหนยังไม่คุ้นเคยกับ ES2015 แนะนำให้อ่าน พื้นฐาน ES2015 สำหรับการเขียน JavaScript สมัยใหม่ ก่อน ส่วนคนไหนพร้อมทั้งกายและใจแล้วจะอ่านบทความนี้มากกว่าสองรอบก็ได้ครับ เราไม่ห้ามดื่มเกินวันละสองขวดแบบลิโพ!
เพราะโปรแกรมเมอร์นั้นขี้เกียจ
บทความของ Babel Coder นั้นมีสามสถานะคือ drafted, published และ upcoming ดังนั้นถ้าเราต้องการสร้างเมธอดเพื่อตรวจสอบสถานะของบทความ เราต้องนิยามสามเมธอดเพื่อแทนแต่ละสถานะดังนี้
1class Article {2 static statuses = ['drafted', 'published', 'upcoming']34 constructor(status) {5 this.status = status6 }78 isDrafted() {9 return this.status === 'drafted'10 }1112 isPublished() {13 return this.status === 'published'14 }1516 isUpcoming() {17 return this.status === 'upcoming'18 }19}2021const article = new Article('drafted')22console.log(article.isDrafted()) // true23console.log(article.isPublished()) // false
จากตัวอย่างข้างบนเราพบว่าเมธอดทั้งสามเป็นพี่น้องร่วมสาบานที่มีหน้าตาเหมือนกัน คือประกอบด้วย is บวกด้วยชื่อสถานะเพียงแต่อักษรตัวแรกเป็นตัวใหญ่ โอ๊ะโอ ทั้งๆที่เราประกาศในบรรทัดที่2แล้วว่าสถานะทั้งหมดของเรามีสามค่า เราใช้ประโยชน์จากส่วนนี้ไม่ได้หรอ แทนที่จะต้องมานั่งประกาศเมธอดใหม่ตั้งสามตัว นี่แหละครับความขี้เกียจของโปรแกรมเมอร์ เอาหละลองพยายามอีกครั้ง
1// สร้างฟังก์ชันขึ้นมาอันนึงมีหน้าที่แปลงข้อความให้ตัวแรกเป็นอักษรตัวใหญ่2// เช่นเปลี่ยนจาก upcoming เป็น Upcoming3const capitalize = (text) => {4 return text.charAt(0).toUpperCase() + text.slice(1)5}67class Article {8 static statuses = ['drafted', 'published', 'upcoming']910 constructor(status) {11 this.status = status12 }13}1415// เมื่อเรามีอาร์เรย์ของสถานะทั้งหลายแล้วจะรอช้าอยู่ใย16// วนลูปรอบมันเพื่อสร้างเมธอดตามจำนวนสถานะที่เรามี17Article.statuses.forEach((status) => {18 // แปะเมธอดที่สร้างใหม่เข้าไปในใน prototype19 // ใครไม่รู้จัก prototype ไม่เป็นไรครับ ลืมมันไปซะ20 // คิดซะว่านี่คือวิธีสร้างเมธอดอีกวิธีหนึ่งใน JavaScript แล้วกัน21 Article.prototype[`is${capitalize(status)}`] = function () {22 return this.status === status23 }24})2526const article = new Article('drafted')27console.log(article.isDrafted()) // true28console.log(article.isPublished()) // false
ตอนนี้โค๊ดของเราดูมีชั้นเชิงมากขึ้น แค่วนลูปชีวิตก็เปลี่ยน ช่างตอบสนองความสันหลังยาวของพวกเราชาวโปรแกรมเมอร์เสียนี่กระไร (หรือมีผมคนเดียวที่ขี้เกียจแฮะ T__T)
Metaprogramming คืออะไร?
ก่อนจะเข้าใจความหมายในส่วนลึก เราลองมาแยกคำว่า meta ออกจาก programming กันก่อนครับ หลังจากคุ้ยวิกิพีเดียพบว่า meta เป็นคำภาษากรีกแปลว่ายิ่งใหญ่ จึงไม่น่าแปลกใจที่ Metaphysics จะหมายถึงอภิปรัชญา ส่วน metaprogramming นั้นจึงน่าจะเป็นการเขียนโปรแกรมที่เวอร์วังอลังการ แล้วแบบไหนหละที่เรียกว่ายิ่งใหญ่สำหรับการเขียนโปรแกรม?
Metaprogramming คือเทคนิคการเขียนโปรแกรมแบบหนึ่งเพื่อสร้างโค๊ดหรือโปรแกรมที่จะไปสร้าง/เปลี่ยนแปลง/วิเคราะห์โค๊ดหรือโปรแกรมขึ้นมาอีกที ลองพิจารณาสถานะของบทความในตัวอย่างที่แล้วครับ ผมเขียนโปรแกรมขึ้นมาชุดนึง แล้วโปรแกรมชุดนี้จะไปสร้างเมธอดขึ้นมาอีกทีนึง จึงจัดเป็น metaprogramming เพราะเป็นการเขียนโปรแกรมเพื่อให้สร้างโปรแกรมขึ้นมาอีกทีหนึ่ง
Metaprogramming ใน JavaScript
Metaprogramming ใน JavaScript นั้นสามารถแบ่งหลักๆได้เป็น3ประเภท ดังนี้
1. Introspection
เตือนเพื่อนๆไว้ก่อนครับว่าอย่าพยายามหาความหมายของคำนี้จาก Google translate เพราะสิ่งที่คุณจะได้คือ วิปัสสนา
หืม? introspection คือความสามารถในการเข้าถึงโค๊ดหรือข้อมูลโดยไม่เปลี่ยนแปลงผลลัพธ์ของโค๊ดต้นฉบับ
Introspection กลุ่มฟังก์ชัน
ฟังก์ชันใน JavaScript นั้นเป็นอ็อบเจ็กต์ ดังนั้นตัวฟังก์ชันเองจึงมีเมธอดให้เราเรียกใช้เพื่อเข้าถึงข้อมูล metadata ของตัวฟังก์ชัน กลุ่มของเมธอดเหล่านี้เองครับที่เป็น introspection
1function print(firstName, lastName) {2 console.log(`${firstName} ${lastName}`)3}45console.log(typeof print) // function67// function นั้นเป็นอ็อบเจ็กต์ชนิดหนึ่ง8console.log(print instanceof Object) // true910// เข้าถึงชื่อของฟังก์ชัน11console.log(print.name) // print1213// เข้าถึงข้อมูลว่าฟังก์ชันดังกล่าวรับ arguments กี่ตัว14console.log(print.length) // 2
Introspection กลุ่มอ็อบเจ็กต์
introspection ในกลุ่มอ็อบเจ็กต์นั้นเราใช้บ่อยมาก โดยเฉพาะในสมัย ES5 ที่เราต้องการสร้าง OOP บน JavaScript ผ่าน prototype ตัวอย่างเช่น
1const obj = {2 firstName: 'Nuttavut',3 lastName: 'thongjor',4}56Object.keys(obj) // ["firstName","lastName"]7Object.values(obj) // ["Nuttavut","thongjor"]8Object.entries(obj) // [["firstName","Nuttavut"],["lastName","thongjor"]]910// ขอข้อมูลของ property ที่ระบุ11Object.getOwnPropertyDescriptor(obj, 'firstName') // {"value":"Nuttavut","writable":true,"enumerable":true,"configurable":true}
Introspection กลุ่ม operators หรือการดำเนินการ
introspection กลุ่มนี้ได้แก่ typeof และ instanceof
1const obj = {}23console.log(typeof obj) // object4console.log(obj instanceof Object) // true
2. Self-modification
Metaprogramming กลุ่มนี้ใช้เพื่อแก้ไขข้อมูลหรือส่วนของโค๊ด ตัวอย่างเช่น delete ที่เป็น operator ดำเนินการสำหรับลบ property ที่ไม่ต้องการ
1// สร้างฟังก์ชันสำหรับย้าย property ทั้งหมดของอ็อบเจ็กต์หนึ่งไปยังอีกตัวหนึ่ง2const moveTo = (source, target) => {3 for (let [key, value] of Object.entries(source)) {4 // ย้าย property จาก source ไป target แล้วลบ property ออกจาก source5 target[key] = value6 delete source[key]7 }8}910const obj1 = {11 firstName: 'Nuttavut',12 lastName: 'Thongjor',13}1415const obj2 = {}1617moveTo(obj1, obj2)1819console.log(obj1) // {}20console.log(obj2) // {"firstName":"Nuttavut","lastName":"Thongjor"}
3. Intercession
Intercession นั้นหมายถึงมือที่สามครับ เมื่อความรักเกิดขึ้นย่อมมีมือที่สามมาแทรกกลางระหว่างสองเรา และมีที่สามนี่แหละที่จะมาปั่นป่วนความรัก เริ่มตั้งแต่ป้ายสีให้อีกฝ่ายดูเลว จากนั้นจึงเปิดศึกเปลี่ยนรักสลับคู่ เห็นไหมครับมือที่สามนั้นช่ำชองในด้านการสร้างความหมายใหม่ให้กับความรักเดิมๆ และนั่นหละครับคือ Intercession
เราสามารถใช้ Intercession เพื่อสร้างความหมายใหม่ให้กับสิ่งที่มีอยู่เดิม เป็นการเพิ่มความสามารถของโค๊ดเก่าด้วยการทำตัวเป็นคนคั่นกลาง เช่นในการสร้างอ็อบเจ็กต์เราสามารถใช้ getter และ setter เพื่อตรวจสอบข้อมูลก่อนตั้งค่าให้ property ได้ดังนี้
1const obj = {2 // โลก JavaScript นิยมใส่ _ นำหน้า property ที่เราคิดในใจว่าเป็น private property3 _name: null,45 get name() {6 // ก่อนเข้าถึง name เราจะตรวจสอบค่า _name ก่อน7 // ถ้า _name ไม่มีค่า จะคืนค่ากลับเป็ย no name8 return this._name ? this._name : 'no name'9 },1011 set name(value) {12 // ถ้าไม่ระบุค่าเข้ามาจะ throw error13 if (!!value) throw 'Please enter your name.'14 this._name = value15 },16}1718obj.name = '' // Please enter your name1920obj.name = 'nut'21console.log(obj.name) // nut
ของเล่นใหม่ ES2015 กับ Metaprogramming
การอุบัติแห่ง ES2015 ที่แม้แต่ภูเขาไฟฟูจิยังต้องสั่นสะเทือนมาพร้อมกับของเล่นใหม่สองสิ่งที่ช่วยเพิ่มความสามารถให้กับ metaprogramming ใน JavaScript น้องสาวคนเล็กชื่อ Symbol
และน้องชายคนรองชื่อ Reflex
และน้องชายคนสุดท้องชื่อ Proxy
เราไปทำความรู้จักกับครอบครัวนี้กัน
Symbol: สัญลักษณ์ที่สื่อสารได้รอบโลก
น้อง symbol นั้นเป็นชนิดข้อมูลใหม่ใน ES2015 ครับ คุณสมบัติที่สำคัญของน้องคือ น้องจะมีความเป็นตัวของตัวเองสูง ดังนั้นทุกครั้งที่เราสร้าง symbol ขึ้นมาใหม่ น้องจะเป็นตัวของตัวเองด้วยการไม่เหมือนกับใครเลย
1const str1 = 'Hello'2const str2 = 'Hello'34// เราสร้าง symbol ด้วยการเรียก Symbol5// ส่วน Hello นั่นเป็นการใส่คำอธิบาย symbol เฉยๆ6// จะใส่หรือไม่ใส่ก็ได้7// แต่แนะนำให้ใส่เสมอครับ เพื่อประโยชน์ในการ debug โปรแกรม8const sym1 = Symbol('hello')9const sym2 = Symbol('hello')1011// string 2 ตัวมีค่าเหมือนกันย่อยเท่ากัน12console.log(str1 === str2) // true1314// รอถึงชาติหน้า symbol 2 ตัวก็ไม่เท่ากัน!15console.log(sym1 === sym2) // false
คนเราต่อให้มีความเป็นตัวเองสูงเพียงใดก็ยังต้องการสังคมใช่ไหมครับ น้อง symbol ก็เช่นกัน เราสามารถสร้าง symbol แล้วนำกลับมาใช้ใหม่ได้ด้วยการเรียกใช้ผ่าน global symbol registry
ดังนี้ฮะ
1const sym1 = Symbol.for('hello')2const sym2 = Symbol.for('hello')34console.log(sym1 === sym2) // true
เนื่องจากรายละเอียดของน้อง symbol นั้นมีค่อนข้างเยอะ ผมจะไม่กล่าวถึงในบทความนี้มากนัก แต่จะมุ่งประเด็นไปที่การใช้ symbol กับ metaprogramming เลยครับ เอาหละพักผ่อนดื่มน้ำปัสสาวะแล้วลุยต่อกันเลย
ES2015 นั้นช่างพิลึกคน มันได้จับกลุ่มของ symbol ชุดหนึ่งที่ได้นิยามเอาไว้แล้วเข้าไปไว้ในตัว array, string และอ็อบเจ็กต์อื่นๆเลย เราเรียก symbol กลุ่มนี้ว่า well known symbols ลองดูตัวอย่างบางตัวจาก symbol กลุ่มนี้กันครับ
Symbol.iterator
เราสามารถใช้ for..of เพื่อวนลูปรอบอาร์เรย์ได้ดังนี้
1const arr = [1, 2, 3]23for (let i of arr) {4 console.log(i)5}67// 1, 2, 3
สมมติตอนนี้เรามีคลาสชื่อ Text ทำหน้าที่เก็บคำทั้งหมดที่ต่อกันเป็นประโยค เช่นถ้าประโยคคือ This is a book
Text จะเก็บ ['This', 'is', 'a', 'book']
โจทย์ของเราคือเราต้องการให้เมื่อไหร่ก็ตามที่เราเรียกใช้อ็อบเจ็กต์ของคลาสนี้ใน for..of ให้มันวนลูปเพื่อพิมพ์ค่าแต่ละคำออกมา เอาหละลงมือ!
1class Text {2 constructor(text) {3 // แตกข้อความยาวๆออกเป็นอาร์เรย์โดยใช้ช่องว่างเป็นตัวหั่น4 this.words = text.split(' ')5 }67 // ส่วนนี้คือ ES2015 Generators ถ้าไม่รู้จักไม่เป็นไรครับ8 // จำรูปแบบการเขียนไปใช้พอ9 *[Symbol.iterator]() {10 for (let word of this.words) {11 yield word12 }13 }14}1516const text = new Text('This is a book')1718// ทีนี้คลาส Text ของเราก็จะนำไปวนลูปได้แล้ว แหล่มแมวฝุดๆ19for (let i of text) {20 console.log(i)21}2223// This24// is25// a26// book
Symbol.match
ก่อนอื่นเราไปทบทวนกันหน่อยดีกว่าว่าใน String#match ของ JavaScript ดั้งเดิมนั้นทำงานยังไง
1const str1 = 'hello world'2const regex = /^hello/34// match ใช้เพื่อตรวจสอบว่า string นั้นมีรูปแบบตรงกับ regular expression5// ที่เราระบุไว้หรือไม่ สังเกตสิ่งที่มันคืนค่ากลับมานะครับ6console.log(str1.match(regex)) // ['hello']
เรามีคลาส Word สำหรับใช้เก็บคำที่อาจมี article(a, an, the) ประกอบอยู่ด้วย โจทย์ของเราคือเราต้องการสร้างเมธอด match
เพื่อเปรียบเทียบ string กับคลาส Word ของเรา โดยที่ถ้า string นั้นเหมือนกับ Word ของเราตอนที่ไม่มี article จะถือว่าเข้าคู่กัน เช่นถ้าเราเปรียบเทียบ book
กับอ็อบเจ็กต์ของคลาส Word ที่เก็บค่า a book
ไว้จะถือว่าเท่ากัน เพราะเราไม่คำนึงถึง article
1class Word {2 constructor(word) {3 this.word = word4 }56 [Symbol.match](string) {7 const index = this.word8 .replace(/^an /, '')9 .replace(/^a /, '')10 .replace(/^the /, '')11 .indexOf(string)1213 // หลังจากตัด article ออกแล้วถ้าไม่เท่ากันให้คืนค่าเป็น -114 // แต่ถ้าเท่ากันให้คืนค่าเป็นอาร์เรย์แบบเดียวกับที่ String#match ทำ15 return index === -1 ? null : [this.word]16 }17}1819const word = new Word('a book')20console.log('book'.match(word)) // ['a book']
Reflect
ES2015 ได้เพิ่ม Reflect เป็นอ็อบเจ็กต์อีกประเภทหนึ่งในตัวภาษา มีหลายเมธอดภายในคล้ายๆเมธอดของอ็อบเจ็กต์ เราจะมาลองเรียนรู้สามเมธอดต่อไปนี้ของ Reflect กันดังนี้
1const obj = {2 name: null,3 age: null,4}56// ใช้เพื่อตั้งค่า property ในอ็อบเจ็กต์7Reflect.set(obj, 'name', 'Nuttavut Thongjor')89// ใช้เพื่อดึงค่า property ในอ็อบเจ็กต์10Reflect.get(obj, 'name') // Nuttavut Thongjor1112// ใช้เพื่อตรวจสอบว่ามี property นั้นในอ็อบเจ็กต์หรือไม่13Reflect.has(obj, 'age') // true
Proxy
เรื่องสุดท้ายละครับที่ผมจะกล่าวถึงในบทความนี้นั่นคือ Proxy
Proxy นั้นเป็นความสามารถอย่างหนึ่งที่จะทำการห่อหุ่มอ็อบเจ็กต์เอาไว้ จากนั้นจะทำตัวเป็นมือที่สามคั่นกลางระหว่างเราผ่านสิ่งที่เรียกว่า trap
1// อ็อบเจ็กต์ตั้งต้นที่เราต้องการเพิ่มความสามารถให้มันด้วยการใช้ Proxy2const target = {}34// handler เป็นอ็อบเจ็กต์ที่นิยามวิธีเพิ่มความสามารถให้อ็อบเจ็กต์ target5// ผ่านเมธอดที่เรียกว่า trap (จะได้พูดถึงต่อไป)6const handler = {}78// วิธีสร้าง Proxy แสนง่ายแค่ส่ง target และ handler เข้าไป9const proxy = new Proxy(target, handler)
trap นั้นคือเมธอดที่ JavaScript มีให้เรานำไปใช้ใน handler ตัวอย่างของ trap เช่น get และ set เราจะมาลองใช้ get เพื่อ log ค่าของ property เมื่อเราเข้าถึงกัน
1const target = {}2const handler = {3 // get นี่ละครับคือ trap4 // get ตัวนี้รับพารามิเตอร์3ตัว5 // เราต้องการ log ชือของ property จึงเข้าถึง name6 get: (target, name, receiver) => {7 console.log(`Getting ${name}`)8 },9}1011const book = new Proxy(target, handler)12book.title = 'Introduction to ES2015'13book.title // Getting title
ลองดูตัวอย่างสุดคลาสสิกของการใช้ Proxy กันครับ นั่นคือการใช้ Proxy เพื่อตรวจสอบข้อมูล
1const validator = {2 // ใช้ trap ชื่อ set3 // เพราะเราต้องการเช็คข้อมูลเมื่อทำการตั้งค่าตัวแปร4 set(object, property, value) {5 switch (property) {6 case 'name':7 // ถ้า name ไม่มีค่าให้ error8 if (value) break9 else throw new TypeError("Can't be null")10 case 'age':11 // ถ้าอายุไม่เป้นตัวเลขจำนวนเต็มให้ error12 if (Number.isInteger(value)) break13 else throw new TypeError('Must be integer')14 }1516 object[property] = value1718 // ถ้าสำเร็จต้อง return true กลับออกไปด้วย19 return true20 },21}2223const person = new Proxy({}, validator)24person.name = '' // Can't be null25person.age = NaN // Must be integer
จบแล้วครับ ต้องออกตัวก่อนเลยว่าเรื่องของ metaprogramming ใน JavaScript ยังมีอีกเยอะมากและนำไปประยุกต์ต่อได้ในหลายๆทาง หวังว่าบทความชิ้นนี้จะช่วยเบิกทางให้เพื่อนๆอ่านเอกสารในเรื่อง metaprogramming อื่นๆได้เข้าใจมากขึ้นครับ
เอกสารอ้างอิง
getify. Chapter 7: Meta Programming. Retrieved May, 27, 2016, from https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20%26%20beyond/ch7.md
Dr. Axel Rauschmayer (2015). Classes in ECMAScript 6 (final semantics). Retrieved May, 27, 2016, from http://www.2ality.com/2015/02/es6-classes-final.html
Addy Osmani. Introducing ES2015 Proxies. Retrieved May, 27, 2016, from https://developers.google.com/web/updates/2016/02/es2015-proxies
Keith Cirkel (2015). Metaprogramming in ES6: Symbols and why they're awesome. Retrieved May, 27, 2016, from http://blog.keithcirkel.co.uk/metaprogramming-in-es6-symbols/
Keith Cirkel (2015). Metaprogramming in ES6: Part 2 - Reflect. Retrieved May, 27, 2016, from http://blog.keithcirkel.co.uk/metaprogramming-in-es6-part-2-reflect/
Dr. Axel Rauschmayer. Metaprogramming with proxies. Retrieved May, 27, 2016, from http://exploringjs.com/es6/ch_proxies.html
Ravi Kiran (2016). Meta Programming in ES6 using Symbols. Retrieved May, 27, 2016, from http://www.dotnetcurry.com/javascript/1265/metaprogramming-javascript-using-es6-symbols
Dr. Axel Rauschmayer (2011). Reflection and meta-programming in JavaScript. Retrieved May, 27, 2016, from http://www.2ality.com/2011/01/reflection-and-meta-programming-in.html
สารบัญ
- เพราะโปรแกรมเมอร์นั้นขี้เกียจ
- Metaprogramming คืออะไร?
- Metaprogramming ใน JavaScript
- ของเล่นใหม่ ES2015 กับ Metaprogramming
- Symbol: สัญลักษณ์ที่สื่อสารได้รอบโลก
- Reflect
- Proxy
- เอกสารอ้างอิง