Buggy JavaScript Code: 10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา JavaScript สร้าง

เผยแพร่แล้ว: 2022-03-11

ปัจจุบัน JavaScript เป็นแกนหลักของเว็บแอปพลิเคชันสมัยใหม่แทบทั้งหมด โดยเฉพาะอย่างยิ่งในช่วงหลายปีที่ผ่านมาได้เห็นการเพิ่มขึ้นของไลบรารีและเฟรมเวิร์กที่ใช้ JavaScript อันทรงพลังจำนวนมากสำหรับการพัฒนาแอปพลิเคชันหน้าเดียว (SPA) กราฟิกและแอนิเมชั่น และแม้แต่แพลตฟอร์ม JavaScript ฝั่งเซิร์ฟเวอร์ JavaScript ได้กลายเป็นที่แพร่หลายอย่างแท้จริงในโลกของการพัฒนาเว็บแอป ดังนั้นจึงเป็นทักษะที่สำคัญมากขึ้นในการเป็นผู้เชี่ยวชาญ

ในตอนแรกหน้าแดง JavaScript อาจดูค่อนข้างง่าย และแน่นอน การสร้างฟังก์ชันพื้นฐานของ JavaScript ลงในหน้าเว็บเป็นงานที่ค่อนข้างตรงไปตรงมาสำหรับนักพัฒนาซอฟต์แวร์ที่มีประสบการณ์ แม้ว่าจะยังใหม่กับ JavaScript ก็ตาม ทว่าภาษานั้นมีความเหมาะสมยิ่งยวด ทรงพลัง และซับซ้อนกว่าที่ใครจะนำไปสู่การเชื่อในตอนแรก อันที่จริง รายละเอียดปลีกย่อยจำนวนมากของ JavaScript นำไปสู่ปัญหาทั่วไปหลายประการที่ทำให้ไม่สามารถใช้งานได้ - 10 ข้อที่เราพูดถึงในที่นี้ - ซึ่งเป็นสิ่งสำคัญที่ต้องระวังและหลีกเลี่ยงในการแสวงหาที่จะเป็นนักพัฒนา JavaScript ที่เชี่ยวชาญ

ข้อผิดพลาดทั่วไป #1: การอ้างอิงถึง this ไม่ถูกต้อง

ฉันเคยได้ยินนักแสดงตลกพูดว่า:

ฉันไม่ได้อยู่ที่นี่จริงๆ เพราะสิ่งที่อยู่ที่นี่ นอกจากที่นั่น ไม่มี 't'?

เรื่องตลกดังกล่าวได้อธิบายลักษณะความสับสนที่มักเกิดขึ้นสำหรับนักพัฒนาซอฟต์แวร์เกี่ยวกับคีย์เวิร์ด this ของ JavaScript ฉันหมายความว่านี่เป็น this จริงๆหรือเป็นอย่างอื่นทั้งหมด? หรือไม่ได้กำหนดไว้?

เนื่องจากเทคนิคการเข้ารหัส JavaScript และรูปแบบการออกแบบมีความซับซ้อนมากขึ้นเรื่อยๆ ในช่วงหลายปีที่ผ่านมา ขอบเขตการอ้างอิงตนเองเพิ่มขึ้นตามลำดับภายในการเรียกกลับและการปิด ซึ่งเป็นที่มาของ "ความสับสนนี้/นี้"

พิจารณาข้อมูลโค้ดตัวอย่างนี้:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };

การดำเนินการโค้ดข้างต้นส่งผลให้เกิดข้อผิดพลาดต่อไปนี้:

 Uncaught TypeError: undefined is not a function

ทำไม?

มันคือทั้งหมดที่เกี่ยวกับบริบท สาเหตุที่คุณได้รับข้อผิดพลาดข้างต้นเป็นเพราะเมื่อคุณเรียกใช้ setTimeout() คุณกำลังเรียกใช้ window.setTimeout() จริงๆ ด้วยเหตุนี้ ฟังก์ชันที่ไม่ระบุชื่อที่ส่งผ่านไปยัง setTimeout() จึงถูกกำหนดในบริบทของวัตถุ window ซึ่งไม่มี clearBoard()

