เมื่อเราพัฒนาบางสิ่งบางอย่าง เรามักจะต้องมีคลาสข้อผิดพลาดของเราเองเพื่อสะท้อนถึงสิ่งเฉพาะที่อาจผิดพลาดในงานของเรา สำหรับข้อผิดพลาดในการทำงานของเครือข่าย เราอาจต้องใช้ HttpError
สำหรับการดำเนินการฐานข้อมูล DbError
สำหรับการค้นหาการดำเนินการ NotFoundError
และอื่นๆ
ข้อผิดพลาดของเราควรสนับสนุนคุณสมบัติข้อผิดพลาดพื้นฐาน เช่น message
name
และถ้าจะให้ดี stack
แต่ยังอาจมีคุณสมบัติอื่น ๆ ของตนเองด้วย เช่น อ็อบเจ็กต์ HttpError
อาจมีคุณสมบัติ statusCode
ที่มีค่าเช่น 404
หรือ 403
หรือ 500
JavaScript อนุญาตให้ใช้ throw
กับอาร์กิวเมนต์ใดก็ได้ ดังนั้นในทางเทคนิคแล้วคลาสข้อผิดพลาดที่กำหนดเองของเราจึงไม่จำเป็นต้องสืบทอดจาก Error
แต่ถ้าเราสืบทอดมา มันก็เป็นไปได้ที่จะใช้ obj instanceof Error
เพื่อระบุอ็อบเจ็กต์ที่มีข้อผิดพลาด ดังนั้นจึงเป็นการดีกว่าที่จะสืบทอดจากมัน
เมื่อแอปพลิเคชันเติบโตขึ้น ข้อผิดพลาดของเราเองจะก่อให้เกิดลำดับชั้นตามธรรมชาติ ตัวอย่างเช่น HttpTimeoutError
อาจสืบทอดมาจาก HttpError
และอื่นๆ
ตามตัวอย่าง ลองพิจารณาฟังก์ชัน readUser(json)
ที่ควรอ่าน JSON พร้อมข้อมูลผู้ใช้
ต่อไปนี้เป็นตัวอย่างลักษณะของ json
ที่ถูกต้อง:
ให้ json = `{ "ชื่อ": "จอห์น", "อายุ": 30 }`;
ภายในเราจะใช้ JSON.parse
หากได้รับ json
ที่มีรูปแบบไม่ถูกต้อง ระบบจะพ่น SyntaxError
แต่ถึงแม้ว่า json
จะถูกทางวากยสัมพันธ์ แต่นั่นไม่ได้หมายความว่าเป็นผู้ใช้ที่ถูกต้องใช่ไหม อาจพลาดข้อมูลที่จำเป็น ตัวอย่างเช่น อาจไม่มีคุณสมบัติ name
และ age
ที่จำเป็นสำหรับผู้ใช้ของเรา
ฟังก์ชัน readUser(json)
ของเราจะไม่เพียงแต่อ่าน JSON เท่านั้น แต่ยังตรวจสอบ (“ตรวจสอบ”) ข้อมูลด้วย หากไม่มีช่องที่ต้องกรอกหรือรูปแบบไม่ถูกต้อง แสดงว่าเกิดข้อผิดพลาด และนั่นไม่ใช่ SyntaxError
เนื่องจากข้อมูลมีความถูกต้องทางไวยากรณ์ แต่เป็นข้อผิดพลาดประเภทอื่น เราจะเรียกมันว่า ValidationError
และสร้างคลาสให้กับมัน ข้อผิดพลาดประเภทนั้นควรมีข้อมูลเกี่ยวกับสนามที่ละเมิดด้วย
คลาส ValidationError
ของเราควรสืบทอดมาจากคลาส Error
คลาส Error
มีอยู่แล้วภายใน แต่นี่คือโค้ดโดยประมาณเพื่อให้เราเข้าใจสิ่งที่เรากำลังขยาย:
// "pseudocode" สำหรับคลาส Error ในตัวที่กำหนดโดย JavaScript เอง ข้อผิดพลาดของคลาส { ตัวสร้าง (ข้อความ) { this.message = ข้อความ; this.name = "ข้อผิดพลาด"; // (ชื่อที่แตกต่างกันสำหรับคลาสข้อผิดพลาดในตัวที่แตกต่างกัน) this.stack = <เรียกสแต็ค>; // ไม่ได้มาตรฐาน แต่สภาพแวดล้อมส่วนใหญ่รองรับ - -
ตอนนี้เรามาสืบทอด ValidationError
จากมันแล้วลองใช้งานจริง:
ValidationError คลาสขยายข้อผิดพลาด { ตัวสร้าง (ข้อความ) { ซุปเปอร์(ข้อความ); // (1) this.name = "ข้อผิดพลาดในการตรวจสอบ"; // (2) - - ทดสอบฟังก์ชัน() { โยน ValidationError ใหม่ ("อ๊ะ!"); - พยายาม { ทดสอบ(); } จับ (ผิดพลาด) { การแจ้งเตือน (ข้อผิดพลาดข้อความ); //อุ๊ย! alert(err.name); // การตรวจสอบข้อผิดพลาด การแจ้งเตือน (err.stack); // รายการการโทรแบบซ้อนพร้อมหมายเลขบรรทัดสำหรับแต่ละรายการ -
โปรดทราบ: ในบรรทัด (1)
เราเรียกตัวสร้างหลัก JavaScript กำหนดให้เราต้องเรียก super
ในตัวสร้างลูก ดังนั้นจึงเป็นข้อบังคับ ตัวสร้างหลักตั้งค่าคุณสมบัติ message
ตัวสร้างหลักยังตั้งค่าคุณสมบัติ name
เป็น "Error"
ดังนั้นในบรรทัด (2)
เราจึงรีเซ็ตเป็นค่าที่ถูกต้อง
มาลองใช้มันใน readUser(json)
:
ValidationError คลาสขยายข้อผิดพลาด { ตัวสร้าง (ข้อความ) { ซุปเปอร์(ข้อความ); this.name = "ข้อผิดพลาดในการตรวจสอบ"; - - // การใช้งาน ฟังก์ชั่น readUser (json) { ให้ผู้ใช้ = JSON.parse(json); ถ้า (!user.age) { โยน ValidationError ใหม่ ("ไม่มีฟิลด์: อายุ"); - ถ้า (!user.name) { โยน ValidationError ใหม่ ("ไม่มีฟิลด์: ชื่อ"); - ผู้ใช้ที่กลับมา; - // ตัวอย่างการทำงานด้วย try..catch พยายาม { ให้ผู้ใช้ = readUser('{ "age": 25 }'); } จับ (ผิดพลาด) { ถ้า (ข้อผิดพลาดของ ValidationError) { alert("ข้อมูลไม่ถูกต้อง: " + err.message); // ข้อมูลไม่ถูกต้อง: ไม่มีฟิลด์: ชื่อ } อื่นถ้า (ข้อผิดพลาดอินสแตนซ์ของ SyntaxError) { // (*) alert("ข้อผิดพลาดทางไวยากรณ์ JSON: " + err.message); } อื่น { โยนผิดพลาด; // ข้อผิดพลาดที่ไม่รู้จัก โยนมันใหม่ (**) - -
บล็อก try..catch
ในโค้ดด้านบนจัดการทั้ง ValidationError
และ SyntaxError
ในตัวจาก JSON.parse
โปรดดูวิธีที่เราใช้ instanceof
เพื่อตรวจสอบประเภทข้อผิดพลาดเฉพาะในบรรทัด (*)
เรายังสามารถดู err.name
ได้ด้วย เช่น:
- // แทน (ข้อผิดพลาดอินสแตนซ์ของ SyntaxError) } อื่นถ้า (err.name == "SyntaxError") { // (*) -
เวอร์ชัน instanceof
นั้นดีกว่ามาก เพราะในอนาคตเราจะขยาย ValidationError
สร้างชนิดย่อยของมัน เช่น PropertyRequiredError
และการตรวจสอบ instanceof
จะยังคงทำงานต่อไปสำหรับคลาสที่สืบทอดใหม่ นั่นเป็นข้อพิสูจน์ในอนาคต
สิ่งสำคัญคือหาก catch
พบกับข้อผิดพลาดที่ไม่ทราบสาเหตุ ก็จะทำการโยนมันใหม่ในบรรทัด (**)
catch
block รู้เพียงวิธีจัดการกับข้อผิดพลาดในการตรวจสอบความถูกต้องและไวยากรณ์ ส่วนประเภทอื่นๆ (เกิดจากการพิมพ์ผิดในโค้ดหรือสาเหตุอื่นๆ ที่ไม่ทราบ) ก็น่าจะผ่านพ้นไปได้
คลาส ValidationError
เป็นแบบทั่วไปมาก หลายสิ่งหลายอย่างอาจผิดพลาดได้ คุณสมบัติอาจไม่อยู่หรืออยู่ในรูปแบบที่ไม่ถูกต้อง (เช่น ค่าสตริงสำหรับ age
แทนที่จะเป็นตัวเลข) มาสร้างคลาสที่เป็นรูปธรรมมากขึ้น PropertyRequiredError
สำหรับคุณสมบัติที่ขาดหายไป โดยจะมีข้อมูลเพิ่มเติมเกี่ยวกับทรัพย์สินที่สูญหาย
ValidationError คลาสขยายข้อผิดพลาด { ตัวสร้าง (ข้อความ) { ซุปเปอร์(ข้อความ); this.name = "ข้อผิดพลาดในการตรวจสอบ"; - - คลาส PropertyRequiredError ขยาย ValidationError { ตัวสร้าง (คุณสมบัติ) { super("ไม่มีคุณสมบัติ: " + คุณสมบัติ); this.name = "PropertyRequiredError"; this.property = คุณสมบัติ; - - // การใช้งาน ฟังก์ชั่น readUser (json) { ให้ผู้ใช้ = JSON.parse(json); ถ้า (!user.age) { โยน PropertyRequiredError ใหม่ ("อายุ"); - ถ้า (!user.name) { โยน PropertyRequiredError ใหม่ ("ชื่อ"); - ผู้ใช้ที่กลับมา; - // ตัวอย่างการทำงานด้วย try..catch พยายาม { ให้ผู้ใช้ = readUser('{ "age": 25 }'); } จับ (ผิดพลาด) { ถ้า (ข้อผิดพลาดของ ValidationError) { alert("ข้อมูลไม่ถูกต้อง: " + err.message); // ข้อมูลไม่ถูกต้อง: ไม่มีคุณสมบัติ: ชื่อ alert(err.name); // PropertyRequiredError การแจ้งเตือน (ข้อผิดพลาดคุณสมบัติ); // ชื่อ } อื่นถ้า (ข้อผิดพลาดอินสแตนซ์ของ SyntaxError) { alert("ข้อผิดพลาดทางไวยากรณ์ JSON: " + err.message); } อื่น { โยนผิดพลาด; // ข้อผิดพลาดที่ไม่ทราบสาเหตุ, โยนมันใหม่ - -
คลาสใหม่ PropertyRequiredError
นั้นใช้งานง่าย: เราเพียงแต่ต้องส่งชื่อคุณสมบัติ: new PropertyRequiredError(property)
message
ที่มนุษย์สามารถอ่านได้ถูกสร้างขึ้นโดยตัวสร้าง
โปรดทราบว่า this.name
ในตัวสร้าง PropertyRequiredError
ถูกกำหนดด้วยตนเองอีกครั้ง นั่นอาจจะดูน่าเบื่อสักหน่อย – ในการกำหนด this.name = <class name>
ในทุกคลาสข้อผิดพลาดที่กำหนดเอง เราสามารถหลีกเลี่ยงมันได้ด้วยการสร้างคลาส “ข้อผิดพลาดพื้นฐาน” ของเราเอง ซึ่งกำหนด this.name = this.constructor.name
จากนั้นรับช่วงข้อผิดพลาดที่เรากำหนดเองทั้งหมดจากข้อผิดพลาดดังกล่าว
ลองเรียกมันว่า MyError
นี่คือโค้ดที่มี MyError
และคลาสข้อผิดพลาดแบบกำหนดเองอื่นๆ ที่เรียบง่าย:
คลาส MyError ขยายข้อผิดพลาด { ตัวสร้าง (ข้อความ) { ซุปเปอร์(ข้อความ); this.name = this.constructor.name; - - คลาส ValidationError ขยาย MyError { } คลาส PropertyRequiredError ขยาย ValidationError { ตัวสร้าง (คุณสมบัติ) { super("ไม่มีคุณสมบัติ: " + คุณสมบัติ); this.property = คุณสมบัติ; - - //ชื่อถูกต้อง alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
ขณะนี้ข้อผิดพลาดแบบกำหนดเองสั้นลงมาก โดยเฉพาะ ValidationError
เนื่องจากเรากำจัดบรรทัด "this.name = ..."
ในตัวสร้างแล้ว
วัตถุประสงค์ของฟังก์ชัน readUser
ในโค้ดด้านบนคือ "เพื่ออ่านข้อมูลผู้ใช้" อาจมีข้อผิดพลาดประเภทต่างๆ เกิดขึ้นในกระบวนการ ขณะนี้เรามี SyntaxError
และ ValidationError
แต่ในอนาคตฟังก์ชัน readUser
อาจเพิ่มขึ้นและอาจสร้างข้อผิดพลาดประเภทอื่นๆ
รหัสที่เรียก readUser
ควรจัดการกับข้อผิดพลาดเหล่านี้ ตอนนี้มันใช้ if
s หลายตัวใน catch
block ซึ่งจะตรวจสอบคลาสและจัดการกับข้อผิดพลาดที่ทราบ และโยนข้อผิดพลาดที่ไม่รู้จักอีกครั้ง
โครงการเป็นดังนี้:
พยายาม { - readUser() // แหล่งที่มาของข้อผิดพลาดที่อาจเกิดขึ้น - } จับ (ผิดพลาด) { ถ้า (ข้อผิดพลาดของ ValidationError) { // จัดการข้อผิดพลาดในการตรวจสอบ } อื่นถ้า (ข้อผิดพลาดอินสแตนซ์ของ SyntaxError) { // จัดการข้อผิดพลาดทางไวยากรณ์ } อื่น { โยนผิดพลาด; // ข้อผิดพลาดที่ไม่ทราบสาเหตุ, โยนมันใหม่ - -
ในโค้ดด้านบน เราจะเห็นข้อผิดพลาดสองประเภท แต่อาจมีมากกว่านั้น
หากฟังก์ชัน readUser
สร้างข้อผิดพลาดหลายประเภท เราควรถามตัวเองว่า: เราต้องการตรวจสอบข้อผิดพลาดทุกประเภททีละรายการทุกครั้งหรือไม่
บ่อยครั้งคำตอบคือ “ไม่”: เราต้องการที่จะ “อยู่เหนือสิ่งอื่นใด” เราแค่อยากทราบว่ามี "ข้อผิดพลาดในการอ่านข้อมูล" หรือไม่ เพราะเหตุใดจึงมักไม่เกี่ยวข้องกัน (ข้อความแสดงข้อผิดพลาดอธิบายไว้) หรือดีไปกว่านั้น เราต้องการมีวิธีรับรายละเอียดข้อผิดพลาดแต่เฉพาะในกรณีที่จำเป็นเท่านั้น
เทคนิคที่เราอธิบายในที่นี้เรียกว่า "การยกเว้นข้อยกเว้น"
เราจะสร้างคลาส ReadError
ใหม่เพื่อแสดงข้อผิดพลาด "การอ่านข้อมูล" ทั่วไป
ฟังก์ชัน readUser
จะตรวจจับข้อผิดพลาดในการอ่านข้อมูลที่เกิดขึ้นภายใน เช่น ValidationError
และ SyntaxError
และสร้าง ReadError
แทน
วัตถุ ReadError
จะเก็บการอ้างอิงถึงข้อผิดพลาดดั้งเดิมไว้ในคุณสมบัติ cause
จากนั้นโค้ดที่เรียก readUser
จะต้องตรวจสอบ ReadError
เท่านั้น ไม่ใช่ข้อผิดพลาดในการอ่านข้อมูลทุกประเภท และหากต้องการรายละเอียดเพิ่มเติมเกี่ยวกับข้อผิดพลาด ก็สามารถตรวจสอบคุณสมบัติของ cause
ได้
นี่คือโค้ดที่กำหนด ReadError
และสาธิตการใช้งานใน readUser
และ try..catch
:
คลาส ReadError ขยายข้อผิดพลาด { ตัวสร้าง (ข้อความสาเหตุ) { ซุปเปอร์(ข้อความ); this.cause = สาเหตุ; this.name = 'ReadError'; - - ValidationError คลาสขยายข้อผิดพลาด { /*...*/ } คลาส PropertyRequiredError ขยาย ValidationError { /* ... */ } ฟังก์ชั่น validateUser (ผู้ใช้) { ถ้า (!user.age) { โยน PropertyRequiredError ใหม่ ("อายุ"); - ถ้า (!user.name) { โยน PropertyRequiredError ใหม่ ("ชื่อ"); - - ฟังก์ชั่น readUser (json) { ให้ผู้ใช้; พยายาม { ผู้ใช้ = JSON.parse(json); } จับ (ผิดพลาด) { ถ้า (ข้อผิดพลาดอินสแตนซ์ของ SyntaxError) { โยน ReadError ใหม่ ("ข้อผิดพลาดทางไวยากรณ์", ผิดพลาด); } อื่น { โยนผิดพลาด; - - พยายาม { ตรวจสอบผู้ใช้(ผู้ใช้); } จับ (ผิดพลาด) { ถ้า (ข้อผิดพลาดของ ValidationError) { โยน ReadError ใหม่ ("ข้อผิดพลาดในการตรวจสอบความถูกต้อง", ผิดพลาด); } อื่น { โยนผิดพลาด; - - - พยายาม { readUser('{json ไม่ถูกต้อง}'); } จับ (e) { ถ้า (อินสแตนซ์ของ ReadError) { การแจ้งเตือน (e); // ข้อผิดพลาดเดิม: SyntaxError: โทเค็นที่ไม่คาดคิด b ใน JSON ที่ตำแหน่ง 1 alert("ข้อผิดพลาดเดิม: " + e.cause); } อื่น { โยนอี; - -
ในโค้ดด้านบน readUser
ทำงานตรงตามที่อธิบายไว้ - จับข้อผิดพลาดทางไวยากรณ์และการตรวจสอบและส่งข้อผิดพลาด ReadError
แทน (ข้อผิดพลาดที่ไม่รู้จักจะถูกโยนทิ้งใหม่ตามปกติ)
ดังนั้นโค้ดภายนอกจะตรวจสอบ instanceof ReadError
เท่านี้ก็เรียบร้อย ไม่จำเป็นต้องแสดงรายการประเภทข้อผิดพลาดที่เป็นไปได้ทั้งหมด
วิธีการนี้เรียกว่า "การตัดข้อยกเว้น" เนื่องจากเราใช้ข้อยกเว้น "ระดับต่ำ" และ "รวม" ลงใน ReadError
ที่เป็นนามธรรมมากกว่า มีการใช้กันอย่างแพร่หลายในการเขียนโปรแกรมเชิงวัตถุ
เราสามารถสืบทอดจาก Error
และคลาสข้อผิดพลาดในตัวอื่นๆ ได้ตามปกติ เราแค่ต้องดูแลทรัพย์สิน name
และอย่าลืมเรียก super
เราสามารถใช้ instanceof
เพื่อตรวจสอบข้อผิดพลาดเฉพาะได้ มันยังทำงานร่วมกับมรดกอีกด้วย แต่บางครั้งเรามีออบเจ็กต์ข้อผิดพลาดที่มาจากไลบรารีบุคคลที่สาม และไม่มีวิธีง่ายๆ ในการได้รับคลาสนั้น จากนั้นคุณสมบัติ name
สามารถนำมาใช้สำหรับการตรวจสอบดังกล่าวได้
การตัดข้อยกเว้นเป็นเทคนิคที่ใช้กันอย่างแพร่หลาย: ฟังก์ชันจะจัดการกับข้อยกเว้นระดับต่ำและสร้างข้อผิดพลาดในระดับที่สูงกว่าแทนที่จะเป็นข้อผิดพลาดระดับต่ำต่างๆ ข้อยกเว้นระดับต่ำบางครั้งอาจกลายเป็นคุณสมบัติของออบเจ็กต์นั้น เช่น err.cause
ในตัวอย่างข้างต้น แต่นั่นไม่ได้บังคับอย่างเคร่งครัด
ความสำคัญ: 5
สร้างคลาส FormatError
ที่สืบทอดมาจากคลาส SyntaxError
ที่มีอยู่แล้วภายใน
ควรรองรับคุณสมบัติ message
name
และ stack
ตัวอย่างการใช้งาน:
ให้ข้อผิดพลาด = new FormatError("ข้อผิดพลาดในการจัดรูปแบบ"); การแจ้งเตือน (ข้อผิดพลาดข้อความ); // ข้อผิดพลาดในการจัดรูปแบบ การแจ้งเตือน (ผิดพลาดชื่อ); // FormatError การแจ้งเตือน (err.stack); // กอง การแจ้งเตือน (ข้อผิดพลาดของ FormatError ); // จริง การแจ้งเตือน (ข้อผิดพลาดของ SyntaxError ); // จริง (เพราะสืบทอดมาจาก SyntaxError)
คลาส FormatError ขยาย SyntaxError { ตัวสร้าง (ข้อความ) { ซุปเปอร์(ข้อความ); this.name = this.constructor.name; - - ให้ข้อผิดพลาด = new FormatError("ข้อผิดพลาดในการจัดรูปแบบ"); การแจ้งเตือน (ข้อผิดพลาดข้อความ); // ข้อผิดพลาดในการจัดรูปแบบ การแจ้งเตือน (ผิดพลาดชื่อ); // FormatError การแจ้งเตือน (err.stack); // กอง การแจ้งเตือน (ข้อผิดพลาดของ SyntaxError ); // จริง