φυβλαςのβλογ
phyblasのブログ



javascript เบื้องต้น บทที่ ๒๒: การรับทอด
เขียนเมื่อ 2019/07/31 23:36
แก้ไขล่าสุด 2021/09/28 16:42


การรับทอด

ในภาษาที่มีแนวคิดเชิงวัตถุนั้น โดยทั่วไปจะมีสิ่งที่เรียกว่า "การรับทอด" (inheritance)

คือการกำหนดออบเจ็กต์คลาสใหม่โดยเอาคุณสมบัติต่างๆจากออบเจ็กต์คลาสที่มีอยู่เดิมก่อนแล้ว ไม่ต้องสร้างคลาสใหม่ตั้งแต่ต้นทั้งหมด

ในจาวาสคริปต์นั้นก็มีแนวคิดเรื่องการรับทอดอยู่ แต่ก็มีลักษณะเฉพาะตัวที่แปลกออกไปสักหน่อย การรับทอดไม่ได้เกิดที่ตัวคลาส แต่เกิดที่ตัวอินสแตนซ์ของคลาส

ขออธิบายวิธีการรับทอดด้วยการยกตัวอย่าง
function Pokemon() {} // สร้างคลาส Pokemon
Pokemon.prototype.rong = function() {
  alert(this.siang);
};

function Pikachu() {} // สร้างคลาส Pikachu
Pikachu.prototype = new Pokemon(); // ให้คลาส Pikachu รับทอดคลาส Pokemon
Pikachu.prototype.siang = "พีคา พีคา"; // ป้อนพรอเพอร์ตีใหม่ให้คลาส Pikachu

pika = new Pikachu();
pika.rong(); // ได้ พีคา พีคา

ตรง Pikachu.prototype = new Pokemon() นี้เองที่เป็นการรับทอด

ซึ่งการใช้ new ก็คือการสร้างอินสแตนซ์ขึ้น

จะเห็นได้ว่าที่จริงการรับทอดในจาวาสคริปต์นั้นก็คือ การเอาอินสแตนซ์ของอีกคลาสมาเป็นโพรโทไทป์นั่นเอง

จากนั้นก็สามารถเพิ่มพรอเพอร์ตีใหม่ลงไปให้กับคลาส Pikachu ในที่นี้คือเพิ่ม .siang (เสียง) ลงไป

ดังนั้นอินสแตนซ์ของคลาส Pikachu จะมีทั้ง .siang ซึ่งเป็นพรอเพอร์ตีเฉพาะของคลาสนี้เอง และมี .rong ซึ่งเป็นเป็นเมธอดที่รับทอดมา

ดังนั้นจึงสามารถเรียกใช้เมธอด rong ได้ และเมธอดนี้จะไปหาพรอเพอร์ตี siang ในตัวออบเจ็กต์ แล้วก็แสดงผลออกมาเป็นการส่งเสียงร้อง

แต่ว่าหากสร้างอินสแตนซ์ขึ้นจากคลาส Pokemon จะไม่มีพรอเพอร์ตี siang มีแค่เมธอด rong แบบนี้เมื่อเรียกเมธอด rong จะหาพรอเพอร์ตี siang ไม่เจอ จึงได้ undefined
poke = new Pokemon();
poke.rong(); // ได้ undefined

อินสแตนซ์ของคลาสใหม่นั้นถือว่าเป็นอินสแตนซ์ของคลาสของออบเจ็กต์ที่รับทอดมาด้วย

ดังนั้นหากใช้ instanceof จะพบว่า pika ก็จะได้ true ทั้งกับ Pikachu และ Pokemon แต่ poke จะได้ true แค่กับ Pokemon
alert(pika instanceof Pikachu); // ได้ true
alert(pika instanceof Pokemon); // ได้ true
alert(poke instanceof Pikachu); // ได้ false
alert(poke instanceof Pokemon); // ได้ true

จะเห็นว่าความสัมพันธ์จากการรับทอดนั้นได้ถูกสร้างขึ้นแล้ว และถ้าหากมีการสร้างคลาสใหม่ที่รับทอดจาก Pikachu แบบนั้นอินสแตนซ์ของคลาสนั้นก็จะเป็นอินสแตนซ์ของคลาส Pokemon ด้วยเช่นกัน