วิธีแก้ปัญหาแบบดั้งเดิมที่เข้ากันได้กับเบราว์เซอร์เก่าคือการบันทึกการอ้างอิงของคุณไปยังตัวแปร this ในตัวแปรที่ปิดแล้วสามารถสืบทอดได้ เช่น:

 Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };

หรือในเบราว์เซอร์ที่ใหม่กว่า คุณสามารถใช้เมธอด bind() เพื่อส่งผ่านข้อมูลอ้างอิงที่เหมาะสม:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };

ข้อผิดพลาดทั่วไป #2: คิดว่ามีขอบเขตระดับบล็อก

ตามที่กล่าวไว้ในคู่มือการจ้างงาน JavaScript แหล่งที่มาทั่วไปของความสับสนในหมู่นักพัฒนา JavaScript (และจึงเป็นที่มาของข้อบกพร่องทั่วไป) ถือว่า JavaScript สร้างขอบเขตใหม่สำหรับแต่ละบล็อกโค้ด แม้ว่าสิ่งนี้จะเป็นความจริงในภาษาอื่นๆ มากมาย แต่ก็ ไม่ เป็นความจริงใน JavaScript พิจารณาตัวอย่างเช่นรหัสต่อไปนี้:

 for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?

หากคุณเดาว่าการเรียก console.log() อาจเป็นเอาต์พุตที่ undefined หรือแสดงข้อผิดพลาด แสดงว่าคุณเดาผิด เชื่อหรือไม่ มันจะส่งออก 10 . ทำไม?

ในภาษาอื่นๆ ส่วนใหญ่ โค้ดด้านบนจะทำให้เกิดข้อผิดพลาดเนื่องจาก "ชีวิต" (เช่น ขอบเขต) ของตัวแปร i จะถูกจำกัดให้อยู่ในบล็อก for อย่างไรก็ตาม ใน JavaScript นี่ไม่ใช่กรณีและตัวแปร i ยังคงอยู่ในขอบเขตแม้ว่าลูป for จะเสร็จสิ้น โดยคงค่าสุดท้ายไว้หลังจากออกจากลูป (พฤติกรรมนี้เป็นที่ทราบกันโดยบังเอิญว่าเป็นการชักรอกแบบแปรผัน)

อย่างไรก็ตาม เป็นที่น่าสังเกตว่าการรองรับขอบเขตระดับบล็อก กำลัง เข้าสู่ JavaScript ผ่านคีย์เวิร์ด let ใหม่ คำหลัก let มีอยู่แล้วใน JavaScript 1.7 และถูกกำหนดให้เป็นคำหลัก JavaScript ที่ได้รับการสนับสนุนอย่างเป็นทางการตั้งแต่ ECMAScript 6

ใหม่กับ JavaScript? อ่านเกี่ยวกับขอบเขต ต้นแบบ และอื่นๆ

ข้อผิดพลาดทั่วไป #3: การสร้างหน่วยความจำรั่ว

การรั่วไหลของหน่วยความจำเป็นปัญหา JavaScript ที่หลีกเลี่ยงไม่ได้หากคุณไม่ได้เข้ารหัสอย่างมีสติเพื่อหลีกเลี่ยงปัญหาเหล่านี้ มีหลายวิธีที่จะเกิดขึ้น ดังนั้นเราจะเน้นให้เห็นถึงเหตุการณ์ที่พบบ่อยๆ ของพวกเขาสองสามอย่าง

ตัวอย่างการรั่วไหลของหน่วยความจำ 1: การอ้างอิงถึงวัตถุที่หมดอายุ

พิจารณารหัสต่อไปนี้:

 var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second

