ในบทแรกของส่วนนี้ เราได้กล่าวถึงว่ามีวิธีการที่ทันสมัยในการตั้งค่าต้นแบบ
การตั้งค่าหรือการอ่านต้นแบบด้วย obj.__proto__
ถือว่าล้าสมัยและค่อนข้างเลิกใช้แล้ว (ย้ายไปที่สิ่งที่เรียกว่า “ภาคผนวก B” ของมาตรฐาน JavaScript ซึ่งมีไว้สำหรับเบราว์เซอร์เท่านั้น)
วิธีการที่ทันสมัยในการรับ/ตั้งค่าต้นแบบคือ:
Object.getPrototypeOf(obj) – ส่งคืน [[Prototype]]
ของ obj
Object.setPrototypeOf(obj, proto) – ตั้งค่า [[Prototype]]
ของ obj
เป็น proto
การใช้งานเพียงอย่างเดียวของ __proto__
ที่ไม่ได้ขมวดคิ้วนั้นถือเป็นคุณสมบัติเมื่อสร้างวัตถุใหม่: { __proto__: ... }
แม้ว่าจะมีวิธีพิเศษสำหรับสิ่งนี้เช่นกัน:
Object.create(proto[, descriptors]) – สร้างวัตถุว่างโดยมี proto
ที่กำหนดเป็น [[Prototype]]
และตัวอธิบายคุณสมบัติเพิ่มเติม
ตัวอย่างเช่น:
ให้สัตว์ = { กิน: จริง - // สร้างวัตถุใหม่โดยมีสัตว์เป็นตัวอย่าง ให้กระต่าย = Object.create (สัตว์); // เช่นเดียวกับ {__proto__: สัตว์} การแจ้งเตือน (rabbit.eats); // จริง การแจ้งเตือน (Object.getPrototypeOf (กระต่าย) === สัตว์); // จริง Object.setPrototypeOf (กระต่าย, {}); // เปลี่ยนต้นแบบของ rabbit เป็น {}
เมธอด Object.create
มีประสิทธิภาพมากกว่าเล็กน้อย เนื่องจากมีอาร์กิวเมนต์ที่สองที่เป็นทางเลือก: ตัวอธิบายคุณสมบัติ
เราสามารถจัดเตรียมคุณสมบัติเพิ่มเติมให้กับออบเจ็กต์ใหม่ได้ เช่นนี้
ให้สัตว์ = { กิน: จริง - ให้กระต่าย = Object.create (สัตว์ { กระโดด: { ค่า: จริง - - การแจ้งเตือน (rabbit.jumps); // จริง
ตัวอธิบายอยู่ในรูปแบบเดียวกับที่อธิบายไว้ในบท ค่าสถานะและตัวอธิบายคุณสมบัติ
เราสามารถใช้ Object.create
เพื่อทำการโคลนวัตถุที่มีประสิทธิภาพมากกว่าการคัดลอกคุณสมบัติใน for..in
:
ให้โคลน = Object.create( Object.getPrototypeOf (obj), Object.getOwnPropertyDescriptors (obj) -
การเรียกนี้สร้างสำเนาที่แน่นอนของ obj
รวมถึงคุณสมบัติทั้งหมด: นับได้และนับไม่ได้ คุณสมบัติข้อมูลและตัวตั้งค่า/ตัวรับ - ทุกอย่าง และด้วย [[Prototype]]
ที่ถูกต้อง
มีหลายวิธีในการจัดการ [[Prototype]]
มันเกิดขึ้นได้อย่างไร? ทำไม
นั่นเป็นเหตุผลทางประวัติศาสตร์
มรดกต้นแบบเป็นภาษาตั้งแต่รุ่งอรุณ แต่วิธีการจัดการก็มีการพัฒนาไปตามกาลเวลา
คุณสมบัติ prototype
ของฟังก์ชันคอนสตรัคเตอร์นั้นใช้ได้ผลมาตั้งแต่สมัยโบราณ เป็นวิธีที่เก่าแก่ที่สุดในการสร้างวัตถุด้วยต้นแบบที่กำหนด
ต่อมาในปี พ.ศ. 2555 Object.create
ก็ปรากฏอยู่ในมาตรฐาน มันให้ความสามารถในการสร้างวัตถุด้วยต้นแบบที่กำหนด แต่ไม่ได้ให้ความสามารถในการรับ/ตั้งค่ามัน เบราว์เซอร์บางตัวใช้ตัวเข้าถึง __proto__
ที่ไม่ได้มาตรฐาน ซึ่งอนุญาตให้ผู้ใช้รับ/ตั้งค่าต้นแบบได้ตลอดเวลา เพื่อให้นักพัฒนามีความยืดหยุ่นมากขึ้น
ต่อมาในปี 2558 Object.setPrototypeOf
และ Object.getPrototypeOf
ได้ถูกเพิ่มเข้าไปในมาตรฐาน เพื่อให้มีฟังก์ชันการทำงานเหมือนกับ __proto__
เนื่องจาก __proto__
ถูกนำไปใช้จริงทุกที่ จึงเลิกใช้แล้วและได้มาถึงภาคผนวก B ของมาตรฐาน นั่นคือ: ตัวเลือกสำหรับสภาพแวดล้อมที่ไม่ใช่เบราว์เซอร์
ต่อมาในปี 2022 ได้รับอนุญาตอย่างเป็นทางการให้ใช้ __proto__
ใน object literals {...}
(ย้ายออกจากภาคผนวก B) แต่ไม่ใช่ในฐานะ getter/setter obj.__proto__
(ยังอยู่ในภาคผนวก B)
เหตุใด __proto__
จึงถูกแทนที่ด้วยฟังก์ชัน getPrototypeOf/setPrototypeOf
?
เหตุใด __proto__
จึงได้รับการฟื้นฟูบางส่วนและอนุญาตให้ใช้งานใน {...}
แต่ไม่ใช่ในฐานะ getter/setter
นั่นเป็นคำถามที่น่าสนใจ ซึ่งกำหนดให้เราต้องเข้าใจว่าเหตุใด __proto__
จึงไม่ดี
และอีกไม่นานเราคงได้คำตอบ
อย่าเปลี่ยน [[Prototype]]
บนวัตถุที่มีอยู่หากความเร็วเป็นสิ่งสำคัญ
ในทางเทคนิคแล้ว เราสามารถรับ/ตั้งค่า [[Prototype]]
ได้ตลอดเวลา แต่โดยปกติแล้วเราจะตั้งค่ามันเพียงครั้งเดียวในเวลาสร้างวัตถุและจะไม่แก้ไขอีกต่อไป: rabbit
สืบทอดมาจาก animal
และนั่นจะไม่เปลี่ยนแปลง
และเอ็นจิ้น JavaScript ได้รับการปรับให้เหมาะสมที่สุดสำหรับสิ่งนี้ การเปลี่ยนต้นแบบ “ทันที” ด้วย Object.setPrototypeOf
หรือ obj.__proto__=
เป็นการดำเนินการที่ช้ามาก เนื่องจากจะทำให้การปรับให้เหมาะสมภายในสำหรับการดำเนินการเข้าถึงคุณสมบัติของอ็อบเจ็กต์หยุดชะงัก ดังนั้นควรหลีกเลี่ยงมันเว้นแต่คุณจะรู้ว่าคุณกำลังทำอะไรอยู่ ไม่เช่นนั้นความเร็วของ JavaScript ก็ไม่สำคัญสำหรับคุณเลย
ดังที่เราทราบ วัตถุสามารถใช้เป็นอาร์เรย์ที่เชื่อมโยงเพื่อจัดเก็บคู่คีย์/ค่าได้
…แต่ถ้าเราพยายามจัดเก็บคีย์ ที่ผู้ใช้ระบุไว้ ในนั้น (เช่น พจนานุกรมที่ผู้ใช้ป้อน) เราจะเห็นข้อผิดพลาดที่น่าสนใจ: คีย์ทั้งหมดทำงานได้ดียกเว้น "__proto__"
ลองดูตัวอย่าง:
ให้ obj = {}; ให้ key = prompt("กุญแจคืออะไร?", "__proto__"); obj[key] = "ค่าบางอย่าง"; การแจ้งเตือน (obj [คีย์]); // [วัตถุวัตถุ] ไม่ใช่ "ค่าบางอย่าง"!
ที่นี่ หากผู้ใช้พิมพ์ __proto__
การมอบหมายในบรรทัดที่ 4 จะถูกละเว้น!
นั่นอาจเป็นเรื่องที่น่าแปลกใจสำหรับผู้ที่ไม่ใช่นักพัฒนา แต่ก็ค่อนข้างเข้าใจได้สำหรับเรา คุณสมบัติ __proto__
เป็นพิเศษ: จะต้องเป็นวัตถุหรือ null
สตริงไม่สามารถเป็นแบบอย่างได้ นั่นเป็นเหตุผลว่าทำไมการกำหนดสตริงให้กับ __proto__
จึงถูกละเว้น
แต่เราไม่ได้ ตั้งใจ จะนำพฤติกรรมดังกล่าวไปใช้ใช่ไหม? เราต้องการจัดเก็บคู่คีย์/ค่า และคีย์ชื่อ "__proto__"
ได้รับการบันทึกไม่ถูกต้อง นั่นเป็นข้อผิดพลาด!
ผลที่ตามมาที่นี่ไม่น่ากลัว แต่ในกรณีอื่น ๆ เราอาจจัดเก็บอ็อบเจ็กต์แทนสตริงใน obj
จากนั้นต้นแบบก็จะมีการเปลี่ยนแปลงอย่างแน่นอน เป็นผลให้การดำเนินการผิดพลาดไปในทางที่ไม่คาดคิดโดยสิ้นเชิง
ที่แย่กว่านั้นคือโดยปกติแล้วนักพัฒนาจะไม่คิดถึงความเป็นไปได้ดังกล่าวเลย นั่นทำให้สังเกตเห็นจุดบกพร่องได้ยากและยังเปลี่ยนให้กลายเป็นช่องโหว่ โดยเฉพาะอย่างยิ่งเมื่อใช้ JavaScript บนฝั่งเซิร์ฟเวอร์
สิ่งที่ไม่คาดคิดอาจเกิดขึ้นเมื่อกำหนดให้กับ obj.toString
เนื่องจากเป็นเมธอดอ็อบเจ็กต์ในตัว
เราจะหลีกเลี่ยงปัญหานี้ได้อย่างไร?
ขั้นแรก เราสามารถเปลี่ยนไปใช้ Map
เพื่อจัดเก็บข้อมูลแทนวัตถุธรรมดา จากนั้นทุกอย่างจะเรียบร้อยดี:
ให้ map = new Map(); ให้ key = prompt("กุญแจคืออะไร?", "__proto__"); map.set(คีย์ "ค่าบางค่า"); การแจ้งเตือน (map.get (คีย์)); // "ค่าบางอย่าง" (ตามที่ตั้งใจไว้)
…แต่ไวยากรณ์ Object
มักจะดูน่าดึงดูดมากกว่า เนื่องจากมีความกระชับมากกว่า
โชคดีที่เรา สามารถ ใช้วัตถุได้ เพราะผู้สร้างภาษาเคยคิดถึงปัญหานั้นมานานแล้ว
ดังที่เราทราบ __proto__
ไม่ใช่คุณสมบัติของวัตถุ แต่เป็นคุณสมบัติของ accessor ของ Object.prototype
:
ดังนั้น หาก obj.__proto__
ถูกอ่านหรือตั้งค่า getter/setter ที่เกี่ยวข้องจะถูกเรียกจากต้นแบบ และจะได้รับ/sets [[Prototype]]
ตามที่กล่าวไว้ในตอนต้นของส่วนบทช่วยสอนนี้: __proto__
เป็นวิธีการเข้าถึง [[Prototype]]
ไม่ใช่ [[Prototype]]
เอง
ตอนนี้ ถ้าเราตั้งใจที่จะใช้อ็อบเจ็กต์เป็นอาร์เรย์ที่เชื่อมโยงและปราศจากปัญหาดังกล่าว เราสามารถทำได้โดยใช้เคล็ดลับเล็กๆ น้อยๆ:
ให้ obj = Object.create(null); // หรือ: obj = { __proto__: null } ให้ key = prompt("กุญแจคืออะไร?", "__proto__"); obj[key] = "ค่าบางอย่าง"; การแจ้งเตือน (obj [คีย์]); // "ค่าบางอย่าง"
Object.create(null)
สร้างวัตถุว่างโดยไม่มีต้นแบบ ( [[Prototype]]
เป็น null
):
ดังนั้นจึงไม่มี getter/setter ที่สืบทอดมาสำหรับ __proto__
ขณะนี้ได้รับการประมวลผลเป็นคุณสมบัติข้อมูลปกติ ดังนั้นตัวอย่างด้านบนจึงใช้งานได้ดี
เราสามารถเรียกวัตถุดังกล่าวว่า "ธรรมดามาก" หรือ "พจนานุกรมล้วนๆ" ได้ เพราะมันง่ายกว่าวัตถุธรรมดาทั่วไป {...}
ข้อเสียคือวัตถุดังกล่าวขาดวิธีการวัตถุในตัว เช่น toString
:
ให้ obj = Object.create(null); การแจ้งเตือน(obj); // ข้อผิดพลาด (ไม่มี toString)
...แต่นั่นก็เป็นเรื่องปกติสำหรับอาเรย์แบบเชื่อมโยง
โปรดทราบว่าวิธีการที่เกี่ยวข้องกับวัตถุส่วนใหญ่เป็น Object.something(...)
เช่น Object.keys(obj)
ซึ่งไม่ได้อยู่ในต้นแบบ ดังนั้นพวกเขาจะทำงานกับวัตถุดังกล่าวต่อไป:
ให้ chineseDictionary = Object.create(null); chineseDictionary.hello = "คุณ好"; chineseDictionary.bye = "再见"; การแจ้งเตือน (Object.keys (พจนานุกรมภาษาจีน)); //สวัสดี บาย
หากต้องการสร้างวัตถุด้วยต้นแบบที่กำหนด ให้ใช้:
Object.create
มอบวิธีง่ายๆ ในการคัดลอกวัตถุแบบตื้นพร้อมคำอธิบายทั้งหมด:
ให้โคลน = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
ไวยากรณ์ตามตัวอักษร: { __proto__: ... }
อนุญาตให้ระบุคุณสมบัติหลายรายการ
หรือ Object.create(proto[, descriptors]) อนุญาตให้ระบุตัวอธิบายคุณสมบัติ
วิธีการสมัยใหม่ในการรับ/ตั้งค่าต้นแบบคือ:
Object.getPrototypeOf(obj) – ส่งคืน [[Prototype]]
ของ obj
(เหมือนกับ __proto__
getter)
Object.setPrototypeOf(obj, proto) – ตั้งค่า [[Prototype]]
ของ obj
เป็น proto
(เหมือนกับ __proto__
setter)
ไม่แนะนำให้รับ/ตั้งค่าต้นแบบโดยใช้ __proto__
getter/setter ในตัว ตอนนี้อยู่ในภาคผนวก B ของข้อกำหนดแล้ว
นอกจากนี้เรายังครอบคลุมถึงวัตถุที่ไม่มีต้นแบบซึ่งสร้างด้วย Object.create(null)
หรือ {__proto__: null}
ออบเจ็กต์เหล่านี้ใช้เป็นพจนานุกรมเพื่อจัดเก็บคีย์ใดๆ (อาจสร้างโดยผู้ใช้)
โดยปกติแล้ว อ็อบเจ็กต์จะสืบทอดเมธอดในตัวและ __proto__
getter/setter จาก Object.prototype
ทำให้คีย์ที่เกี่ยวข้อง "ถูกครอบครอง" และอาจทำให้เกิดผลข้างเคียงได้ ด้วยต้นแบบที่ null
วัตถุจะว่างเปล่าอย่างแท้จริง
ความสำคัญ: 5
มี object dictionary
ที่สร้างเป็น Object.create(null)
เพื่อจัดเก็บคู่ key/value
ใด ๆ
เพิ่มเมธอด dictionary.toString()
เข้าไป ซึ่งควรส่งคืนรายการคีย์ที่คั่นด้วยเครื่องหมายจุลภาค toString
ของคุณไม่ควรปรากฏใน for..in
เหนือวัตถุ
นี่คือวิธีการทำงาน:
ให้พจนานุกรม = Object.create (null); // รหัสของคุณเพื่อเพิ่มวิธี Dictionary.toString // เพิ่มข้อมูลบางส่วน พจนานุกรม.apple = "แอปเปิล"; พจนานุกรม.__โปรโต__ = "ทดสอบ"; // __proto__ เป็นคีย์คุณสมบัติปกติที่นี่ // มีเพียง apple และ __proto__ เท่านั้นที่อยู่ในลูป สำหรับ (ให้ใส่พจนานุกรม) { การแจ้งเตือน (สำคัญ); // "apple" จากนั้น "__proto__" - // toString ของคุณในการดำเนินการ การแจ้งเตือน (พจนานุกรม); // "แอปเปิ้ล__โปรโต__"
วิธีการนี้สามารถรับคีย์ที่นับได้ทั้งหมดโดยใช้ Object.keys
และส่งออกรายการ
หากต้องการทำให้ toString
ไม่สามารถนับได้ เรามากำหนดมันโดยใช้ตัวอธิบายคุณสมบัติ ไวยากรณ์ของ Object.create
ช่วยให้เราสามารถจัดเตรียมวัตถุที่มีตัวอธิบายคุณสมบัติเป็นอาร์กิวเมนต์ที่สอง
ให้พจนานุกรม = Object.create (null, { toString: { // กำหนดคุณสมบัติ toString value() { // ค่าเป็นฟังก์ชัน กลับ Object.keys (นี้). เข้าร่วม (); - - - พจนานุกรม.apple = "แอปเปิ้ล"; พจนานุกรม.__โปรโต__ = "ทดสอบ"; // apple และ __proto__ อยู่ในลูป สำหรับ (ให้ใส่พจนานุกรม) { การแจ้งเตือน (สำคัญ); // "apple" จากนั้น "__proto__" - // รายการคุณสมบัติที่คั่นด้วยเครื่องหมายจุลภาคโดย toString การแจ้งเตือน (พจนานุกรม); // "แอปเปิ้ล__โปรโต__"
เมื่อเราสร้างคุณสมบัติโดยใช้ descriptor ค่าสถานะจะเป็น false
ตามค่าเริ่มต้น ดังนั้นในโค้ดด้านบน dictionary.toString
จึงไม่สามารถนับได้
ดูบทที่สถานะคุณสมบัติและคำอธิบายสำหรับการตรวจสอบ
ความสำคัญ: 5
มาสร้างวัตถุ rabbit
ใหม่กัน:
ฟังก์ชั่น Rabbit (ชื่อ) { this.name = ชื่อ; - Rabbit.prototype.sayHi = ฟังก์ชั่น () { การแจ้งเตือน (this.name); - ให้กระต่าย = กระต่ายใหม่ ("กระต่าย");
การโทรเหล่านี้ทำสิ่งเดียวกันหรือไม่?
กระต่าย.sayHi(); Rabbit.prototype.sayHi(); Object.getPrototypeOf(กระต่าย).sayHi(); กระต่าย.__โปรโต__.sayHi();
การเรียกครั้งแรกมี this == rabbit
ส่วนการโทรอื่นมี this
เท่ากับ Rabbit.prototype
เพราะจริงๆ แล้วมันคือวัตถุก่อนจุด
ดังนั้นเฉพาะการโทรครั้งแรกเท่านั้นที่แสดง Rabbit
ส่วนการโทรอื่น ๆ จะแสดง undefined
:
ฟังก์ชั่น Rabbit (ชื่อ) { this.name = ชื่อ; - Rabbit.prototype.sayHi = ฟังก์ชั่น () { การแจ้งเตือน( this.name ); - ให้กระต่าย = กระต่ายใหม่ ("กระต่าย"); กระต่าย.sayHi(); // กระต่าย Rabbit.prototype.sayHi(); // ไม่ได้กำหนด Object.getPrototypeOf(กระต่าย).sayHi(); // ไม่ได้กำหนด กระต่าย.__โปรโต__.sayHi(); // ไม่ได้กำหนด