ภาคผนวก

การสืบทอดแบบโปรโตไทป์

ยกเว้น 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 ข้อเป็นจริง

  • โทเค็นนั้นจะคั่นจากโทเค็นก่อนหน้าด้วยตัวแบ่งบรรทัด
  • โทเค็นดังกล่าวคือ }
  • โทเค็นก่อนหน้าคือ ) และเซมิโคลอนแทรกจะเป็นเซมิโคลอนปิดของคำสั่ง dowhile

ดูข้อมูลเพิ่มเติมได้ที่กฎ 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 ซึ่งจะตรวจสอบสถานะของกองคิวการเรียกและคิวการเรียกกลับอย่างต่อเนื่อง หากมีคิวการติดต่อกลับและลูปเหตุการณ์ระบุว่ากองซ้อนการเรียกไม่มีข้อมูล ระบบจะส่งคิวการติดต่อกลับไปยังกองซ้อนทีละรายการเพื่อดำเนินการ