หากคุณเรียกใช้โค้ดด้านบนและตรวจสอบการใช้หน่วยความจำ คุณจะพบว่ามีหน่วยความจำรั่วจำนวนมาก ทำให้มีการรั่วไหลเต็มเมกะไบต์ต่อวินาที! และแม้แต่ GC แบบแมนนวลก็ไม่ได้ช่วยอะไร ดูเหมือนว่าเราจะรั่ว longStr ทุกครั้งที่มีการเรียกใช้ replaceThing แต่ทำไม?

ลองตรวจสอบสิ่งต่าง ๆ ในรายละเอียดเพิ่มเติม:

แต่ละอ็อบเจ็กต์ theThing มีออบเจ็กต์ longStr 1MB ของตัวเอง ทุกวินาที เมื่อเราเรียก replaceThing มันจะยึดตามการอ้างอิงไปยังอ็อบเจกต์ theThing ก่อนหน้าใน priorThing แต่เรายังคงไม่คิดว่านี่จะเป็นปัญหา เนื่องจากในแต่ละครั้ง priorThing ที่อ้างอิงก่อนหน้านี้จะถูกยกเลิกการอ้างอิง (เมื่อ priorThing ถูกรีเซ็ตผ่าน priorThing = theThing; ) และยิ่งไปกว่านั้น อ้างอิงเฉพาะในเนื้อหาหลักของ replaceThing และในฟังก์ชันที่ unused ซึ่งอันที่จริง ไม่เคยใช้เลย

เราก็เลยสงสัยว่าทำไมหน่วยความจำถึงรั่วที่นี่!?

เพื่อให้เข้าใจถึงสิ่งที่เกิดขึ้น เราต้องเข้าใจมากขึ้นว่าสิ่งต่าง ๆ ทำงานอย่างไรใน JavaScript ภายใต้ประทุน วิธีทั่วไปในการดำเนินการปิดคือทุกอ็อบเจ็กต์ฟังก์ชันมีลิงก์ไปยังอ็อบเจ็กต์สไตล์พจนานุกรมที่แสดงถึงขอบเขตคำศัพท์ หากทั้งสองฟังก์ชันกำหนดไว้ภายใน replaceThing ใช้ priorThing จริง ๆ มันเป็นสิ่งสำคัญที่พวกเขาทั้งสองจะได้รับอ็อบเจกต์เดียวกัน แม้ว่า priorThing จะได้รับมอบหมายซ้ำแล้วซ้ำอีก ดังนั้น ฟังก์ชันทั้งสองจึงใช้สภาพแวดล้อมคำศัพท์เดียวกัน แต่ทันทีที่ตัวแปรถูกใช้โดยการปิดใดๆ ตัวแปรนั้นจะจบลงในสภาพแวดล้อมคำศัพท์ที่แชร์โดยการปิดทั้งหมดในขอบเขตนั้น และความแตกต่างเล็กน้อยนั้นคือสิ่งที่นำไปสู่การรั่วไหลของหน่วยความจำที่น่ากลัวนี้ (รายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้มีอยู่ที่นี่)

ตัวอย่างการรั่วไหลของหน่วยความจำ 2: การอ้างอิงแบบวงกลม

พิจารณาส่วนย่อยของรหัสนี้:

 function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }

ที่นี่ onClick มีการปิดซึ่งอ้างอิงถึง element (ผ่าน element.nodeName ) ด้วยการกำหนด onClick ให้กับ element.click การอ้างอิงแบบวงกลมจะถูกสร้างขึ้น เช่น: element -> onClick -> element -> onClick -> element ...

น่าสนใจ แม้ว่า element จะถูกลบออกจาก DOM การอ้างอิงตนเองแบบวงกลมด้านบนจะป้องกันไม่ให้ element และ onClick ถูกรวบรวม และด้วยเหตุนี้ หน่วยความจำรั่ว

การหลีกเลี่ยงการรั่วไหลของหน่วยความจำ: สิ่งที่คุณต้องรู้

การจัดการหน่วยความจำของ JavaScript (และโดยเฉพาะการรวบรวมขยะ) ส่วนใหญ่ขึ้นอยู่กับแนวคิดของการเข้าถึงวัตถุ

