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 ยังคงคิดว่า "เข้าถึงได้"
ข้อผิดพลาดทั่วไป #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 ของคุณไม่ทำงาน