โพรโทไทป์
หากคอนสตรักเตอร์เปรียบเสมือนแม่พิมพ์กุญแจ
อินสแตนซ์ก็เปรียบเหมือนกุญแจที่สร้างจากแม่พิมพ์นี้
แต่ว่าปกติการจะสร้างแม่พิมพ์นั้นขึ้นมาได้ จะต้องมีตัวต้นแบบก่อน
แม่พิมพ์กุญแจต้องถูกสร้างขึ้นมาจากกุญแจที่เป็นต้นแบบ
ตัวต้นแบบนั้นเรียกว่า
"โพรโทไทป์" (prototype)
ในตอนที่เรานิยามคอนสตรักเตอร์ขึ้นมานั้น จริงๆแล้วเป็นการสร้างโพรโทไทป์ขึ้น
แล้วเราก็ได้คอนสตรักเตอร์ตามมาด้วยพร้อมกัน
โพรโทไทป์ของคอนสตรักเตอร์นั้นสามารถเข้าถึงได้โดยผ่านพรอเพอร์ตี prototype
ลองมาดูกัน
function Cons() {}
var proto = Cons.prototype;
alert(proto); // ได้ [object Object]
alert(proto.constructor); // ได้ function Cons() {}
alert(proto instanceof Cons); // ได้ false
alert(new proto.constructor() instanceof Cons); // ได้ true
จะเห็นว่าโพรโทไทป์ก็เป็นออบเจ็กต์ตัวหนึ่ง
และถ้าดูว่าคอนสตรักเตอร์ของมันคืออะไรก็จะได้ว่าเป็นตัวคอนสตรักเตอร์ตัวนั้นเอง
แต่พอดูว่าเป็นอินสแตนซ์ของคอนสตรักเตอร์นั้นหรือไม่ก็จะได้คำตอบว่าไม่ใช่
โพรโทไทป์ก็เป็นออบเจ็กต์เหมือนกับอินสแตนซ์ แต่ไม่ใช่อินสแตนซ์
มีฐานะที่พิเศษกว่านั้น
พรอเพอร์ตีใดๆที่กำหนดลงไปให้กับโพรโทไทป์จะไปปรากฏในทุกอินสแตนซ์
แม้ว่าอินสแตนซ์นั้นจะถูกสร้างขึ้นมาก่อนที่จะกำหนดพรอเพอร์ตีนั้นขึ้นมา
แต่ว่าในทางกลับกัน พรอเพอร์ตีที่ใส่ให้กับอินสแตนซ์จะไม่ปรากฏในโพรโทไทป์
function Cons() {
this.prop1 = "อิจิ";
}
var proto = Cons.prototype;
proto.prop2 = "นิ";
var inst = new Cons();
proto.prop3 = "ซัง";
alert(inst.prop1); // ได้ อิจิ
alert(inst.prop2); // ได้ นิ
alert(inst.prop3); // ได้ ซัง
inst.prop4 = "ชิ";
alert(proto.prop1); // ได้ undefined
alert(proto.prop4); // ได้ undefined
ในที่นี้ prop1
เป็นพรอเพอร์ตีที่ถูกป้อนให้อินสแตนซ์ตอนสร้างอินสแตนซ์ขึ้นจากคอนสตรักเตอร์
ดังนั้นไม่มีอยู่ในตัวโพรโทไทป์
พรอเพอร์ตีที่จะมีอยู่ในโพรโทไทป์คือพรอเพอร์ตีที่ป้อนให้โพรโทไทป์โดยตรงเท่านั้น
ดังนั้นหากมีพรอเพอร์อะไรที่ต้องการให้ทุกอินสแตนซ์มีเหมือนกันหมดก็ให้กำหนดที่ตัวโพรโทไทป์
เมื่อพรอเพอร์ตีในอินสแตนซ์ไปซ้ำกับในโพรโทไทป์
กรณีที่มีการป้อนพรอเพอร์ตีให้อินสแตนซ์ด้วยชื่อที่เหมือนกับพรอเพอร์ที่ที่กำหนดในโพรโทไทป์
พรอเพอร์ตีนั้นในอินสแตนซ์นั้นก็จะถูกแทนด้วยค่าที่ป้อนเข้าไปใหม่
แต่จะเป็นแค่ในอินสแตนซ์ตัวนั้นเท่านั้น
ไม่ส่งผลต่ออินสแตนซ์ตัวอื่นและตัวโพรโทไทป์
อีกทั้ง
หลังจากที่อินสแตนซ์นั้นได้ป้อนพรอเพอร์ตีทับตัวที่มีอยู่ในโพรโทไทป์ไปแล้ว
ต่อให้มีการแก้ค่านั้นในโพรโทไทป์ก็จะไม่ส่งผลต่ออินสแตนซ์ตัวนั้นอีก
แต่จะยังส่งผลต่ออินสแตนซ์ตัวอื่น
ตัวอย่าง
function Cons() {}
var proto = Cons.prototype;
proto.prop = "เกาลัด";
var inst1 = new Cons();
var inst2 = new Cons();
inst1.prop = "ลูกพลับ";
alert(proto.prop); // ได้ เกาลัด
alert(inst1.prop); // ได้ ลูกพลับ
alert(inst2.prop); // ได้ เกาลัด
proto.prop = "เห็ด";
alert(proto.prop); // ได้ เห็ด
alert(inst1.prop); // ได้ ลูกพลับ
alert(inst2.prop); // ได้ เห็ด
ในที่นี้ .prop ถูกป้อนให้กับ inst1 ดังนั้น .prop ของ inst1 ก็จะเป็น .prop
ตัวนี้ ไม่เกี่ยวอะไรกับ .prop ในโพรโทไทป์อีก
แต่ inst2 ไม่มีการป้อน .prop ใหม่ให้ ดังนั้นจึงเป็นค่าเดียวกับในโพรโทไทป์
เปลี่ยนตามกันไปตลอด
เพียงแต่หากลบพรอเพอร์ตีตัวนั้นทิ้งไปจากอินสแตนซ์
พรอเพอร์ตีตัวนั้นก็จะกลับมาเป็นค่าในโพรโทไทป์ได้อีก
delete inst1.prop;
alert(inst1.prop); // ได้ เห็ด
จากตรงนี้สรุปได้ว่า เวลาที่เราเข้าถึงพรอเพอร์ตีในอินสแตนซ์
โปรแกรมจะดูว่ามีการกำหนดพรอเพอร์ตีนี้ไว้ในตัวอินสแตนซ์เองหรือเปล่า
ถ้ามีก็ใช้ค่านี้เลย แต่ถ้าไม่มีจึงจะไปหาดูในโพรโทไทป์
การตรจสอบว่าเป็นพรอเพอร์ตีในตัวอินแสตนซ์เองหรือในโพรโทไทป์
อินสแตนซ์จะสามารถใช้พรอเพอร์ตีจากในโพรโทไทป์ได้จนดูเผินๆก็อาจมองว่าเหมือนกัน
แต่เมื่อต้องการแยกก็สามารถทำได้โดยใช้เมธอด hasOwnProperty
โดยใส่ชื่อพรอเพอร์ตีลงไป
เมธอดนี้จะให้ค่า true เมื่อใส่ชื่อที่เป็นพรอเพอร์ตีในตัวอินสแตนซ์เองเท่านั้น
ถ้าใส่พรอเพอร์ที่ที่อยู่ในโพรโทไทป์
หรือเป็นพรอเพอร์ตีติดตัวที่มีในทุกออบเจ็กต์อยู่แล้วก็จะได้ false
ตัวอย่าง
function Constr() {
this.propa = 11; // พรอเพอร์ตีตั้งต้นใส่ในตัวอินแสตนซ์
}
Constr.prototype.propi = 33; // สร้าง propi เป็นพรอเพอร์ตีในโพรโทป์
var insta = new Constr();
insta.prope = 22; // พรอเพอร์ตีเพิ่มเติมเฉพาะของ insta
alert(insta.hasOwnProperty("propa")); // ได้ true
alert(insta.hasOwnProperty("prope")); // ได้ true
alert(insta.hasOwnProperty("propi")); // ได้ false
insta.propi = 44; // ทับ propi กลายเป็นพรอเพอร์ตีในตัว insta
alert(insta.hasOwnProperty("propi")); // ได้ true
delete insta.propi; // ลบ propi ในตัว insta ทำให้ propi กลับไปป็นพรอเพอร์ตีในโพรโทไทป์
alert(insta.hasOwnProperty("propi")); // ได้ false
alert(insta.hasOwnProperty("hasOwnProperty")); // ได้ false
สร้างเมธอดให้โพรโทไทป์
ด้วยความที่พรอเพอร์ตีที่ป้อนให้โพรโทไทป์จะไปปรากฏในอินสแตนซ์ทั้งหมด
ดังนั้นหากต้องการสร้างเมธอดจึงมักกำหนดที่ตัวโพรโทไทป์
หากลองสร้างคอนสตรักเตอร์ Phanakngan เช่นเดียวกับในตัวอย่างที่ยกไปในบทที่แล้ว
แต่เปลี่ยนเป็นกำหนดเมธอดลงในโพรโทไทป์จะเขียนได้แบบนี้
function Phanakngan(chue, namsakun) {
this.chue = chue;
this.namsakun = namsakun;
}
Phanakngan.prototype.naenamtua = function() {
alert("ฉันชื่อ" + this.namsakun + " " + this.chue);
};
var hajime = new Phanakngan("ฮาจิเมะ", "ชิโนดะ");
hajime.naenamtua(); // ได้ ฉันชื่อชิโนดะ ฮาจิเมะ
(
ชิโนดะ ฮาจิเมะ)
เมธอดจะถูกใส่ลงที่ .prototype แทนที่จะใส่ที่อินสแตนซ์โดยตรง
แบบนี้จะทำให้ประหยัดพื้นที่หน่วยความจำ
นอกจากนี้ โดยทั่วไปหากมีเมธอดอยู่หลายตัว
มักจะเขียนในรูปแบบการสร้างออบเจ็กต์ใหม่ใส่ทับลงไปในโพรโทไทป์ คือเขียนแบบนี้
function Phanakngan(chue, namsakun) {
this.chue = chue;
this.namsakun = namsakun;
}
Phanakngan.prototype = {
// เมธอดแนะนำตัว
naenamtua: function() {
alert("ฉันชื่อ" + this.namsakun + " " + this.chue);
},
// เมธอดทักทาย
thakthai: function(x) {
alert("สวัสดี คุณ" + x.namsakun);
}
};
var umiko = new Phanakngan("อุมิโกะ", "อาฮางง");
var shizuku = new Phanakngan("ชิซึกุ", "ฮาซึกิ");
umiko.naenamtua(); // ได้ ฉันชื่ออาฮางง อุมิโกะ
umiko.thakthai(shizuku); // ได้ สวัสดี คุณฮาซึกิ
(
อาฮางง กับ ฮาซึกิ)
ในตัวอย่างนี้เขียนเมธอดใส่ลงไปพร้อมกันสองตัว พอเขียนแบบนี้จะสั้นลง
และดูเป็นระเบียบกว่า โดยเฉพาะยิ่งถ้าเมธอดเยอะกว่านี้มากเข้า
เพราะทำให้ไม่ต้องมาเขียน .prototype ซ้ำๆหลายรอบ
การใส่เมธอดให้โพรโทไทป์ของข้อมูลชนิดที่มีอยู่แล้ว
ไม่ใช่แค่ข้อมูลชนิดออบเจ็กต์เท่านั้นที่มีโพรโทไทป์ ข้อมูลชนิดสายอักขระ, ตัวเลข
และ บูล ก็มีทั้งโพรโทไทป์ละคอนสตรักเตอร์
และโพรโทไทป์เหล่านี้ก็สามารถถูกแก้ได้เช่นกัน
เพื่อเพิ่มหรือปรับความสามารถตามที่คนเขียนโปรแกรมต้องการ
สามารถแก้โพรโทไทป์ได้โดยผ่านพรอเพอร์ตี .prototype ของคอนสตรักเตอร์
ตัวอย่างเช่นต้องการเพิ่มเมธอดให้ตัวเลขทุกตัว ก็ใส่เมธอดลงไปที่
Number.prototype
Number.prototype.pow = function(x) {
return Math.pow(this, x);
};
alert((2).pow(4)); // ได้ 16
หรือสามารถลองใส่เมธอดสำหรับเรียงกลับด้านอักษรจากหลังมาหน้า
String.prototype.riangklaplang = function() {
return this.split("").reverse().join("");
};
alert("กอด".riangklaplang()); // ได้ ดอก
หากใส่เมธอดให้กับออบเจ็กต์
จะสามารถเรียกใช้ได้จากข้อมูลทุกชนิดแม้แต่ข้อมูลชนิดตัวเลข สายอักขระ และบูลด้วย
เพราะทุกสิ่งทุกอย่างถือเป็นออบเจ็กต์
Object.prototype.alertInstance = function() {
alert(this + " เป็นอินสแตนซ์ของ " + /\S+\(\)/.exec(this.constructor));
};
true.alertInstance(); // ได้ true เป็นอินสแตนซ์ของ Boolean()
(1).alertInstance(); // ได้ 1 เป็นอินสแตนซ์ของ Number()
"xxx".alertInstance(); // ได้ xxx เป็นอินสแตนซ์ของ String()
({}.alertInstance()); // ได้ [object Object] เป็นอินสแตนซ์ของ Object()
[1, 2, 3].alertInstance(); // ได้ 1,2,3 เป็นอินสแตนซ์ของ Array()
new Date("2019-09-01").alertInstance(); // ได้ Sat Sep 01 2019 07:00:00 GMT+0700 เป็นอินสแตนซ์ของ Date()
เปรียบเทียบโพรโทไทป์กับคลาสในภาษาอื่น
ในภาษาที่มีเป็นเชิงวัตถุนั้นโดยทั่วไปออบเจ็กต์จะถูกแบ่งออกเป็นชนิดโดยชนิดนี้จะเรียกว่า
"คลาส" (class)โดยคลาสเป็นตัวกำหนดว่าออบเจ็กต์จะมีคุณสมบัติอย่างไร มีหน้าที่ทำอะไรได้บ้าง
โพรโทไทป์ในจาวาสคริปต์นั้น อาจเทียบได้กับสิ่งที่เรียกว่าคลาส ในภาษาอื่น
จาวาสคริปต์นั้นเป็นภาษาที่มีแนวคิดเชิงวัตถุแบบยืนพื้นบนโพรโทไทป์
(prototype-based) ต่างจากภาษาอื่นส่วนใหญ่ที่เป็นแบบยืนพื้นบนคลาส
(class-based)
ข้อแตกต่างคือ ในแบบโพรโทไทป์นั้นเราจะกำหนดคอนสตรักเตอร์และโพรโทไทป์ขึ้น
ซึ่งเป็นแค่การกำหนดรูปแบบของออบเจ็กต์แบบหลวมๆเท่านั้น
ออบเจ็กต์แต่ละตัวจะเหมือนถูกสร้างโดยการโคลนมาจากตัวต้นแบบตัวเดียวกัน
แต่หลังจากนั้นอาจจะเปลี่ยนแปลงไปอีกแค่ไหนก็ได้ ไม่ได้มีอะไรมาผูกมัดมากนัก
คอนสตรักเตอร์และโพรโทไทป์เป็นแค่ตัวกำหนดว่าให้ออบเจ็กต์นี้เริ่มต้นมามีลักษณะเป็นแบบนี้
แต่สุดท้ายออบเจ็กต์นั้นสามารถเพิ่มพรอเพอร์ตีหรือเมธอดใหม่ขึ้นมา
โดยสิ่งที่เพิ่มมาก็อาจถูกใส่ให้แค่กับออบเจ็กต์ตัวนั้นเท่านั้นโดยไม่ได้มีผลอะไรกับออบเจ็กต์ตัวอื่นที่สร้างมาจากคอนสตรักเตอร์เดียวกัน
แต่สำหรับคลาสในภาษาโปรแกรมอื่นๆส่วนใหญ่ไม่ใช่แบบนั้น
คลาสนั้นกำหนดขอบเขตข้อจำกัดให้กับออบเจ็กต์ในแต่ละคลาสมากกว่า
รายละเอียดของข้อผูกมัดของคลาสนั้นอาจแตกต่างกันไปในแต่ละภาษา
ดังนั้นที่จริงแล้วความแตกต่างนี้ก็มีความคลุมเครือ
ดังนั้นจะเรียกว่าโพรโทไทป์และคอนสตรักเตอร์ในจาวาสคริปต์คือคลาสก็ได้ ไม่ผิด
แต่โดยรวมแล้วคือ ออบเจ็กต์ในจาวาสคริปต์นั้น
มีอิสระในการเปลี่ยนแปลงมากกว่าออบเจ็กต์ในภาษาอื่นที่ยืนพื้นบนคลาส
ใน ES6 มีวิธีการนิยามโพรโทไทป์ในแบบใหม่
ซึ่งใกล้เคียงกับการสร้างคลาสแบบภาษาอื่นมากขึ้น
ทำให้คนที่คุ้นเคยกับภาษาอื่นมาก่อนเข้าใจได้ง่ายขึ้น
แต่เนื้อในก็คือเป็นการไปสร้างโพรโทไทป์ เหมือนกับที่อธิบายไปในบทนี้
วิธีการนั้นจะไปเขียนถึงในส่วนที่อธิบายเนื้อหา ES6
ในบทต่อจากนี้ไป เพื่อความง่ายก็จะใช้คำว่า "คลาส"
เพื่อเรียกรวมๆระหว่างคอนสตรักเตอร์และโพรโทไทป์
แต่ขอให้เข้าใจว่าสำหรับในจาวาสคริปต์แล้ว
คลาสก็คือสิ่งที่ประกอบไปด้วยคอนสตรักเตอร์กับโพรโทไทป์