ออบเจ็กต์ต่อไปนี้จะถือว่า สามารถเข้าถึงได้ และเรียกว่า "ราก":

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

อ็อบเจ็กต์จะถูกเก็บไว้ในหน่วยความจำอย่างน้อยตราบเท่าที่สามารถเข้าถึงได้จากรูทใด ๆ ผ่านการอ้างอิงหรือห่วงโซ่ของการอ้างอิง

มี Garbage Collector (GC) ในเบราว์เซอร์ซึ่งล้างหน่วยความจำที่ถูกครอบครองโดยวัตถุที่ไม่สามารถเข้าถึงได้ กล่าวคือ วัตถุจะถูกลบออกจากหน่วยความจำ ก็ต่อเมื่อ GC เชื่อว่าไม่สามารถเข้าถึงได้ น่าเสียดายที่มันค่อนข้างง่ายที่จะจบลงด้วยวัตถุ "ซอมบี้" ที่เลิกใช้แล้วซึ่งอันที่จริงไม่ได้ใช้งานแล้ว แต่ GC ยังคงคิดว่า "เข้าถึงได้"

ที่เกี่ยวข้อง: JavaScript Best Practices and Tips โดย Toptal Developers

ข้อผิดพลาดทั่วไป #4: ความสับสนเกี่ยวกับความเท่าเทียมกัน

ความสะดวกอย่างหนึ่งใน JavaScript คือจะบังคับค่าใดๆ ที่ถูกอ้างอิงในบริบทบูลีนให้เป็นค่าบูลีนโดยอัตโนมัติ แต่มีบางกรณีที่อาจทำให้สับสนได้ตามสะดวก ตัวอย่างเช่น บางส่วนต่อไปนี้เป็นที่รู้กันว่ากัดผู้พัฒนา JavaScript จำนวนมาก:

 // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...

เกี่ยวกับสองรายการสุดท้ายแม้จะว่างเปล่า (ซึ่งอาจทำให้คนเชื่อว่าพวกเขาจะประเมิน false ) ทั้ง {} และ [] เป็นวัตถุจริงและวัตถุ ใด ๆ จะถูกบังคับให้มีค่าบูลีน true ใน JavaScript สอดคล้องกับข้อกำหนด ECMA-262

ดังที่ตัวอย่างเหล่านี้แสดงให้เห็น บางครั้งกฎการบังคับประเภทก็ชัดเจนเหมือนโคลน ดังนั้น เว้นแต่ต้องการการบีบบังคับแบบชัดแจ้ง โดยทั่วไปแล้วควรใช้ === และ !== (แทนที่จะเป็น == และ != ) เพื่อหลีกเลี่ยงผลข้างเคียงที่ไม่ได้ตั้งใจจากการบีบบังคับ ( == และ != ทำการแปลงประเภทโดยอัตโนมัติเมื่อเปรียบเทียบสองสิ่ง ในขณะที่ === และ !== ทำการเปรียบเทียบแบบเดียวกันโดยไม่มีการแปลงประเภท)

และเป็นจุดด้านข้างอย่างสมบูรณ์ – แต่เนื่องจากเรากำลังพูดถึงการบีบบังคับและการเปรียบเทียบประเภท – จึงควรค่าแก่การกล่าวถึงว่าการเปรียบเทียบ NaN กับ อะไรก็ได้ (แม้แต่ NaN !) จะส่งกลับ false เสมอ คุณจึงไม่สามารถใช้ตัวดำเนินการความเท่าเทียมกัน ( == , === , != , !== ) เพื่อกำหนดว่าค่าเป็น NaN หรือไม่ ให้ใช้ฟังก์ชัน isNaN() ในตัวแทน:

 console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true

ข้อผิดพลาดทั่วไป #5: การจัดการ DOM ที่ไม่มีประสิทธิภาพ