ห่วงโซ่โพรโทไทป์

ความสัมพันธ์จากการรับทอดในจาวาสคริปต์ในลักษณะที่กล่าวมานี้เรียกว่าห่วงโซ่โพรโทไทป์ (prototype chain)

ห่วงโซ่นี้จะโยงจากอินสแตนซ์ของคลาสที่รับทอดมาไปยังโพรโทไทป์ แล้วก็โยงไปเรื่อยๆ

และแม้แต่คลาสที่ไม่ได้กำหนดโพรโทไทป์ให้ไปรับทอดมาจากอินสแตนซ์ของคลาสไหนก็ตาม ความจริงแล้วก็มีการรับทอดจากโพรโทไทป์ของ Object มาโดยอัตโนมัติ เพราะออบเจ็กต์ทุกชนิดคือออบเจ็กต์ รับพรอเพอร์ตีและเมธอดพื้นบานของออบเจ็กต์มา เช่น .constructor, .hasOwnProperty .toString พวกนี้เป็นพรอเพอร์ตีและเมธอดที่มีอยู่แล้วตั้งแต่แรก ไม่ได้ไปนิยามเพิ่มให้

ดังนั้นปลายทางของห่วงโซ่โพรโทไทป์จะอยู่ที่ Object เสมอ

ถ้าใช้ instanceof กับคอนสตรักเตอร์ที่อยู่ปลายทางของห่วงโซ่โพรโทไทป์จะได้ true ทั้งหมด

ตัวอย่าง
function Fushigidane() {}
function Fushigisou() {}
Fushigisou.prototype = new Fushigidane();
function Fushigibana() {}
Fushigibana.prototype = new Fushigisou();

var fujii = new Fushigibana();
alert(fujii instanceof Fushigibana); // ได้ true
alert(fujii instanceof Fushigisou); // ได้ true
alert(fujii instanceof Fushigidane); // ได้ true
alert(fujii instanceof Object); // ได้ true

แบบนี้อินสแตนซ์ของ Fushigidane เป็นโพรโทไทป์ของ Fushigisou

ดังนั้นอินสแตนซ์ของ Fushigisou ก็จะเป็นอินสแตนซ์ของ Fushigidane ด้วย

และอินสแตนซ์ของ Fushigisou เป็นโพรโทไทป์ของ Fushigibana

ดังนั้นอินสแตนซ์ของ Fushigibana ก็จะเป็นอินสแตนซ์ของ Fushigisou ด้วย แล้วก็จะเป็นอินสแตนซ์ของ Fushigidane ด้วย



ห่วงโซ่โพรโทไทป์เป็นสิ่งที่เปลี่ยนแปลงได้

ห่วงโซ่โพรโทไทป์นั้นแท้จริงแล้วไม่ได้เป็นสิ่งตายตัว ที่จริงแล้วหลังจากกำหนดให้คลาสหนึ่งรับทอดจากอีกคลาสหนึ่งแล้วก็ยัง สามารถเปลี่ยนแปลงภายหลังได้

นั่นหมายถึงว่าในช่วงเวลาหนึ่ง คลาส C อาจรับทอดจากคลาส A อยู่ แต่เวลาต่อมาอาจเปลี่ยนมารับทอดคลาส B

แต่ความเปลี่ยนแปลงนี้จะไม่ส่งผลไปถึงอินสแตนซ์ที่สร้างขึ้นจากคลาสนั้นก่อนที่จะเปลี่ยนแปลงคลาสที่รับทอด

หมายความว่า สมมุติว่าสร้างออบเจ็กต์ c1 มาตอนที่คลาส C รับทอดคลาส A อยู่ และสร้างออบเจ็กต์ c2 มาตอนที่คลาส C รับทอดคลาส B อยู่

แบบนี้ออบเจ็กต์ c2 จะได้คุณสมบัติจากคลาส B ติดตัวมา ส่วนออบเจ็กต์ c1 นั้นจะได้คุณสมบัติจากคลาส A ติดตัวมา และติดตัวไปตลอดแม้ว่าคลาส C จะเปลี่ยนมารับทอดคลาส B แทนแล้วก็ตาม

