การสืบทอดคลาสเป็นวิธีหนึ่งสำหรับคลาสหนึ่งเพื่อขยายคลาสอื่น
ดังนั้นเราจึงสามารถสร้างฟังก์ชันใหม่เพิ่มเติมจากที่มีอยู่ได้
สมมติว่าเรามีคลาส Animal
:
สัตว์ชั้น { ตัวสร้าง (ชื่อ) { นี่.ความเร็ว = 0; this.name = ชื่อ; - วิ่ง (ความเร็ว) { this.speed = ความเร็ว; alert(`${this.name} วิ่งด้วยความเร็ว ${this.speed}.`); - หยุด() { นี่.ความเร็ว = 0; alert(`${this.name} ยืนนิ่ง.`); - - ให้สัตว์ = สัตว์ใหม่ ("สัตว์ของฉัน");
นี่คือวิธีที่เราสามารถแสดงวัตถุ animal
และคลาส Animal
แบบกราฟิก:
…และเราอยากจะสร้าง class Rabbit
หนึ่ง
เนื่องจากกระต่ายเป็นสัตว์ คลาส Rabbit
จึงควรอยู่บนพื้นฐานของ Animal
มีการเข้าถึงวิธีการของสัตว์ เพื่อให้กระต่ายสามารถทำสิ่งที่สัตว์ "ทั่วไป" ทำได้
ไวยากรณ์ที่จะขยายคลาสอื่นคือ: class Child extends Parent
มาสร้าง class Rabbit
ที่สืบทอดมาจาก Animal
:
คลาส Rabbit ขยายสัตว์ { ซ่อน() { alert(`${this.name} ซ่อน!`); - - ให้กระต่าย = กระต่ายใหม่ ("กระต่ายขาว"); กระต่าย.รัน(5); // กระต่ายขาววิ่งด้วยความเร็ว 5 กระต่าย.ซ่อน(); // กระต่ายขาวซ่อนตัว!
Object ของคลาส Rabbit
มีสิทธิ์เข้าถึงทั้งวิธี Rabbit
เช่น rabbit.hide()
และวิธี Animal
เช่น rabbit.run()
ภายใน extends
การทำงานของคีย์เวิร์ดโดยใช้กลไกต้นแบบเก่าที่ดี มันตั้งค่า Rabbit.prototype.[[Prototype]]
เป็น Animal.prototype
ดังนั้นหากไม่พบวิธีการใน Rabbit.prototype
JavaScript จะนำวิธีการนั้นมาจาก Animal.prototype
ตัวอย่างเช่น หากต้องการค้นหาวิธี rabbit.run
เครื่องยนต์จะตรวจสอบ (จากล่างขึ้นบนของรูปภาพ):
วัตถุ rabbit
(ไม่มี run
)
ต้นแบบของมันนั่นคือ Rabbit.prototype
(มี hide
แต่ไม่ได้ run
)
ต้นแบบของมันนั่นคือ (เนื่องจาก extends
) Animal.prototype
ซึ่งในที่สุดก็มีวิธี run
ดังที่เราจำได้จากบท Native Prototype ตัว JavaScript เองใช้การสืบทอด Prototypal สำหรับออบเจ็กต์บิวท์อิน เช่น Date.prototype.[[Prototype]]
คือ Object.prototype
นั่นเป็นเหตุผลที่วันที่สามารถเข้าถึงวิธีการของวัตถุทั่วไป
อนุญาตให้ใช้นิพจน์ใดๆ หลังจาก extends
ไวยากรณ์ของคลาสอนุญาตให้ระบุไม่เพียงแต่คลาส แต่นิพจน์ใดๆ หลังจาก extends
ตัวอย่างเช่น การเรียกใช้ฟังก์ชันที่สร้างคลาสพาเรนต์:
ฟังก์ชั่น f(วลี) { กลับคลาส { sayHi() { การแจ้งเตือน (วลี); - - - ผู้ใช้คลาสขยาย f("สวัสดี") {} ผู้ใช้ใหม่().sayHi(); // สวัสดี
ที่นี่ class User
สืบทอดมาจากผลลัพธ์ของ f("Hello")
ซึ่งอาจเป็นประโยชน์สำหรับรูปแบบการเขียนโปรแกรมขั้นสูง เมื่อเราใช้ฟังก์ชันเพื่อสร้างคลาสโดยขึ้นอยู่กับเงื่อนไขต่างๆ มากมายและสามารถสืบทอดจากเงื่อนไขเหล่านั้นได้
ตอนนี้เรามาก้าวไปข้างหน้าและแทนที่วิธีการกัน ตามค่าเริ่มต้น วิธีการทั้งหมดที่ไม่ได้ระบุไว้ใน class Rabbit
จะถูกนำไปใช้โดยตรงจาก class Animal
แต่ถ้าเราระบุวิธีการของเราเองใน Rabbit
เช่น stop()
ก็จะใช้แทน:
คลาส Rabbit ขยายสัตว์ { หยุด() { // ...ตอนนี้จะใช้กับ rabbit.stop() // แทนที่จะหยุด() จากคลาส Animal - -
อย่างไรก็ตาม โดยปกติแล้ว เราไม่ต้องการแทนที่วิธีการหลักโดยสิ้นเชิง แต่ต้องการต่อยอดจากวิธีการดังกล่าวเพื่อปรับแต่งหรือขยายฟังก์ชันการทำงาน เราทำบางอย่างในวิธีการของเรา แต่เรียกวิธีหลักก่อน/หลังหรือในกระบวนการ
คลาสให้คีย์เวิร์ด "super"
สำหรับสิ่งนั้น
super.method(...)
เพื่อเรียกเมธอดพาเรนต์
super(...)
เพื่อเรียกตัวสร้างหลัก (ภายในตัวสร้างของเราเท่านั้น)
ตัวอย่างเช่น ให้กระต่ายของเราซ่อนอัตโนมัติเมื่อหยุด:
สัตว์ชั้น { ตัวสร้าง (ชื่อ) { นี่.ความเร็ว = 0; this.name = ชื่อ; - วิ่ง (ความเร็ว) { this.speed = ความเร็ว; alert(`${this.name} วิ่งด้วยความเร็ว ${this.speed}.`); - หยุด() { นี่.ความเร็ว = 0; alert(`${this.name} ยืนนิ่ง.`); - - คลาส Rabbit ขยายสัตว์ { ซ่อน() { alert(`${this.name} ซ่อน!`); - หยุด() { ซุปเปอร์.หยุด(); // เรียกผู้ปกครองหยุด นี่.ซ่อน(); //แล้วซ่อน. - - ให้กระต่าย = กระต่ายใหม่ ("กระต่ายขาว"); กระต่าย.รัน(5); // กระต่ายขาววิ่งด้วยความเร็ว 5 กระต่าย.หยุด(); // กระต่ายขาวยืนนิ่ง กระต่ายขาวซ่อนตัว!
ตอนนี้ Rabbit
มีวิธี stop
ที่เรียกพาเรนต์ super.stop()
ในกระบวนการ
ฟังก์ชันลูกศรไม่มี super
ดังที่กล่าวไว้ในบทที่ฟังก์ชัน Arrow กลับมาอีกครั้ง ฟังก์ชันลูกศรไม่มี super
หากเข้าถึงได้ก็จะนำมาจากฟังก์ชันภายนอก ตัวอย่างเช่น:
คลาส Rabbit ขยายสัตว์ { หยุด() { setTimeout(() => super.stop(), 1,000); // เรียกผู้ปกครองหยุดหลังจาก 1 วินาที - -
ฟังก์ชั่น super
ในลูกศรนั้นเหมือนกับใน stop()
ดังนั้นจึงทำงานได้ตามที่ตั้งใจไว้ หากเราระบุฟังก์ชัน "ปกติ" ที่นี่ ก็จะมีข้อผิดพลาด:
//สุดยอดอย่างไม่คาดคิด setTimeout(ฟังก์ชั่น() { super.stop() }, 1,000);
ด้วยตัวสร้างมันจะยุ่งยากเล็กน้อย
จนถึงขณะนี้ Rabbit
ยังไม่มี constructor
ของตัวเอง
ตามข้อกำหนด หากคลาสขยายคลาสอื่นและไม่มี constructor
ดังนั้น constructor
“ว่าง” ต่อไปนี้จะถูกสร้างขึ้น:
คลาส Rabbit ขยายสัตว์ { // สร้างขึ้นเพื่อขยายคลาสโดยไม่มีตัวสร้างของตัวเอง ตัวสร้าง (...args) { ซุปเปอร์(...หาเรื่อง); - -
ดังที่เราเห็น โดยทั่วไปแล้วมันจะเรียก constructor
พาเรนต์ผ่านอาร์กิวเมนต์ทั้งหมด นั่นจะเกิดขึ้นถ้าเราไม่เขียน Constructor ของเราเอง
ตอนนี้เรามาเพิ่ม Constructor แบบกำหนดเองให้กับ Rabbit
มันจะระบุ earLength
นอกเหนือจาก name
:
สัตว์ชั้น { ตัวสร้าง (ชื่อ) { นี่.ความเร็ว = 0; this.name = ชื่อ; - - - คลาส Rabbit ขยายสัตว์ { ตัวสร้าง (ชื่อ, earLength) { นี่.ความเร็ว = 0; this.name = ชื่อ; this.earLength = ความยาวหู; - - - //ไม่ได้ผล! ให้ rabbit = new Rabbit("White Rabbit", 10); // ข้อผิดพลาด: ไม่ได้กำหนดไว้
อ๊ะ! เราได้รับข้อผิดพลาด ตอนนี้เราไม่สามารถสร้างกระต่ายได้ เกิดอะไรขึ้น?
คำตอบสั้น ๆ คือ:
Constructor ในการสืบทอดคลาสต้องเรียก super(...)
และ (!) ทำก่อนใช้ this
…แต่ทำไมล่ะ? เกิดอะไรขึ้นที่นี่? จริงๆ แล้วข้อกำหนดก็ดูแปลกไป
แน่นอนว่ามีคำอธิบาย มาดูรายละเอียดกันดีกว่า แล้วคุณจะเข้าใจสิ่งที่เกิดขึ้นอย่างแท้จริง
ใน JavaScript มีความแตกต่างระหว่างฟังก์ชันคอนสตรัคเตอร์ของคลาสที่สืบทอด (เรียกว่า "คอนสตรัคเตอร์ที่ได้รับ") และฟังก์ชันอื่นๆ ตัวสร้างที่ได้รับมีคุณสมบัติภายในพิเศษ [[ConstructorKind]]:"derived"
นั่นเป็นป้ายกำกับภายในแบบพิเศษ
ป้ายกำกับนั้นส่งผลต่อพฤติกรรมด้วย new
เมื่อฟังก์ชันปกติถูกดำเนินการด้วย new
มันจะสร้างวัตถุว่างและกำหนดให้กับ this
แต่เมื่อตัวสร้างที่ได้รับมาทำงาน มันจะไม่ทำเช่นนี้ คาดว่าตัวสร้างหลักจะทำงานนี้
ดังนั้นคอนสตรัคเตอร์ที่ได้รับจะต้องเรียก super
เพื่อดำเนินการคอนสตรัคเตอร์พาเรนต์ (ฐาน) มิฉะนั้นจะไม่สร้างอ็อบเจ็กต์สำหรับ this
และเราจะได้รับข้อผิดพลาด
เพื่อให้ Rabbit
Constructor ทำงานได้ จะต้องเรียก super()
ก่อนใช้ this
ดังตัวอย่างที่นี่:
สัตว์ชั้น { ตัวสร้าง (ชื่อ) { นี่.ความเร็ว = 0; this.name = ชื่อ; - - - คลาส Rabbit ขยายสัตว์ { ตัวสร้าง (ชื่อ, earLength) { ซุปเปอร์(ชื่อ); this.earLength = ความยาวหู; - - - // ตอนนี้สบายดีแล้ว ให้ rabbit = new Rabbit("White Rabbit", 10); การแจ้งเตือน (rabbit.name); // กระต่ายขาว การแจ้งเตือน (rabbit.earLength); // 10
หมายเหตุขั้นสูง
หมายเหตุนี้ถือว่าคุณมีประสบการณ์บางอย่างกับชั้นเรียน ซึ่งอาจเป็นภาษาโปรแกรมอื่นๆ
ช่วยให้เข้าใจภาษาได้ดีขึ้น และยังอธิบายพฤติกรรมที่อาจเป็นสาเหตุของจุดบกพร่อง (แต่ไม่บ่อยนัก)
หากคุณพบว่ามันยากที่จะเข้าใจ ให้อ่านต่อ แล้วค่อยกลับมาอ่านใหม่ในภายหลัง
เราไม่เพียงแต่สามารถแทนที่เมธอดเท่านั้น แต่ยังรวมถึงฟิลด์คลาสด้วย
แม้ว่าจะมีพฤติกรรมที่ยุ่งยากเมื่อเราเข้าถึงฟิลด์ที่ถูกแทนที่ในตัวสร้างพาเรนต์ ซึ่งค่อนข้างแตกต่างจากภาษาโปรแกรมอื่น ๆ ส่วนใหญ่
ลองพิจารณาตัวอย่างนี้:
สัตว์ชั้น { ชื่อ = 'สัตว์'; ตัวสร้าง () { การแจ้งเตือน (this.name); - - - คลาส Rabbit ขยายสัตว์ { ชื่อ = 'กระต่าย'; - สัตว์ใหม่ (); // สัตว์ กระต่ายใหม่ (); // สัตว์
ที่นี่ คลาส Rabbit
จะขยาย Animal
และแทนที่ฟิลด์ name
ด้วยค่าของมันเอง
ไม่มีตัวสร้างของตัวเองใน Rabbit
ดังนั้นจึงเรียกว่าตัวสร้าง Animal
สิ่งที่น่าสนใจคือในทั้งสองกรณี: new Animal()
และ new Rabbit()
alert
ในบรรทัด (*)
จะแสดง animal
กล่าวอีกนัยหนึ่ง ตัวสร้างหลักจะใช้ค่าฟิลด์ของตัวเองเสมอ ไม่ใช่ค่าที่ถูกแทนที่
มีอะไรแปลกเกี่ยวกับเรื่องนี้?
หากยังไม่ชัดเจนกรุณาเปรียบเทียบกับวิธีการ
นี่เป็นโค้ดเดียวกัน แต่แทนที่จะเป็นฟิลด์ this.name
เราเรียกเมธอด this.showName()
:
สัตว์ชั้น { showName() { // แทน this.name = 'animal' alert('สัตว์'); - ตัวสร้าง () { this.showName(); // แทนการแจ้งเตือน(this.name); - - คลาส Rabbit ขยายสัตว์ { ชื่อรายการ() { alert('กระต่าย'); - - สัตว์ใหม่ (); // สัตว์ กระต่ายใหม่ (); // กระต่าย
โปรดทราบ: ตอนนี้ผลลัพธ์จะแตกต่างออกไป
และนั่นคือสิ่งที่เราคาดหวังโดยธรรมชาติ เมื่อตัวสร้างพาเรนต์ถูกเรียกในคลาสที่ได้รับ จะใช้วิธีการแทนที่
…แต่สำหรับสาขาชั้นเรียนกลับไม่เป็นเช่นนั้น ตามที่กล่าวไว้ ตัวสร้างพาเรนต์จะใช้ฟิลด์พาเรนต์เสมอ
ทำไมถึงมีความแตกต่าง?
เหตุผลก็คือลำดับการเริ่มต้นฟิลด์ ฟิลด์คลาสถูกเตรียมใช้งาน:
ก่อนตัวสร้างสำหรับคลาสฐาน (ที่ไม่ขยายอะไรเลย)
ทันทีหลังจาก super()
สำหรับคลาสที่ได้รับ
ในกรณีของเรา Rabbit
เป็นคลาสที่ได้รับ ไม่มี constructor()
อยู่ในนั้น ดังที่กล่าวไว้ก่อนหน้านี้ นั่นก็เหมือนกับว่ามี Constructor ว่างๆ super(...args)
เท่านั้น
ดังนั้น new Rabbit()
เรียก super()
ดังนั้นการรันคอนสตรัคเตอร์พาเรนต์และ (ตามกฎสำหรับคลาสที่ได้รับ) หลังจากนั้นฟิลด์คลาสจะถูกเตรียมใช้งานเท่านั้น ในขณะที่ดำเนินการคอนสตรัคเตอร์พาเรนต์ ยังไม่มีฟิลด์คลาส Rabbit
นั่นคือสาเหตุว่าทำไมจึงใช้ฟิลด์ Animal
ความแตกต่างเล็กๆ น้อยๆ ระหว่างฟิลด์และวิธีการนี้มีความเฉพาะเจาะจงกับ JavaScript
โชคดีที่พฤติกรรมนี้จะเปิดเผยตัวเองก็ต่อเมื่อมีการใช้ฟิลด์ที่ถูกแทนที่ในตัวสร้างพาเรนต์ ถ้าอย่างนั้นมันอาจเป็นเรื่องยากที่จะเข้าใจว่าเกิดอะไรขึ้น ดังนั้นเราจึงอธิบายไว้ที่นี่
หากเกิดปัญหาขึ้น เราสามารถแก้ไขได้โดยใช้เมธอดหรือ getters/setters แทนฟิลด์
ข้อมูลขั้นสูง
หากคุณกำลังอ่านบทช่วยสอนเป็นครั้งแรก ส่วนนี้อาจถูกข้ามไป
เป็นเรื่องเกี่ยวกับกลไกภายในที่อยู่เบื้องหลังการสืบทอดและ super
มาเจาะลึกใต้ฝากระโปรงของ super
กันดีกว่า เราจะเห็นสิ่งที่น่าสนใจระหว่างทาง
ก่อนอื่นต้องบอกว่า จากทั้งหมดที่เราได้เรียนรู้มาจนถึงตอนนี้ มันเป็นไปไม่ได้เลยที่ super
จะทำงานได้เลย!
ใช่แล้ว ลองถามตัวเองดูว่าในทางเทคนิคแล้วควรทำงานอย่างไร? เมื่อเมธอดอ็อบเจ็กต์ทำงาน ก็จะได้รับอ็อบเจ็กต์ปัจจุบัน this
ถ้าเราเรียก super.method()
กลไกจำเป็นต้องรับ method
จากต้นแบบของอ็อบเจ็กต์ปัจจุบัน แต่อย่างไร?
งานอาจดูเหมือนง่าย แต่ก็ไม่เป็นเช่นนั้น เอ็นจิ้นรู้วัตถุปัจจุบัน this
ดังนั้นจึงสามารถรับเมธอดพา method
เป็น this.__proto__.method
น่าเสียดายที่วิธีแก้ปัญหาแบบ "ไร้เดียงสา" ดังกล่าวใช้ไม่ได้ผล
มาสาธิตปัญหากัน ไม่มีคลาส ใช้วัตถุธรรมดาเพื่อความเรียบง่าย
คุณสามารถข้ามส่วนนี้และไปที่ส่วนย่อย [[HomeObject]]
ด้านล่าง หากคุณไม่ต้องการทราบรายละเอียด นั่นจะไม่เป็นอันตราย หรืออ่านต่อหากคุณสนใจที่จะทำความเข้าใจสิ่งต่าง ๆ ในเชิงลึก
ในตัวอย่างด้านล่าง rabbit.__proto__ = animal
ทีนี้มาลองกัน: ใน rabbit.eat()
เราจะเรียก animal.eat()
โดยใช้ this.__proto__
:
ให้สัตว์ = { ชื่อ: "สัตว์" กิน() { alert(`${this.name} กิน.`); - - ให้กระต่าย = { __โปรโต__: สัตว์ ชื่อ: "กระต่าย", กิน() { // นั่นคือวิธีที่ super.eat() สามารถทำงานได้ this.__proto__.eat.call (สิ่งนี้); - - - กระต่าย.กิน(); // กระต่ายกิน.
ที่บรรทัด (*)
เรา eat
จากต้นแบบ ( animal
) และเรียกมันในบริบทของวัตถุปัจจุบัน โปรดทราบว่า .call(this)
มีความสำคัญที่นี่ เนื่องจากคำสั่งง่ายๆ this.__proto__.eat()
จะดำเนินการ parent eat
ในบริบทของต้นแบบ ไม่ใช่วัตถุปัจจุบัน
และในโค้ดด้านบนนี้ใช้งานได้จริงตามที่ตั้งใจไว้: เรามี alert
ที่ถูกต้อง
ทีนี้มาเพิ่มวัตถุอีกหนึ่งชิ้นในห่วงโซ่ เรามาดูกันว่าสิ่งต่าง ๆ พังอย่างไร:
ให้สัตว์ = { ชื่อ: "สัตว์" กิน() { alert(`${this.name} กิน.`); - - ให้กระต่าย = { __โปรโต__: สัตว์ กิน() { // ...เด้งกลับแบบกระต่ายแล้วเรียกวิธี parent (animal) this.__proto__.eat.call (สิ่งนี้); - - - ให้ลองเอียร์ = { __โปรโต__: กระต่าย กิน() { // ...ทำบางอย่างที่มีหูยาวแล้วเรียกเมธอด parent (rabbit) this.__proto__.eat.call (สิ่งนี้); - - - longEar.กิน(); // ข้อผิดพลาด: เกินขนาดสแต็กการโทรสูงสุด
รหัสไม่ทำงานอีกต่อไป! เราจะเห็นข้อผิดพลาดในการพยายามโทร longEar.eat()
อาจจะไม่ชัดเจนนัก แต่ถ้าเราติดตามการเรียก longEar.eat()
เราก็จะเข้าใจว่าทำไม ในทั้งสองบรรทัด (*)
และ (**)
ค่าของ this
คือวัตถุปัจจุบัน ( longEar
) นั่นเป็นสิ่งสำคัญ: วิธีการของวัตถุทั้งหมดได้รับวัตถุปัจจุบันเช่น this
ไม่ใช่ต้นแบบหรืออะไรสักอย่าง
ดังนั้นในทั้งสองบรรทัด (*)
และ (**)
ค่าของ this.__proto__
จะเหมือนกันทุกประการ: rabbit
ทั้งคู่เรียก rabbit.eat
โดยไม่ต้องต่อโซ่วนซ้ำไม่สิ้นสุด
นี่คือภาพของสิ่งที่เกิดขึ้น:
ภายใน longEar.eat()
บรรทัด (**)
เรียก rabbit.eat
โดยระบุ this=longEar
// ข้างใน longEar.eat() เรามีสิ่งนี้ = longEar this.__proto__.eat.call (นี่) // (**) // กลายเป็น longEar.__proto__.eat.call(นี้) //นั่นคือ rabbit.eat.call(นี่);
จากนั้นในบรรทัด (*)
ของ rabbit.eat
เราอยากจะส่งต่อการโทรให้สูงกว่านี้ในสายโซ่ แต่ this=longEar
ดังนั้น this.__proto__.eat
จึงเป็นอีกครั้ง rabbit.eat
!
// ข้างใน rabbit.eat() เราก็มีสิ่งนี้ = longEar this.__proto__.eat.call (นี่) // (*) // กลายเป็น longEar.__proto__.eat.call(นี้) // หรือ (อีกครั้ง) rabbit.eat.call(นี่);
…ดังนั้น rabbit.eat
จึงเรียกตัวเองว่าวนซ้ำไม่สิ้นสุด เพราะมันไม่สามารถขึ้นไปต่อไปได้อีก
ปัญหาไม่สามารถแก้ไขได้โดยใช้ this
เพียงอย่างเดียว
[[HomeObject]]
เพื่อจัดเตรียมโซลูชัน JavaScript จะเพิ่มคุณสมบัติภายในพิเศษอีกหนึ่งรายการสำหรับฟังก์ชัน: [[HomeObject]]
เมื่อฟังก์ชันถูกระบุเป็นคลาสหรือวิธีอ็อบเจ็กต์ คุณสมบัติ [[HomeObject]]
ของฟังก์ชันจะกลายเป็นอ็อบเจ็กต์นั้น
จากนั้น super
จะใช้มันเพื่อแก้ไขต้นแบบหลักและวิธีการของมัน
มาดูกันว่ามันทำงานอย่างไร อันดับแรกกับวัตถุธรรมดา:
ให้สัตว์ = { ชื่อ: "สัตว์" eat() { // animal.eat.[[HomeObject]] == สัตว์ alert(`${this.name} กิน.`); - - ให้กระต่าย = { __โปรโต__: สัตว์ ชื่อ: "กระต่าย", eat() { // rabbit.eat.[[HomeObject]] == กระต่าย ซุปเปอร์.กิน(); - - ให้ลองเอียร์ = { __โปรโต__: กระต่าย ชื่อ: "หูยาว", eat() { // longEar.eat.[[HomeObject]] == longEar ซุปเปอร์.กิน(); - - // ทำงานได้อย่างถูกต้อง longEar.กิน(); //หูยาวกิน.
มันทำงานได้ตามที่ตั้งใจไว้ เนื่องจากกลไกของ [[HomeObject]]
เมธอด เช่น longEar.eat
รู้จัก [[HomeObject]]
และใช้เมธอดพาเรนต์จากต้นแบบ โดยไม่ต้องใช้ this
อย่างที่เราเคยทราบมาก่อน ฟังก์ชั่นโดยทั่วไปจะ “ฟรี” และไม่ได้ผูกกับอ็อบเจ็กต์ใน JavaScript ดังนั้นจึงสามารถคัดลอกระหว่างวัตถุและเรียก this
กับวัตถุอื่นได้
การมีอยู่จริงของ [[HomeObject]]
ละเมิดหลักการดังกล่าว เนื่องจากวิธีการจดจำวัตถุของพวกเขา [[HomeObject]]
ไม่สามารถเปลี่ยนแปลงได้ ดังนั้นความผูกพันนี้จะคงอยู่ตลอดไป
ที่เดียวในภาษาที่ใช้ [[HomeObject]]
คือ super
ดังนั้นหากเมธอดไม่ได้ใช้ super
เราก็ยังสามารถพิจารณาว่ามันว่างและคัดลอกระหว่างอ็อบเจ็กต์ได้ แต่สิ่ง super
อาจผิดพลาดได้
นี่คือการสาธิตผลลัพธ์ super
ที่ไม่ถูกต้องหลังจากการคัดลอก:
ให้สัตว์ = { พูดสวัสดี() { alert(`ฉันเป็นสัตว์`); - - // กระต่ายสืบทอดมาจากสัตว์ ให้กระต่าย = { __โปรโต__: สัตว์ พูดสวัสดี() { ซุปเปอร์.sayHi(); - - ให้ปลูก = { พูดสวัสดี() { alert("ฉันเป็นพืช"); - - // ต้นไม้สืบทอดมาจากพืช ให้ต้นไม้ = { __โปรโต__: พืช sayHi: rabbit.sayHi // (*) - tree.sayHi(); //ฉันเป็นสัตว์(?!?)
การเรียก tree.sayHi()
แสดงว่า “ฉันเป็นสัตว์” ผิดแน่นอน.
เหตุผลง่ายๆ:
ในบรรทัด (*)
วิธีการ tree.sayHi
ถูกคัดลอกมาจาก rabbit
บางทีเราแค่อยากหลีกเลี่ยงการทำซ้ำโค้ด?
[[HomeObject]]
ของมันคือ rabbit
เนื่องจากมันถูกสร้างขึ้นใน rabbit
ไม่มีทางที่จะเปลี่ยน [[HomeObject]]
ได้
โค้ดของ tree.sayHi()
มี super.sayHi()
อยู่ข้างใน มันขึ้นจาก rabbit
และใช้วิธีจาก animal
นี่คือแผนภาพของสิ่งที่เกิดขึ้น:
[[HomeObject]]
ถูกกำหนดไว้สำหรับวิธีการทั้งในคลาสและในวัตถุธรรมดา แต่สำหรับอ็อบเจ็กต์ ต้องระบุวิธีการให้ตรงตาม method()
ไม่ใช่ "method: function()"
ความแตกต่างอาจไม่จำเป็นสำหรับเรา แต่สำคัญสำหรับ JavaScript
ในตัวอย่างด้านล่าง ไวยากรณ์ที่ไม่ใช่วิธีการใช้สำหรับการเปรียบเทียบ ไม่ได้ตั้งค่าคุณสมบัติ [[HomeObject]]
และการสืบทอดไม่ทำงาน:
ให้สัตว์ = { eat: function() { // ตั้งใจเขียนแบบนี้แทน eat() {... - - - ให้กระต่าย = { __โปรโต__: สัตว์ กิน: ฟังก์ชั่น () { ซุปเปอร์.กิน(); - - กระต่าย.กิน(); // เกิดข้อผิดพลาดในการเรียก super (เพราะไม่มี [[HomeObject]])
หากต้องการขยายคลาส: class Child extends Parent
:
นั่นหมายความว่า Child.prototype.__proto__
จะเป็น Parent.prototype
ดังนั้นวิธีการจึงสืบทอดมา
เมื่อเอาชนะคอนสตรัคเตอร์:
เราต้องเรียก parent Constructor ว่า super()
ใน Child
Constructor ก่อนที่จะใช้ this
เมื่อแทนที่วิธีอื่น:
เราสามารถใช้ super.method()
ในเมธอด Child
เพื่อเรียกเมธอด Parent
ได้
ภายใน:
วิธีการจดจำคลาส/วัตถุในคุณสมบัติ [[HomeObject]]
ภายใน นั่นคือวิธีที่ super
แก้ไขเมธอดพาเรนต์
ดังนั้นจึงไม่ปลอดภัยที่จะคัดลอกวิธีการด้วย super
จากวัตถุหนึ่งไปยังอีกวัตถุหนึ่ง
อีกด้วย:
ฟังก์ชั่นลูกศรไม่มี this
หรือ super
ของตัวเอง ดังนั้นจึงเข้ากันได้อย่างโปร่งใสกับบริบทโดยรอบ
ความสำคัญ: 5
นี่คือโค้ดที่มี Rabbit
ขยาย Animal
ขออภัย ไม่สามารถสร้างวัตถุ Rabbit
ได้ เกิดอะไรขึ้น? แก้ไขมัน
สัตว์ชั้น { ตัวสร้าง (ชื่อ) { this.name = ชื่อ; - - คลาส Rabbit ขยายสัตว์ { ตัวสร้าง (ชื่อ) { this.name = ชื่อ; this.created = วันที่ ตอนนี้ (); - - ให้กระต่าย = กระต่ายใหม่ ("กระต่ายขาว"); // ข้อผิดพลาด: ไม่ได้กำหนดไว้ การแจ้งเตือน (rabbit.name);
นั่นเป็นเพราะว่าตัวสร้างลูกต้องเรียก super()
นี่คือรหัสที่แก้ไข:
สัตว์ชั้น { ตัวสร้าง (ชื่อ) { this.name = ชื่อ; - - คลาส Rabbit ขยายสัตว์ { ตัวสร้าง (ชื่อ) { ซุปเปอร์(ชื่อ); this.created = วันที่ ตอนนี้ (); - - ให้กระต่าย = กระต่ายใหม่ ("กระต่ายขาว"); // ตกลงตอนนี้ การแจ้งเตือน (rabbit.name); // กระต่ายขาว
ความสำคัญ: 5
เรามีชั้นเรียน Clock
ณ ตอนนี้จะพิมพ์เวลาทุกวินาที
นาฬิกาชั้นเรียน { ตัวสร้าง ({ เทมเพลต }) { this.template = เทมเพลต; - แสดงผล() { ให้วันที่ = วันที่ใหม่ (); ให้ชั่วโมง = date.getHours(); ถ้า (ชั่วโมง < 10) ชั่วโมง = '0' + ชั่วโมง; ให้ mins = date.getMinutes(); ถ้า (นาที < 10) นาที = '0' + นาที; ให้วินาที = date.getSeconds(); ถ้า (วินาที < 10) วินาที = '0' + วินาที; ให้เอาต์พุต = this.template .replace('h', ชั่วโมง) .replace('m', นาที) .replace('s', วินาที); console.log(เอาท์พุท); - หยุด() { clearInterval(this.timer); - เริ่ม() { นี้.render(); this.timer = setInterval(() => this.render(), 1,000); - -
สร้างคลาส ExtendedClock
ใหม่ที่สืบทอดมาจาก Clock
และเพิ่ม precision
ของพารามิเตอร์ - จำนวน ms
ระหว่าง "เห็บ" โดยค่าเริ่มต้นควรเป็น 1000
(1 วินาที)
รหัสของคุณควรอยู่ในไฟล์ extended-clock.js
อย่าแก้ไข clock.js
ดั้งเดิม ขยายมัน.
เปิดแซนด์บ็อกซ์สำหรับงาน
คลาส ExtendedClock ขยายนาฬิกา { ตัวสร้าง (ตัวเลือก) { ซุปเปอร์(ตัวเลือก); ให้ { ความแม่นยำ = 1,000 } = ตัวเลือก; this.precision = ความแม่นยำ; - เริ่ม() { นี้.render(); this.timer = setInterval(() => this.render(), this.precision); - -
เปิดโซลูชันในแซนด์บ็อกซ์