JavaScript ทำให้ง่ายต่อการจัดการ DOM (เช่น เพิ่ม แก้ไข และลบองค์ประกอบ) แต่ไม่ดำเนินการใดๆ เพื่อส่งเสริมการดำเนินการอย่างมีประสิทธิภาพ

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

ทางเลือกหนึ่งที่มีประสิทธิภาพเมื่อต้องเพิ่มองค์ประกอบ DOM หลายรายการคือการใช้ส่วนย่อยของเอกสารแทน ซึ่งจะช่วยปรับปรุงทั้งประสิทธิภาพและประสิทธิภาพ

ตัวอย่างเช่น:

 var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));

นอกเหนือจากประสิทธิภาพที่ได้รับการปรับปรุงโดยเนื้อแท้ของแนวทางนี้ การสร้างองค์ประกอบ DOM ที่แนบมานั้นมีราคาแพง ในขณะที่การสร้างและปรับเปลี่ยนองค์ประกอบในขณะที่แยกออกแล้วแนบองค์ประกอบเหล่านั้นจะให้ประสิทธิภาพที่ดีขึ้นมาก

ข้อผิดพลาดทั่วไป #6: การใช้คำจำกัดความฟังก์ชันภายใน for ลูปไม่ถูกต้อง

พิจารณารหัสนี้:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }

จากโค้ดข้างต้น หากมีองค์ประกอบอินพุต 10 รายการ การคลิกองค์ประกอบ ใดๆ จะแสดง "นี่คือองค์ประกอบ #10"! เนื่องจากเมื่อถึงเวลาที่ onclick ถูกเรียกใช้สำหรับองค์ประกอบ ใด ๆ การวนซ้ำด้านบนจะเสร็จสิ้นและค่าของ i จะเป็น 10 แล้ว (สำหรับองค์ประกอบ ทั้งหมด )

ต่อไปนี้คือวิธีที่เราสามารถแก้ไขปัญหาโค้ดด้านบนได้ เพื่อให้ได้พฤติกรรมที่ต้องการ:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }

ในโค้ดเวอร์ชันปรับปรุงนี้ makeHandler จะดำเนินการทันทีทุกครั้งที่เราผ่านลูป แต่ละครั้งจะได้รับค่า i+1 ที่เป็นปัจจุบันในขณะนั้นและผูกกับตัวแปร num ที่กำหนดขอบเขต ฟังก์ชันภายนอกส่งคืนฟังก์ชันภายใน (ซึ่งยังใช้ตัวแปร num ที่กำหนดขอบเขตนี้ด้วย) และ onclick ขององค์ประกอบถูกตั้งค่าเป็นฟังก์ชันภายในนั้น เพื่อให้แน่ใจว่าแต่ละ onclick ได้รับและใช้ค่า i ที่เหมาะสม (ผ่านตัวแปร num ที่กำหนดขอบเขต)

ข้อผิดพลาดทั่วไป #7: ความล้มเหลวในการใช้ประโยชน์จากมรดกต้นแบบ

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

นี่เป็นตัวอย่างง่ายๆ พิจารณารหัสนี้:

 BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };

ดูเหมือนค่อนข้างตรงไปตรงมา หากคุณระบุชื่อ ให้ใช้ชื่อนั้น มิฉะนั้น ให้ตั้งชื่อเป็น 'ค่าเริ่มต้น' เช่น:

 var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'

แต่ถ้าเราทำสิ่งนี้:

 delete secondObj.name;

เราจะได้:

 console.log(secondObj.name); // -> Results in 'undefined'

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

 BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';

ด้วยเวอร์ชันนี้ BaseObject สืบทอดคุณสมบัติ name จากวัตถุ prototype โดยที่มันถูกตั้งค่า (โดยค่าเริ่มต้น) เป็น 'default' ดังนั้น หากคอนสตรัคเตอร์ถูกเรียกโดยไม่มีชื่อ ชื่อจะมีค่าเริ่มต้นเป็น default และในทำนองเดียวกัน หากคุณสมบัติ name ถูกลบออกจากอินสแตนซ์ของ BaseObject เชนต้นแบบจะถูกค้นหาและคุณสมบัติ name จะถูกดึงมาจากอ็อบเจ็กต์ prototype ซึ่งค่ายังคงเป็น 'default' ตอนนี้เราได้รับ:

 var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'