ตัวอย่าง
function Zenigame() {} // สร้างคลาส Zenigame
Zenigame.prototype.siangrong = function() {
  alert("เซนิ เซนิ");
};

function Hitokage() {} // สร้างคลาส Hitokage
Hitokage.prototype.siangrong = function() {
  alert("คาเงะ คาเงะ");
};

function PokemonX() {} // สร้างคลาส PokemonX
PokemonX.prototype = new Zenigame(); // รับทอดจาก Zenigame
var pk1 = new PokemonX();
pk1.siangrong(); // ได้ เซนิ เซนิ

PokemonX.prototype = new Hitokage(); // เปลี่ยนเป็นรับทอดจาก Hitokage
var pk2 = new PokemonX();
pk2.siangrong(); // ได้ คาเงะ คาเงะ
pk1.siangrong(); // ได้ เซนิ เซนิ

จะเห็นว่า pk1 ที่ถูกสร้างขึ้นมาตอนที่ PokemonX รับรอดคลาส Zenigame อยู่นั้นจะร้อง "เซนิ เซนิ" ซึ่งเป็นเสียงร้องที่กำหนดในคลาส Zenigame

ส่วน pk2 ที่ถูกสร้างขึ้นมาหลังจากที่ PokemoX เปลี่ยนมารับทอดคลาส Hitokage แทนแล้วจะร้อง "คาเงะ คาเงะ" ของคลาส Hitokage

แล้วหลังจากนั้นหากมาดูเสียงร้องของ pk1 ใหม่ก็จะพบว่ายังคงเป็น "เซนิ เซนิ" ของคลาส Zenigame อยู่ไม่เปลี่ยนแปลง

ดังนั้นสรุปได้ว่าห่วงโซ่โพรโทไทป์ถูกเปลี่ยนแปลงกลางคันได้ แต่จะไม่ส่งผลต่ออินสแตนซ์ที่ถูกสร้างขึ้นมาตั้งแต่ก่อนหน้า



โครงสร้างในคอนสตรักเตอร์เมื่อมีการรับทอด

จากตัวอย่างที่ผ่านมานั้น แค่ยกตัวอย่างที่คอนสตรักเตอร์เป็นคลาสเปล่าๆไม่ได้มีการป้อนค่าใดๆให้ให้อินสแตนซ์ภายในโครงสร้างคอนสตรักเตอร์เลย คือเป็นวงเล็บปีกกาเปิดปิดเฉยๆ {}

แล้วพอทำการรับทอดก็แค่ป้อนโพรโทไทป์ให้ เป็นแค่การสร้างห่วงโซ่โพรโทไทป์ สร้างความสัมพันธ์ระหว่างตัวคลาสใหม่กับอินสแตนซ์ของคลาสเดิมเท่านั้น แต่ไม่ได้รับคอนสตรักเตอร์ของคลาสเดิมมาด้วย

หากต้องการรับทอดคอนสตรักเตอร์ด้วยต้องเรียกคอนสตรักเตอร์ของคลาสเดิมภายในคอนสตรักเตอร์ของคลาสใหม่โดยใช้ call

ตัวอย่าง
function Pokemon(chue) {
  this.chue = chue;
}

function Tamatama(chue) {
  Pokemon.call(this, chue); // รับทอดคอนสตรักเตอร์
}
Tamatama.prototype = new Pokemon(); // รับทอดโพรโทไทป์

var tamachan = new Tamatama("ทามะจัง");
alert(tamachan.chue); // ได้ ทามะจัง

การเรียกคอนสตรักเตอร์ของคลาสเดิมทำให้มีการสร้างพรอเพอร์ตี chue ขึ้น และการที่ต้องใช้ .call ก็เพื่อให้ this ในที่นี้คือตัวอินสแตนซ์ของคลาสใหม่

เพียงเท่านี้คอนสตรักเตอร์ของคลาสใหม่ก็จะเหมือนคอนสตรักเตอร์ของคลาสเดิมแล้ว

