ฉันรู้ว่าคำตอบนี้คือ 3 ปีปลาย แต่ผมคิดว่าคำตอบที่ปัจจุบันไม่ได้ให้ข้อมูลเพียงพอเกี่ยวกับวิธีการรับมรดก prototypal ดีกว่ามรดกคลาสสิก
ก่อนอื่นเรามาดูข้อโต้แย้งที่พบบ่อยที่สุดของโปรแกรมจาวาสคริปต์ในการป้องกันการรับมรดกต้นแบบ (ฉันกำลังนำข้อโต้แย้งเหล่านี้มาจากกลุ่มคำตอบปัจจุบัน):
- มันง่ายมาก
- มันทรงพลัง
- มันนำไปสู่รหัสที่เล็กลงและซ้ำซ้อนน้อยลง
- มันเป็นแบบไดนามิกและมันจะดีกว่าสำหรับภาษาแบบไดนามิก
ตอนนี้ข้อโต้แย้งทั้งหมดนั้นใช้ได้ แต่ไม่มีใครสนใจอธิบายว่าทำไม มันเหมือนกับบอกเด็กว่าการเรียนคณิตศาสตร์เป็นเรื่องสำคัญ แน่นอน แต่เด็กไม่สนใจ และคุณไม่สามารถทำให้เด็กอย่างคณิตศาสตร์โดยบอกว่ามันสำคัญ
ฉันคิดว่าปัญหาเกี่ยวกับการรับมรดกต้นแบบคือมันอธิบายได้จากมุมมองของ JavaScript ฉันรัก JavaScript แต่การสืบทอดต้นแบบใน JavaScript ไม่ถูกต้อง ซึ่งแตกต่างจากการสืบทอดดั้งเดิมมีสองรูปแบบของการสืบทอดต้นแบบ:
- รูปแบบต้นแบบของมรดกต้นแบบ
- รูปแบบตัวสร้างของการสืบทอดต้นแบบ
น่าเสียดายที่ JavaScript ใช้รูปแบบคอนสตรัคเตอร์ของการสืบทอดต้นแบบ เนื่องจากเมื่อมีการสร้าง JavaScript เบรนแดนอีช (ผู้สร้าง JS) ต้องการให้ดูเหมือน Java (ซึ่งมีการสืบทอดคลาสสิก):
และเราได้ผลักดันมันในฐานะน้องชายคนเล็กสู่ Java เนื่องจากภาษาที่สมบูรณ์เช่น Visual Basic คือ C ++ ในตระกูลภาษาของ Microsoft ในเวลานั้น
สิ่งนี้ไม่ดีเพราะเมื่อคนใช้ Constructor ใน JavaScript พวกเขาคิดว่า Constructor ที่สืบทอดมาจาก Constructor อื่น ๆ นี่เป็นสิ่งที่ผิด ในวัตถุต้นแบบมรดกสืบทอดจากวัตถุอื่น ช่างก่อสร้างไม่เคยเข้ามาในรูปภาพ นี่คือสิ่งที่คนส่วนใหญ่สับสน
ผู้คนจากภาษาเช่น Java ซึ่งมีการสืบทอดคลาสสิกสับสนมากขึ้นเพราะถึงแม้ว่าคอนสตรัคเตอร์จะดูเหมือนคลาสที่ไม่ทำงานเหมือนคลาส ตามที่Douglas Crockfordกล่าวว่า:
ทางอ้อมนี้มีจุดประสงค์เพื่อทำให้ภาษาดูคุ้นเคยกับโปรแกรมเมอร์ที่ผ่านการฝึกอบรมมาแล้ว แต่ไม่สามารถทำเช่นนั้นได้เนื่องจากเราสามารถเห็นได้จากผู้เขียน Java ที่มีความคิดเห็นต่ำมากที่มีจาวาสคริปต์ รูปแบบตัวสร้างของ JavaScript ไม่ดึงดูดฝูงชนแบบดั้งเดิม นอกจากนี้ยังบดบังธรรมชาติต้นแบบที่แท้จริงของ JavaScript เป็นผลให้มีโปรแกรมเมอร์น้อยมากที่รู้วิธีการใช้ภาษาอย่างมีประสิทธิภาพ
ที่นั่นคุณมีมัน ตรงจากปากม้า
มรดกต้นแบบที่แท้จริง
การสืบทอดต้นแบบเป็นเรื่องเกี่ยวกับวัตถุ วัตถุสืบทอดคุณสมบัติจากวัตถุอื่น ๆ นั่นคือทั้งหมดที่มีให้มัน มีสองวิธีในการสร้างวัตถุโดยใช้การสืบทอดต้นแบบ:
- สร้างวัตถุใหม่
- โคลนวัตถุที่มีอยู่และขยาย
หมายเหตุ: JavaScript มีสองวิธีในการโคลนวัตถุ - คณะผู้แทนและเรียงต่อกัน ต่อจากนี้ไปฉันจะใช้คำว่า "โคลน" เพื่ออ้างถึงมรดกผ่านการมอบหมายเท่านั้นและคำว่า "คัดลอก" เพื่ออ้างถึงมรดกโดยผ่านการต่อข้อมูลเท่านั้น
พอคุยกัน ลองดูตัวอย่าง ว่าฉันมีวงกลมรัศมี5
:
var circle = {
radius: 5
};
เราสามารถคำนวณพื้นที่และเส้นรอบวงของวงกลมจากรัศมี:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
10
ตอนนี้ผมต้องการที่จะสร้างวงกลมรัศมีอีก วิธีหนึ่งในการทำเช่นนี้คือ:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
อย่างไรก็ตาม JavaScript ให้เป็นวิธีที่ดีกว่า - คณะผู้แทน Object.create
ฟังก์ชั่นที่ใช้ในการทำเช่นนี้:
var circle2 = Object.create(circle);
circle2.radius = 10;
นั่นคือทั้งหมดที่ คุณเพิ่งได้รับมรดกต้นแบบใน JavaScript ไม่ง่ายอย่างนั้นเหรอ? คุณนำวัตถุโคลนมันเปลี่ยนแปลงอะไรก็ตามที่คุณต้องการและไงล่ะ - คุณได้วัตถุใหม่เอี่ยม
ตอนนี้คุณอาจถามว่า "มันง่ายขนาดนี้ได้อย่างไรทุกครั้งที่ฉันต้องการสร้างวงกลมใหม่ที่ฉันต้องการโคลนcircle
และกำหนดรัศมีด้วยตนเอง" ทางแก้คือใช้ฟังก์ชั่นในการยกของหนัก:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
ในความเป็นจริงคุณสามารถรวมทั้งหมดนี้เป็นวัตถุเดียวตามตัวอักษรดังนี้
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
การสร้างต้นแบบใน JavaScript
หากคุณสังเกตเห็นในโปรแกรมข้างต้นcreate
ฟังก์ชั่นสร้างโคลนของcircle
ให้กำหนดใหม่radius
ให้กับมันแล้วส่งกลับ นี่คือสิ่งที่คอนสตรัคทำใน JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
รูปแบบตัวสร้างใน JavaScript เป็นรูปแบบต้นแบบกลับด้าน แทนที่จะสร้างวัตถุคุณสร้างนวกรรมิก new
คำหลักที่ผูกthis
ตัวชี้ภายในคอนสตรัคเพื่อโคลนของการprototype
ของการสร้าง
ฟังดูสับสน? เป็นเพราะรูปแบบตัวสร้างใน JavaScript ทำให้สิ่งต่าง ๆ มีความซับซ้อนโดยไม่จำเป็น นี่คือสิ่งที่โปรแกรมเมอร์ส่วนใหญ่เข้าใจยาก
แทนที่จะคิดว่าวัตถุที่สืบทอดมาจากวัตถุอื่น ๆ พวกเขาคิดว่าตัวสร้างที่สืบทอดมาจากตัวสร้างอื่น ๆ และจากนั้นกลายเป็นสับสนอย่างเต็มที่
มีเหตุผลอื่นอีกมากมายที่ทำให้รูปแบบตัวสร้างใน JavaScript ควรหลีกเลี่ยง คุณสามารถอ่านเกี่ยวกับพวกเขาได้ในบล็อกโพสต์ของฉันที่นี่: Constructors vs Prototypes
แล้วประโยชน์ของการรับมรดกต้นแบบเหนือมรดกคลาสสิกคืออะไร? Let 's Go ผ่านข้อโต้แย้งที่พบมากที่สุดอีกครั้งและอธิบายว่าทำไม
1. การสืบทอดต้นแบบเป็นเรื่องง่าย
CMSระบุไว้ในคำตอบของเขา:
ในความคิดของฉันประโยชน์ที่สำคัญของการสืบทอดมรดกต้นแบบคือความเรียบง่าย
ลองพิจารณาสิ่งที่เราเพิ่งทำ เราได้สร้างวัตถุที่มีรัศมีของcircle
จากนั้นเราก็โคลนและให้โคลนรัศมีของ5
10
ด้วยเหตุนี้เราต้องการเพียงสองสิ่งเท่านั้นที่จะทำให้มรดกเป็นแบบอย่าง:
- วิธีสร้างวัตถุใหม่ (เช่นตัวอักษรของวัตถุ)
- วิธีการขยายวัตถุที่มีอยู่ (เช่น
Object.create
)
ในทางตรงกันข้ามมรดกคลาสสิกมีความซับซ้อนมากขึ้น ในการสืบทอดแบบคลาสสิกคุณมี:
- การเรียนการสอน
- วัตถุ.
- อินเตอร์เฟซ
- ชั้นเรียนนามธรรม
- ชั้นเรียนสุดท้าย
- คลาสฐานเสมือน
- ก่อสร้าง
- destructors
คุณได้รับความคิด ประเด็นก็คือมรดกต้นแบบนั้นง่ายต่อการเข้าใจง่ายต่อการนำไปปฏิบัติและเหตุผลที่ง่ายกว่า
ดังที่ Steve Yegge วางไว้ในโพสต์บล็อกสุดคลาสสิค " Portrait of a N00b ":
ข้อมูลเมตาเป็นคำอธิบายหรือแบบจำลองของอย่างอื่น ความคิดเห็นในรหัสของคุณเป็นเพียงคำอธิบายในการคำนวณภาษาธรรมชาติ สิ่งที่ทำให้เมตาดาต้าเมตาดาต้าคือมันไม่จำเป็นอย่างเคร่งครัด ถ้าฉันมีสุนัขที่มีเอกสารทางสายเลือดบางอย่างและฉันทำเอกสารหายฉันก็ยังมีสุนัขที่ถูกต้องสมบูรณ์
ในชั้นเรียนความรู้สึกเดียวกันเป็นเพียงเมตาดาต้า ไม่จำเป็นต้องมีคลาสสำหรับการสืบทอด อย่างไรก็ตามบางคน (โดยปกติคือ n00bs) พบว่าชั้นเรียนสะดวกสบายยิ่งขึ้น มันทำให้พวกเขารู้สึกถึงความปลอดภัยผิด ๆ
เรารู้ด้วยว่าประเภทสแตติกเป็นเพียงข้อมูลเมตา พวกเขาเป็นประเภทของความคิดเห็นเฉพาะที่กำหนดเป้าหมายที่ผู้อ่านสองประเภท: โปรแกรมเมอร์และคอมไพเลอร์ ประเภทคงที่บอกเล่าเรื่องราวเกี่ยวกับการคำนวณน่าจะช่วยให้กลุ่มผู้อ่านทั้งสองเข้าใจวัตถุประสงค์ของโปรแกรม แต่ประเภทคงที่สามารถถูกโยนทิ้งไปที่รันไทม์เพราะในที่สุดพวกเขาก็แค่ความคิดเห็นที่เก๋ พวกเขาเหมือนเอกสารทางสายเลือด: มันอาจทำให้บุคลิกที่ไม่ปลอดภัยบางอย่างมีความสุขมากขึ้นเกี่ยวกับสุนัขของพวกเขา แต่สุนัขก็ไม่สนใจ
ตามที่ฉันได้กล่าวไปแล้วชั้นเรียนให้ความรู้สึกผิด ๆ กับความปลอดภัย ตัวอย่างเช่นคุณได้รับมากเกินไปNullPointerException
ใน Java แม้เมื่อรหัสของคุณชัดเจน ฉันพบว่าการสืบทอดคลาสสิกมักจะได้รับในทางของการเขียนโปรแกรม แต่อาจเป็นเพียง Java Python มีระบบการสืบทอดแบบคลาสสิกที่น่าทึ่ง
2. การสืบทอดต้นแบบมีประสิทธิภาพ
โปรแกรมเมอร์ส่วนใหญ่ที่มาจากพื้นหลังแบบคลาสสิกยืนยันว่าการสืบทอดแบบคลาสสิกนั้นมีประสิทธิภาพมากกว่าการสืบทอดแบบต้นแบบเพราะมี:
- ตัวแปรส่วนตัว
- หลายมรดก
การอ้างสิทธิ์นี้เป็นเท็จ เรารู้อยู่แล้วว่า JavaScript รองรับตัวแปรส่วนตัวผ่านการปิดแต่สิ่งที่เกี่ยวกับการสืบทอดหลาย ๆ วัตถุใน JavaScript มีต้นแบบเดียวเท่านั้น
ความจริงก็คือการถ่ายทอดมรดกต้นแบบสนับสนุนการสืบทอดจากต้นแบบหลาย การสืบทอดต้นแบบนั้นหมายถึงวัตถุหนึ่งที่สืบทอดจากวัตถุอื่น จริงๆแล้วมีสองวิธีในการนำมรดกต้นแบบไปใช้ :
- การมอบหมายหรือการสืบทอดที่แตกต่างกัน
- การโคลนหรือการต่อเชื่อมแบบสืบทอด
ใช่จาวาสคริปต์อนุญาตให้วัตถุเท่านั้นมอบอำนาจให้วัตถุอื่น ๆ อย่างไรก็ตามมันช่วยให้คุณสามารถคัดลอกคุณสมบัติของจำนวนวัตถุโดยพลการ ตัวอย่างเช่น_.extend
เพียงแค่นี้
แน่นอนว่าโปรแกรมเมอร์หลายคนไม่คิดว่านี่เป็นมรดกที่แท้จริงเพราะinstanceof
และisPrototypeOf
พูดอย่างอื่น อย่างไรก็ตามสิ่งนี้สามารถแก้ไขได้อย่างง่ายดายโดยการจัดเก็บชุดต้นแบบบนทุกวัตถุที่สืบทอดจากต้นแบบผ่านการต่อข้อมูล:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
ดังนั้นการรับมรดกต้นแบบมีประสิทธิภาพเท่ากับมรดกดั้งเดิม ในความเป็นจริงมันมีประสิทธิภาพมากกว่าการสืบทอดแบบคลาสสิกเพราะในการรับมรดกต้นแบบคุณสามารถเลือกคุณสมบัติที่จะคัดลอกและคุณสมบัติที่จะละเว้นจากต้นแบบที่แตกต่างกัน
ในการสืบทอดแบบคลาสสิกเป็นไปไม่ได้ (หรืออย่างน้อยยากมาก) ในการเลือกคุณสมบัติที่คุณต้องการรับช่วง พวกเขาใช้ฐานเรียนเสมือนจริงและอินเตอร์เฟซในการแก้ปัญหาเพชร
ใน JavaScript แต่คุณมักจะไม่เคยได้ยินปัญหาเพชรเพราะคุณสามารถควบคุมได้อย่างแม่นยำว่าคุณสมบัติใดที่คุณต้องการสืบทอดและต้นแบบต้นแบบ
3. การสืบทอดต้นแบบมีน้อยลงซ้ำซ้อน
จุดนี้เป็นเรื่องยากที่จะอธิบายเล็กน้อยเนื่องจากการสืบทอดแบบคลาสสิกไม่จำเป็นต้องนำไปสู่รหัสที่ซ้ำซ้อนมากขึ้น ในความเป็นจริงการสืบทอดไม่ว่าจะเป็นแบบดั้งเดิมหรือแบบต้นแบบจะใช้เพื่อลดความซ้ำซ้อนในรหัส
อาร์กิวเมนต์หนึ่งอาจเป็นได้ว่าภาษาการเขียนโปรแกรมส่วนใหญ่ที่มีการสืบทอดแบบคลาสสิกนั้นจะถูกพิมพ์แบบคงที่และต้องการให้ผู้ใช้ประกาศประเภทอย่างชัดเจน ดังนั้นสิ่งนี้นำไปสู่รหัส verbose เพิ่มเติม
Java เป็นที่รู้จักสำหรับพฤติกรรมนี้ ฉันจำBob Nystromได้อย่างชัดเจนโดยกล่าวถึงเกร็ดเล็กเกร็ดน้อยต่อไปนี้ในโพสต์บล็อกของเขาเกี่ยวกับPratt Parsers :
คุณต้องรัก Java ของ "โปรดลงนามในระดับสี่เท่า" ของระบบราชการที่นี่
อีกครั้งฉันคิดว่าเป็นเพียงเพราะ Java ดูดมาก
ข้อโต้แย้งที่ถูกต้องอย่างหนึ่งคือไม่ใช่ทุกภาษาที่มีการสืบทอดแบบดั้งเดิมสนับสนุนการสืบทอดหลายแบบ อีกครั้ง Java อยู่ในใจ ใช่ Java มีอินเตอร์เฟส แต่นั่นไม่เพียงพอ บางครั้งคุณต้องการมรดกหลายอย่าง
เนื่องจากการสืบทอดแบบต้นแบบอนุญาตให้มีการสืบทอดหลายครั้ง, รหัสที่ต้องใช้การสืบทอดหลายแบบจะมีความซ้ำซ้อนน้อยกว่าถ้าเขียนโดยใช้การสืบทอดแบบต้นแบบมากกว่าในภาษาที่มีการสืบทอดแบบดั้งเดิม แต่ไม่มีการสืบทอดหลายแบบ
4. การสืบทอดต้นแบบเป็นแบบไดนามิก
ข้อดีอย่างหนึ่งที่สำคัญที่สุดของการถ่ายทอดทางพันธุกรรมของต้นแบบคือคุณสามารถเพิ่มคุณสมบัติใหม่ให้กับต้นแบบหลังจากสร้างขึ้นแล้ว สิ่งนี้ช่วยให้คุณสามารถเพิ่มวิธีการใหม่ในต้นแบบซึ่งจะทำให้วัตถุทั้งหมดที่มอบหมายให้ต้นแบบนั้นพร้อมใช้งานโดยอัตโนมัติ
สิ่งนี้เป็นไปไม่ได้ในการสืบทอดแบบคลาสสิกเพราะเมื่อสร้างคลาสแล้วคุณจะไม่สามารถแก้ไขได้ในขณะใช้งานจริง นี่อาจเป็นข้อได้เปรียบที่ยิ่งใหญ่ที่สุดข้อเดียวของการรับมรดกต้นแบบเหนือมรดกคลาสสิกและควรจะอยู่ด้านบนสุด อย่างไรก็ตามฉันชอบการประหยัดที่ดีที่สุดสำหรับจุดจบ
ข้อสรุป
การถ่ายทอดทางพันธุกรรมเป็นเรื่องสำคัญ เป็นเรื่องสำคัญที่จะต้องให้ความรู้แก่โปรแกรมเมอร์จาวาสคริปต์เกี่ยวกับสาเหตุที่ต้องละทิ้งรูปแบบตัวสร้างของการถ่ายทอดทางพันธุกรรมต้นแบบเพื่อสนับสนุนรูปแบบต้นแบบของการถ่ายทอดทางพันธุกรรมต้นแบบ
เราจำเป็นต้องเริ่มสอน JavaScript อย่างถูกต้องและนั่นหมายถึงการแสดงโปรแกรมเมอร์ใหม่ถึงวิธีการเขียนโค้ดโดยใช้รูปแบบต้นแบบแทนที่จะเป็นรูปแบบตัวสร้าง
ไม่เพียง แต่จะอธิบายการสืบทอดมรดกโดยใช้รูปแบบต้นแบบได้ง่ายขึ้นเท่านั้น แต่จะทำให้โปรแกรมเมอร์ดีขึ้นด้วย
ถ้าคุณชอบคำตอบนี้คุณควรอ่านโพสต์บล็อกของฉันในหัวข้อ " Why Prototypal Intersitance Matters " เชื่อฉันสิคุณจะไม่ผิดหวัง