ข้อผิดพลาดทั่วไป #8: การสร้างการอ้างอิงถึงวิธีการอินสแตนซ์ที่ไม่ถูกต้อง

มากำหนดวัตถุอย่างง่าย และสร้าง และตัวอย่างของวัตถุดังนี้:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();

เพื่อความสะดวก เรามาสร้างการอ้างอิงถึงเมธอด whoAmI กัน สันนิษฐานว่าเราสามารถเข้าถึงได้โดย whoAmI() แทนที่จะเป็น obj.whoAmI() ที่ยาวกว่า :

 var whoAmI = obj.whoAmI;

และเพื่อให้แน่ใจว่าทุกอย่างดูเข้ากัน มาพิมพ์ค่าของตัวแปร whoAmI ใหม่ของเรากัน:

 console.log(whoAmI);

ผลลัพธ์:

 function () { console.log(this === window ? "window" : "MyObj"); }

โอเคดี. ดูดี

แต่ตอนนี้ ดูความแตกต่างเมื่อเราเรียกใช้ obj.whoAmI() กับการอ้างอิงที่สะดวกของเรา whoAmI() :

 obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)

เกิดอะไรขึ้น?

หัวปลอมที่นี่คือเมื่อเราทำมอบหมาย var whoAmI = obj.whoAmI; ตัวแปรใหม่ whoAmI ถูกกำหนดในเนมสเปซ ส่วนกลาง เป็นผลให้ค่าของ this คือ window ไม่ใช่ อินสแตนซ์ obj ของ MyObject !

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

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)

ข้อผิดพลาดทั่วไป #9: ระบุสตริงเป็นอาร์กิวเมนต์แรกสำหรับ setTimeout หรือ setInterval

สำหรับผู้เริ่มต้น มาทำความเข้าใจกันให้ชัดเจน: การระบุสตริงเป็นอาร์กิวเมนต์แรกสำหรับ setTimeout หรือ setInterval ไม่ใช่ ความผิดพลาดในตัวเอง เป็นโค้ด JavaScript ที่ถูกต้องตามกฎหมายอย่างสมบูรณ์ ประเด็นนี้เป็นอีกประเด็นหนึ่งของประสิทธิภาพและประสิทธิผล สิ่งที่ไม่ค่อยอธิบายคือ ภายใต้ประทุน หากคุณส่งสตริงเป็นอาร์กิวเมนต์แรกไปยัง setTimeout หรือ setInterval สตริงนั้นจะถูกส่งต่อไปยังตัว สร้างฟังก์ชัน เพื่อแปลงเป็นฟังก์ชันใหม่ กระบวนการนี้อาจช้าและไม่มีประสิทธิภาพ และแทบไม่มีความจำเป็น

ทางเลือกอื่นในการส่งสตริงเป็นอาร์กิวเมนต์แรกของเมธอดเหล่านี้คือส่งผ่าน ฟังก์ชัน แทน ลองมาดูตัวอย่างกัน

ในที่นี้จะเป็นการใช้ setInterval และ setTimeout โดยทั่วไปโดยส่ง สตริง เป็นพารามิเตอร์แรก:

 setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);

ทางเลือกที่ดีกว่าคือส่งผ่าน ฟังก์ชัน เป็นอาร์กิวเมนต์เริ่มต้น เช่น:

 setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);

ข้อผิดพลาดทั่วไป #10: ความล้มเหลวในการใช้ “โหมดเข้มงวด”