นอกจากนี้หากต้องการเพิ่มเติมอะไรในคอนสตรักเตอร์ของคลาสใหม่ก็เขียนเพิ่มต่อได้เลย เช่น
function Pokemon(chue) {
  this.chue = chue;
}

function Dogars(chue, siang) {
  Pokemon.call(this, chue); // รับทอดจากคอนสตรักเตอร์ของคลาสเดิม
  this.siang = siang; // ส่วนที่เพิ่มเข้ามาใหม่
}
Dogars.prototype = new Pokemon();

var doga = new Dogars("โดการ์ส", "โดกา โดก้า");
alert(doga.chue); // ได้ โดการ์ส
alert(doga.siang); // ได้ โดกา โดก้า


การค้นหาพรอเพอร์ตีตามห่วงโซ่โพรโทไทป์

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

หากในนั้นยังไม่มีอีกก็จะไปหาในโพรไทป์ของโพรโทไทป์นั้นอีกที ไล่ไปตามห่วงโซ่โพรโทไทป์เรื่อยๆ ถ้าเจอเมื่อไหร่ก็เอาค่าจากตรงนั้น ถ้าไม่เจอก็ไล่ต่อไป

จนสุดท้ายถึงไม่เจอจริงๆจึงจะได้ undefined

เพื่อให้เห็นภาพ ขอยกตัวอย่างโดยสร้างคลาสแล้วรับทอดต่อมาเรื่อยๆ
function Chicorita(chue) {
  this.chue = chue;
}
Chicorita.prototype.namnak = 6.4;

function Bayleaf(chue) {
  Chicorita.call(this, chue);
}
Bayleaf.prototype = new Chicorita();
Bayleaf.prototype.namnak = 15.8;

function Meganium(chue) {
  Bayleaf.call(this, chue);
}
Meganium.prototype = new Bayleaf();
Meganium.prototype.namnak = 100.5;

var mechan = new Meganium("เมจัง");
mechan.namnak = 99.2;

alert(mechan.namnak); // ได้ 99.2
delete mechan.namnak; // ลบ namnak ในตัวอินสแตนซ์
alert(mechan.namnak); // ได้ 100.5
delete Meganium.prototype.namnak; // ลบ namnak โพรโทไทป์
alert(mechan.namnak); // ได้ 15.8
delete Bayleaf.prototype.namnak; // ลบ namnak โพรโทไทป์
alert(mechan.namnak); // ได้ 6.4
delete Chicorita.prototype.namnak; // ลบ namnak โพรโทไทป์
alert(mechan.namnak); // ได้ undefined

ในที่นี้พรอเพอร์ตี namnak ถูกกำหนดไว้ทั้งที่ตัวอินสแตนซ์ mechan เอง แล้วก็แต่ละโพรโทไทป์ที่รับทอดมา

ตอนแรกมีพรอเพอร์ตี namnak อยู่ที่ตัวอินสแตนซ์ พอดูค่าจึงได้ค่าที่กำหนดให้ตัวอินสแนซ์นั้น คือ 99.2

แต่พอลบทิ้งไป พอมาดูค่าอีกทีก็หา namnak อินสแตนซ์ไม่เจอแล้ว จึงไปหาดูในโพรโทไทป์ต่อ

ถ้าลบ namnak ในโพรไทป์ไปอีกก็จะไล่ไปตามห่วงโซ่โพรโทไทป์ ไปหา namnak ในโพรโทป์ลำดับเหนือขึ้นไปอีก

ถ้าลบไปหมดก็กลายเป็น undefined ไป

จากตัวอย่างนี้น่าจะพอทำให้เห็นภาพของห่วงโซ่โพรโทไทป์และเข้าใจความหมายของการรับทอดในจาวาสคริปต์กันได้




-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> javascript

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

目次

日本による名言集
モジュール
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
機械学習
-- ニューラル
     ネットワーク
javascript
モンゴル語
言語学
maya
確率論
日本での日記
中国での日記
-- 北京での日記
-- 香港での日記
-- 澳門での日記
台灣での日記
北欧での日記
他の国での日記
qiita
その他の記事

記事の類別



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  記事を検索

  おすすめの記事

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

ไทย

日本語

中文