การสืบทอดแบบโปรโตไทป์
ยกเว้น null
และ undefined
ประเภทข้อมูลพื้นฐานแต่ละประเภทจะมีโปรโตไทป์ ซึ่งเป็นออบเจ็กต์ Wrapper ที่เกี่ยวข้องซึ่งมีเมธอดสำหรับดำเนินการกับค่า เมื่อเรียกใช้เมธอดหรือการค้นหาพร็อพเพอร์ตี้ในพรอมิเตี JavaScript จะรวมพรอมิเตีไว้เบื้องหลังและเรียกใช้เมธอดหรือทำการค้นหาพร็อพเพอร์ตี้ในออบเจ็กต์ Wrapper แทน
เช่น สตริงลิเทอรัลไม่มีเมธอดเป็นของตัวเอง แต่คุณสามารถเรียกใช้เมธอด .toUpperCase()
กับสตริงลิเทอรัลได้โดยใช้ออบเจ็กต์ Wrapper String
ที่เกี่ยวข้อง ดังนี้
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
ซึ่งเรียกว่าการสืบทอดโปรโตไทป์ นั่นคือการสืบทอดพร็อพเพอร์ตี้และเมธอดจากคอนสตรัคเตอร์ที่สอดคล้องกันของค่า
Number.prototype
> Number { 0 }
> constructor: function Number()
> toExponential: function toExponential()
> toFixed: function toFixed()
> toLocaleString: function toLocaleString()
> toPrecision: function toPrecision()
> toString: function toString()
> valueOf: function valueOf()
> <prototype>: Object { … }
คุณสามารถสร้างพรอมิเตอโดยใช้ตัวสร้างเหล่านี้แทนการกำหนดพรอมิเตอตามค่า เช่น การใช้คอนสตรัคเตอร์ String
จะสร้างออบเจ็กต์สตริง ไม่ใช่สตริงลิเทอรัล ซึ่งเป็นออบเจ็กต์ที่มีไม่เพียงค่าสตริงเท่านั้น แต่ยังมีพร็อพเพอร์ตี้และเมธอดทั้งหมดที่รับค่ามาจากคอนสตรัคเตอร์
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
โดยทั่วไปแล้ว ออบเจ็กต์ที่ได้จะทํางานตามค่าที่เราใช้เพื่อกําหนด ตัวอย่างเช่น แม้ว่าการกำหนดค่าตัวเลขโดยใช้คอนสตรัคเตอร์ new Number
จะส่งผลให้ออบเจ็กต์มีเมธอดและพร็อพเพอร์ตี้ทั้งหมดของโปรโตไทป์ Number
แต่คุณก็สามารถใช้โอเปอเรเตอร์ทางคณิตศาสตร์กับออบเจ็กต์เหล่านั้นได้เช่นเดียวกับที่ใช้กับค่าตัวเลขล้วน ดังนี้
const numberOne = new Number(1);
const numberTwo = new Number(2);
numberOne;
> Number { 1 }
typeof numberOne;
> "object"
numberTwo;
> Number { 2 }
typeof numberTwo;
> "object"
numberOne + numberTwo;
> 3
คุณแทบจะไม่ต้องใช้ตัวสร้างเหล่านี้เลย เนื่องจากการสืบทอดโปรโตไทป์ในตัวของ JavaScript หมายความว่าตัวสร้างเหล่านี้ไม่มีประโยชน์ในทางปฏิบัติ การสร้างพรอมิเตอโดยใช้คอนสตรัคเตอร์อาจทำให้เกิดผลลัพธ์ที่ไม่คาดคิดด้วยเช่นกัน เนื่องจากผลลัพธ์คือออบเจ็กต์ ไม่ใช่ลิเทอรัลธรรมดา
let stringLiteral = "String literal."
typeof stringLiteral;
> "string"
let stringObject = new String( "String object." );
stringObject
> "object"
ซึ่งอาจทําให้การใช้โอเปอเรเตอร์การเปรียบเทียบแบบเข้มงวดมีความซับซ้อน
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
การแทรกเครื่องหมายเซมิโคลอนอัตโนมัติ (ASI)
ขณะแยกวิเคราะห์สคริปต์ โปรแกรมแปลภาษา JavaScript สามารถใช้ฟีเจอร์ที่เรียกว่าการแทรกเครื่องหมายเซมิโคลอนอัตโนมัติ (ASI) เพื่อพยายามแก้ไขอินสแตนซ์ของเครื่องหมายเซมิโคลอนที่ถูกละเว้น หากตัวแยกวิเคราะห์ JavaScript พบโทเค็นที่ไม่อนุญาต ก็จะพยายามเพิ่มเครื่องหมายเซมิโคลอนก่อนโทเค็นนั้นเพื่อแก้ไขข้อผิดพลาดด้านไวยากรณ์ที่อาจเกิดขึ้น ตราบใดที่เงื่อนไขต่อไปนี้อย่างน้อย 1 ข้อเป็นจริง
- โทเค็นนั้นจะคั่นจากโทเค็นก่อนหน้าด้วยตัวแบ่งบรรทัด
- โทเค็นดังกล่าวคือ
}
- โทเค็นก่อนหน้าคือ
)
และเซมิโคลอนแทรกจะเป็นเซมิโคลอนปิดของคำสั่งdo
…while
ดูข้อมูลเพิ่มเติมได้ที่กฎ ASI
ตัวอย่างเช่น การละเว้นเครื่องหมายเซมิโคลอนหลังคำสั่งต่อไปนี้จะไม่ทำให้เกิดข้อผิดพลาดทางไวยากรณ์เนื่องจาก ASI
const myVariable = 2
myVariable + 3
> 5
อย่างไรก็ตาม ASI ไม่สามารถพิจารณาคำสั่งหลายรายการในบรรทัดเดียวกัน หากคุณเขียนคำสั่งมากกว่า 1 รายการในบรรทัดเดียวกัน ให้คั่นคำสั่งเหล่านั้นด้วยเซมิโคลอน
const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier
const myVariable = 2; myVariable + 3;
> 5
ASI คือการพยายามแก้ไขข้อผิดพลาด ไม่ใช่ความยืดหยุ่นทางไวยากรณ์ที่ฝังอยู่ใน JavaScript โปรดใช้เครื่องหมายเซมิโคลอนตามความเหมาะสมเพื่อไม่ให้ต้องอาศัยเครื่องหมายนี้ในการสร้างโค้ดที่ถูกต้อง
โหมดจำกัด
มาตรฐานที่ควบคุมวิธีเขียน JavaScript พัฒนาไปไกลกว่าสิ่งที่ได้พิจารณาไว้ในช่วงแรกของการออกแบบภาษา การเปลี่ยนแปลงใหม่ทั้งหมดในลักษณะการทํางานที่คาดไว้ของ JavaScript ต้องไม่ทําให้เกิดข้อผิดพลาดในเว็บไซต์เก่า
ES5 แก้ไขปัญหาเกี่ยวกับความหมายของ JavaScript ที่แก้ไม่ตกมานานโดยไม่ทำให้การใช้งานที่มีอยู่ขัดข้องด้วยการนำ "โหมดเข้มงวด" มาใช้ ซึ่งเป็นวิธีเลือกใช้ชุดกฎภาษาที่เข้มงวดมากขึ้นสำหรับสคริปต์ทั้งรายการหรือฟังก์ชันแต่ละรายการ หากต้องการเปิดใช้โหมดจำกัด ให้ใช้สตริงตัวอักษรล้วน "use strict"
ตามด้วยเซมิโคลอน ในบรรทัดแรกของสคริปต์หรือฟังก์ชัน
"use strict";
function myFunction() {
"use strict";
}
โหมดเข้มงวดจะป้องกันการดำเนินการที่ "ไม่ปลอดภัย" หรือฟีเจอร์ที่เลิกใช้งานแล้วบางรายการ แสดงข้อผิดพลาดที่ชัดเจนแทนข้อผิดพลาดที่ "ซ่อนอยู่" ทั่วไป และห้ามใช้ไวยากรณ์ที่อาจขัดแย้งกับฟีเจอร์ภาษาในอนาคต ตัวอย่างเช่น การตัดสินใจด้านการออกแบบในช่วงแรกเกี่ยวกับขอบเขตตัวแปรทำให้นักพัฒนาซอฟต์แวร์มีแนวโน้มที่จะ "ปนเปื้อน" ขอบเขตส่วนกลางโดยไม่ตั้งใจเมื่อประกาศตัวแปร โดยไม่คำนึงถึงบริบทที่รวมอยู่ด้วย โดยการละเว้นคีย์เวิร์ด var
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
รันไทม์ JavaScript สมัยใหม่ไม่สามารถแก้ไขลักษณะการทำงานนี้ได้โดยไม่เสี่ยงที่จะทำให้เว็บไซต์ที่อาศัยรันไทม์ดังกล่าวใช้งานไม่ได้ ไม่ว่าจะเป็นการจงใจหรือไม่จงใจก็ตาม แต่ JavaScript สมัยใหม่จะป้องกันปัญหานี้ด้วยการอนุญาตให้นักพัฒนาซอฟต์แวร์เลือกใช้โหมดเข้มงวดสำหรับงานใหม่ และเปิดใช้โหมดเข้มงวดโดยค่าเริ่มต้นเฉพาะในบริบทของฟีเจอร์ภาษาใหม่ที่จะไม่ทำให้การใช้งานเดิมใช้งานไม่ได้
(function() {
"use strict";
mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal
คุณต้องเขียน "use strict"
เป็นสตริงลิเทอรัล
นิพจน์เทมเพลต (use strict
) จะใช้ไม่ได้ นอกจากนี้ คุณยังต้องใส่ "use strict"
ไว้ก่อนโค้ดที่เรียกใช้ได้ในบริบทที่ต้องการ มิฉะนั้นโปรแกรมแปลจะละเว้น
(function() {
"use strict";
let myVariable = "String.";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal
(function() {
let myVariable = "String.";
"use strict";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope
ตามการอ้างอิง ตามค่า
ตัวแปรใดก็ตาม ซึ่งรวมถึงพร็อพเพอร์ตี้ของออบเจ็กต์, พารามิเตอร์ของฟังก์ชัน และองค์ประกอบในอาร์เรย์, เซ็ต หรือแผนที่อาจมีค่าพื้นฐานหรือค่าอ้างอิงก็ได้
เมื่อมีการกําหนดค่าพื้นฐานจากตัวแปรหนึ่งไปยังอีกตัวแปรหนึ่ง เครื่องมือ JavaScript จะสร้างสําเนาของค่านั้นและกำหนดให้กับตัวแปร
เมื่อคุณกําหนดออบเจ็กต์ (อินสแตนซ์ของคลาส อาร์เรย์ และฟังก์ชัน) ให้กับตัวแปร ตัวแปรจะมีการอ้างอิงตําแหน่งที่จัดเก็บของออบเจ็กต์ในหน่วยความจําแทนที่จะสร้างสําเนาใหม่ของออบเจ็กต์นั้น ด้วยเหตุนี้ การเปลี่ยนแปลงออบเจ็กต์ที่อ้างอิงโดยตัวแปรจึงจะเปลี่ยนออบเจ็กต์ที่อ้างอิง ไม่ใช่แค่ค่าที่มีอยู่ในตัวแปรนั้น เช่น หากคุณเริ่มต้นตัวแปรใหม่ด้วยตัวแปรที่มีการอ้างอิงวัตถุ จากนั้นใช้ตัวแปรใหม่เพื่อเพิ่มพร็อพเพอร์ตี้ลงในออบเจ็กต์นั้น ระบบจะเพิ่มพร็อพเพอร์ตี้และค่าของพร็อพเพอร์ตี้นั้นลงในออบเจ็กต์เดิม
const myObject = {};
const myObjectReference = myObject;
myObjectReference.myProperty = true;
myObject;
> Object { myProperty: true }
การดำเนินการนี้ไม่เพียงสำคัญสำหรับการแก้ไขออบเจ็กต์เท่านั้น แต่ยังสำคัญสำหรับการดำเนินการเปรียบเทียบที่เข้มงวดด้วย เนื่องจากความเท่าเทียมที่เข้มงวดระหว่างออบเจ็กต์กำหนดให้ตัวแปรทั้ง 2 รายการต้องอ้างอิงออบเจ็กต์เดียวกันจึงจะประเมินเป็น true
ได้ แต่จะอ้างอิงออบเจ็กต์อื่นไม่ได้ แม้ว่าออบเจ็กต์เหล่านั้นจะมีโครงสร้างเหมือนกันก็ตาม
const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};
myObject === myNewObject;
> false
myObject === myReferencedObject;
> true
การจัดสรรหน่วยความจำ
JavaScript ใช้การจัดการหน่วยความจําอัตโนมัติ ซึ่งหมายความว่าไม่จําเป็นต้องจัดสรรหรือยกเลิกการจัดสรรหน่วยความจําอย่างชัดเจนในระหว่างการพัฒนา แม้ว่ารายละเอียดเกี่ยวกับแนวทางการจัดการหน่วยความจำของเครื่องมือรันไทม์ JavaScript จะอยู่นอกขอบเขตของโมดูลนี้ แต่การทำความเข้าใจวิธีจัดสรรหน่วยความจำจะให้บริบทที่เป็นประโยชน์สำหรับการทำงานกับค่าอ้างอิง
หน่วยความจํามี "พื้นที่" 2 แห่ง ได้แก่ "กอง" และ "กองขยะ" สแต็กจะจัดเก็บข้อมูลแบบคงที่ ซึ่งได้แก่ ค่าพื้นฐานและการอ้างอิงออบเจ็กต์ เนื่องจากสามารถจัดสรรพื้นที่เก็บข้อมูลจํานวนคงที่ที่จําเป็นสําหรับจัดเก็บข้อมูลนี้ก่อนที่จะเรียกใช้สคริปต์ กองเก็บข้อมูลจะจัดเก็บออบเจ็กต์ ซึ่งต้องใช้พื้นที่ที่ระบบจัดสรรแบบไดนามิกเนื่องจากขนาดของออบเจ็กต์อาจเปลี่ยนแปลงระหว่างการเรียกใช้ หน่วยความจำจะเพิ่มว่างโดยกระบวนการที่เรียกว่า "การเก็บขยะ" ซึ่งจะนำออบเจ็กต์ที่ไม่มีข้อมูลอ้างอิงออกจากหน่วยความจำ
เทรดหลัก
JavaScript เป็นภาษาแบบเทรดเธรดเดียวโดยพื้นฐานซึ่งมีรูปแบบการทํางานแบบ "พร้อมกัน" ซึ่งหมายความว่าจะดําเนินการงานได้เพียงงานเดียวในแต่ละครั้ง บริบทการดําเนินการตามลําดับนี้เรียกว่าเธรดหลัก
และเธรดหลักจะแชร์กับงานอื่นๆ ของเบราว์เซอร์ เช่น แยกวิเคราะห์ HTML, แสดงผลและแสดงผลบางส่วนของหน้าเว็บอีกครั้ง, เรียกใช้ภาพเคลื่อนไหว CSS และจัดการการโต้ตอบของผู้ใช้ตั้งแต่แบบง่าย (เช่น การไฮไลต์ข้อความ) ไปจนถึงแบบซับซ้อน (เช่น การโต้ตอบกับองค์ประกอบของแบบฟอร์ม) ผู้ให้บริการเบราว์เซอร์พบวิธีเพิ่มประสิทธิภาพงานที่ดำเนินการโดยเธรดหลักแล้ว แต่สคริปต์ที่ซับซ้อนมากขึ้นอาจยังใช้ทรัพยากรของเธรดหลักมากเกินไปและส่งผลต่อประสิทธิภาพโดยรวมของหน้าเว็บ
งานบางอย่างสามารถดำเนินการในเธรดเบื้องหลังที่เรียกว่า Web Worker โดยมีข้อจำกัดบางอย่างดังนี้
- เวิร์กเกอร์เธรดจะทํางานกับไฟล์ JavaScript แบบสแตนด์อโลนได้เท่านั้น
- ผู้ใช้เข้าถึงหน้าต่างเบราว์เซอร์และ UI ได้น้อยมากหรือไม่ได้เลย
- โดยจะมีข้อจำกัดในการสื่อสารกับชุดข้อความหลัก
ข้อจำกัดเหล่านี้ทำให้โหมดนี้เหมาะสำหรับงานที่ต้องใช้ทรัพยากรมากและต้องการโฟกัส ซึ่งอาจใช้ชุดข้อความหลัก
สแต็กการเรียกใช้
โครงสร้างข้อมูลที่ใช้ในการจัดการ "บริบทการดําเนินการ" ซึ่งเป็นโค้ดที่ดําเนินการอยู่คือลิสต์ที่เรียกว่า กองคําสั่ง (มักเรียกว่า "กอง") เมื่อสคริปต์ทำงานเป็นครั้งแรก ผู้ตีความ JavaScript จะสร้าง "บริบทการเรียกใช้แบบรวม" และส่งไปยังกองคิวการเรียกใช้ โดยคำสั่งภายในบริบทแบบรวมจะทำงานทีละรายการจากบนลงล่าง เมื่อล่ามพบการเรียกใช้ฟังก์ชันขณะที่ดำเนินการบริบทส่วนกลาง ก็จะดัน "บริบทการเรียกใช้ฟังก์ชัน" ของการเรียกนั้นไว้ที่ด้านบนของกอง หยุดบริบทการเรียกใช้ส่วนกลางชั่วคราว และดำเนินการบริบทการเรียกใช้ฟังก์ชัน
ทุกครั้งที่มีการเรียกใช้ฟังก์ชัน ระบบจะดันบริบทการเรียกใช้ฟังก์ชันสําหรับการเรียกใช้นั้นไปไว้ที่ด้านบนของสแต็กเหนือบริบทการเรียกใช้ปัจจุบัน กองซ้อนการเรียกใช้จะทำงานแบบ "เข้าล่าสุดออกก่อน" ซึ่งหมายความว่าระบบจะเรียกใช้การเรียกใช้ฟังก์ชันล่าสุดซึ่งอยู่ในกองซ้อนสูงสุดและดำเนินการต่อจนกว่าจะแก้ไขได้ เมื่อฟังก์ชันนั้นทำงานเสร็จแล้ว ผู้ตีความจะนําฟังก์ชันนั้นออกจากสแต็กการเรียกใช้ และบริบทการดําเนินการที่มีคําเรียกใช้ฟังก์ชันนั้นก็จะกลายเป็นรายการที่สูงที่สุดในสแต็กอีกครั้งและดําเนินการต่อ
บริบทการดําเนินการเหล่านี้จะบันทึกค่าที่จําเป็นสําหรับการดําเนินการ นอกจากนี้ ยังสร้างตัวแปรและฟังก์ชันที่ใช้ได้ภายในขอบเขตของฟังก์ชันตามบริบทหลักของฟังก์ชันนั้น และกำหนดและตั้งค่าคีย์เวิร์ด this
ในบริบทของฟังก์ชัน
ลูปเหตุการณ์และคิวการเรียกกลับ
การดำเนินการตามลำดับนี้หมายความว่างานแบบแอสซิงโครนัสที่มีฟังก์ชันการเรียกกลับ เช่น การดึงข้อมูลจากเซิร์ฟเวอร์ การตอบสนองต่อการโต้ตอบของผู้ใช้ หรือรอตัวจับเวลาที่ตั้งค่าด้วย setTimeout
หรือ setInterval
จะบล็อกเธรดหลักจนกว่างานนั้นจะเสร็จสมบูรณ์ หรือจะขัดจังหวะบริบทการดําเนินการปัจจุบันโดยไม่คาดคิดเมื่อมีการเพิ่มบริบทการดําเนินการของฟังก์ชันการเรียกกลับลงในสแต็ก JavaScript จึงจัดการงานแบบไม่พร้อมกันโดยใช้ "รูปแบบการทำงานพร้อมกัน" ที่ทำงานตามเหตุการณ์ ซึ่งประกอบด้วย "ลูปเหตุการณ์" และ "คิวการเรียกคืน" (บางครั้งเรียกว่า "คิวข้อความ")
เมื่อระบบเรียกใช้งานที่ทำงานพร้อมกันในเธรดหลัก ระบบจะวางบริบทการเรียกใช้ของฟังก์ชันการเรียกคืนไว้ในคิวการเรียกคืน ไม่ใช่ที่ด้านบนของกองคําเรียก ลูปเหตุการณ์เป็นรูปแบบที่บางครั้งเรียกว่า Reactor ซึ่งจะตรวจสอบสถานะของกองคิวการเรียกและคิวการเรียกกลับอย่างต่อเนื่อง หากมีคิวการติดต่อกลับและลูปเหตุการณ์ระบุว่ากองซ้อนการเรียกไม่มีข้อมูล ระบบจะส่งคิวการติดต่อกลับไปยังกองซ้อนทีละรายการเพื่อดำเนินการ