ตามที่อธิบายไว้ในคู่มือการจ้างงาน JavaScript “โหมดเข้มงวด” (เช่น รวมถึง 'use strict'; ที่จุดเริ่มต้นของไฟล์ต้นทาง JavaScript ของคุณ) เป็นวิธีการบังคับใช้การแยกวิเคราะห์ที่เข้มงวดยิ่งขึ้นและการจัดการข้อผิดพลาดในโค้ด JavaScript ของคุณในขณะใช้งานจริงด้วย เนื่องจากทำให้ปลอดภัยยิ่งขึ้น

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

ต่อไปนี้คือประโยชน์หลักบางประการของโหมดเข้มงวด:

  • ทำให้การดีบักง่ายขึ้น ข้อผิดพลาดของโค้ดที่อาจถูกละเลยหรือล้มเหลวอย่างเงียบๆ จะสร้างข้อผิดพลาดหรือส่งข้อยกเว้น แจ้งเตือนคุณถึงปัญหาในโค้ดของคุณเร็วขึ้นและนำคุณไปยังแหล่งที่มาของโค้ดได้เร็วขึ้น
  • ป้องกัน globals โดยไม่ตั้งใจ หากไม่มีโหมดเข้มงวด การกำหนดค่าให้กับตัวแปรที่ไม่ได้ประกาศจะสร้างตัวแปรส่วนกลางที่มีชื่อนั้นโดยอัตโนมัติ นี่เป็นหนึ่งในข้อผิดพลาดที่พบบ่อยที่สุดใน JavaScript ในโหมดเข้มงวด การพยายามทำเช่นนั้นทำให้เกิดข้อผิดพลาด
  • this นี้ หากไม่มีโหมดเข้มงวด การอ้างอิง this ค่า null หรือไม่ได้กำหนดนี้จะถูกบังคับไปยังโกลบอลโดยอัตโนมัติ สิ่งนี้สามารถทำให้เกิดการปลอมแปลงศีรษะและแมลงชนิดดึงผมออกได้ ในโหมดเข้มงวด การอ้างอิง aa ค่า this เป็น null หรือไม่ได้กำหนดจะทำให้เกิดข้อผิดพลาด
  • ไม่อนุญาตให้ใช้ชื่อคุณสมบัติหรือค่าพารามิเตอร์ที่ซ้ำกัน โหมดเข้มงวดแสดงข้อผิดพลาดเมื่อตรวจพบคุณสมบัติที่มีชื่อซ้ำกันในวัตถุ (เช่น var object = {foo: "bar", foo: "baz"}; ) หรืออาร์กิวเมนต์ที่มีชื่อซ้ำกันสำหรับฟังก์ชัน (เช่น function foo(val1, val2, val1){} ) ดังนั้นจึงตรวจจับสิ่งที่เกือบจะเป็นข้อบกพร่องในโค้ดของคุณ ซึ่งคุณอาจเสียเวลามากในการติดตาม
  • ทำให้ eval() ปลอดภัยยิ่งขึ้น มีความแตกต่างบางประการในการทำงานของ eval() ในโหมดเข้มงวดและโหมดไม่เข้มงวด ที่สำคัญที่สุด ในโหมดเข้มงวด ตัวแปรและฟังก์ชันที่ประกาศภายในคำสั่ง eval() จะ ไม่ ถูก สร้างขึ้นในขอบเขตที่มีอยู่
  • เกิดข้อผิดพลาดในการใช้งาน delete ที่ไม่ถูกต้อง ตัวดำเนินการ delete (ใช้เพื่อลบคุณสมบัติออกจากวัตถุ) ไม่สามารถใช้กับคุณสมบัติที่ไม่สามารถกำหนดค่าได้ของวัตถุ รหัสที่ไม่เข้มงวดจะล้มเหลวอย่างเงียบ ๆ เมื่อพยายามลบคุณสมบัติที่ไม่สามารถกำหนดค่าได้ ในขณะที่โหมดเข้มงวดจะทำให้เกิดข้อผิดพลาดในกรณีดังกล่าว

สรุป

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

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

ที่เกี่ยวข้อง: สัญญา JavaScript: บทช่วยสอนพร้อมตัวอย่าง