ขอบเขตและบริบทใน JavaScript มีเอกลักษณ์เฉพาะสำหรับภาษานั้นๆ ส่วนหนึ่งเป็นเพราะความยืดหยุ่นที่นำมาซึ่ง แต่ละฟังก์ชันมีบริบทและขอบเขตของตัวแปรที่แตกต่างกัน แนวคิดเหล่านี้รองรับรูปแบบการออกแบบที่ทรงพลังใน JavaScript อย่างไรก็ตาม สิ่งนี้ยังสร้างความสับสนอย่างมากให้กับนักพัฒนาอีกด้วย ข้อมูลต่อไปนี้จะเปิดเผยความแตกต่างระหว่างบริบทและขอบเขตใน JavaScript อย่างครอบคลุม และวิธีใช้รูปแบบการออกแบบต่างๆ
บริบทและขอบเขต
สิ่งแรกที่ต้องชี้แจงคือบริบทและขอบเขตเป็นแนวคิดที่แตกต่างกัน ในช่วงหลายปีที่ผ่านมา ฉันสังเกตเห็นว่านักพัฒนาซอฟต์แวร์จำนวนมากมักสับสนระหว่างสองคำนี้ โดยอธิบายคำหนึ่งกับอีกคำหนึ่งอย่างไม่ถูกต้อง พูดตามตรง ข้อกำหนดเหล่านี้ทำให้เกิดความสับสนอย่างมาก
การเรียกใช้ฟังก์ชันทุกครั้งมีขอบเขตและบริบทที่เกี่ยวข้อง โดยพื้นฐานแล้วขอบเขตจะขึ้นอยู่กับฟังก์ชันและบริบทจะขึ้นอยู่กับวัตถุ กล่าวอีกนัยหนึ่ง ขอบเขตเกี่ยวข้องกับการเข้าถึงตัวแปรในการเรียกใช้ฟังก์ชันแต่ละครั้ง และการเรียกแต่ละครั้งมีความเป็นอิสระ บริบทจะเป็นค่าของคีย์เวิร์ด this เสมอ ซึ่งเป็นการอ้างอิงถึงออบเจ็กต์ที่เรียกโค้ดปฏิบัติการปัจจุบัน
ขอบเขตตัวแปร
คุณสามารถกำหนดตัวแปรในขอบเขตท้องถิ่นหรือส่วนกลางได้ ซึ่งส่งผลให้มีการเข้าถึงตัวแปรรันไทม์จากขอบเขตที่ต่างกัน ตัวแปรร่วมจำเป็นต้องได้รับการประกาศภายนอกเนื้อหาของฟังก์ชัน มีอยู่ตลอดกระบวนการทำงาน และสามารถเข้าถึงและแก้ไขได้ในขอบเขตใดก็ได้ ตัวแปรท้องถิ่นถูกกำหนดไว้ภายในเนื้อหาของฟังก์ชันเท่านั้น และมีขอบเขตที่แตกต่างกันสำหรับการเรียกใช้ฟังก์ชันแต่ละครั้ง หัวข้อนี้เป็นการมอบหมาย การประเมิน และการดำเนินการของค่าภายในการโทรเท่านั้น และไม่สามารถเข้าถึงค่าที่อยู่นอกขอบเขตได้
ปัจจุบัน JavaScript ไม่รองรับขอบเขตระดับบล็อก ขอบเขตระดับบล็อกหมายถึงการกำหนดตัวแปรในบล็อกคำสั่ง เช่น คำสั่ง if, คำสั่ง switch, คำสั่ง loop ฯลฯ ซึ่งหมายความว่าไม่สามารถเข้าถึงตัวแปรภายนอกบล็อกคำสั่งได้ ปัจจุบันสามารถเข้าถึงตัวแปรใดๆ ที่กำหนดไว้ภายในบล็อกคำสั่งได้ภายนอกบล็อกคำสั่ง อย่างไรก็ตาม สิ่งนี้จะเปลี่ยนไปในไม่ช้า เนื่องจากมีการเพิ่มคีย์เวิร์ด let อย่างเป็นทางการในข้อกำหนด ES6 ใช้แทนคีย์เวิร์ด var เพื่อประกาศตัวแปรโลคัลเป็นขอบเขตระดับบล็อก
บริบท "นี้"
บริบทมักจะขึ้นอยู่กับวิธีการเรียกใช้ฟังก์ชัน เมื่อฟังก์ชันถูกเรียกว่าเป็นวิธีการบนวัตถุ สิ่งนี้จะถูกตั้งค่าเป็นวัตถุที่มีการเรียกใช้วิธีการ:
คัดลอกรหัสรหัสดังต่อไปนี้:
วัตถุ var = {
ฟู: ฟังก์ชั่น(){
การแจ้งเตือน (สิ่งนี้ === วัตถุ);
-
-
object.foo(); // จริง
หลักการเดียวกันนี้ใช้เมื่อเรียกใช้ฟังก์ชันเพื่อสร้างอินสแตนซ์ของออบเจ็กต์โดยใช้ตัวดำเนินการใหม่ เมื่อเรียกด้วยวิธีนี้ ค่าของสิ่งนี้จะถูกตั้งค่าเป็นอินสแตนซ์ที่สร้างขึ้นใหม่:
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่นฟู(){
การแจ้งเตือน (สิ่งนี้);
-
foo() // หน้าต่าง
ใหม่ foo() // foo
เมื่อเรียกใช้ฟังก์ชันที่ไม่ถูกผูกไว้ สิ่งนี้จะถูกตั้งค่าเป็นบริบทส่วนกลางหรือวัตถุหน้าต่าง (หากอยู่ในเบราว์เซอร์) ตามค่าเริ่มต้น อย่างไรก็ตาม หากฟังก์ชันถูกดำเนินการในโหมดเข้มงวด ("ใช้อย่างเข้มงวด") ค่าของสิ่งนี้จะถูกตั้งค่าเป็นไม่ได้กำหนดตามค่าเริ่มต้น
บริบทการดำเนินการและห่วงโซ่ขอบเขต
JavaScript เป็นภาษาแบบเธรดเดียว ซึ่งหมายความว่า JavaScript สามารถทำสิ่งเดียวในแต่ละครั้งในเบราว์เซอร์เท่านั้น เมื่อล่าม JavaScript เริ่มรันโค้ด มันจะตั้งค่าเริ่มต้นเป็นบริบทสากลก่อน การเรียกใช้ฟังก์ชันแต่ละครั้งจะสร้างบริบทการดำเนินการใหม่
ความสับสนมักเกิดขึ้นที่นี่ คำว่า "บริบทการดำเนินการ" ในที่นี้หมายถึงขอบเขต ไม่ใช่บริบทตามที่กล่าวไว้ข้างต้น นี่เป็นการตั้งชื่อที่ไม่ดี แต่คำนี้ถูกกำหนดโดยข้อกำหนด ECMAScript และไม่มีทางเลือกอื่นนอกจากต้องปฏิบัติตาม
แต่ละครั้งที่มีการสร้างบริบทการดำเนินการใหม่ บริบทนั้นจะถูกเพิ่มที่ด้านบนของห่วงโซ่ขอบเขตและกลายเป็นการดำเนินการหรือสแตกการเรียก เบราว์เซอร์จะทำงานในบริบทการดำเนินการปัจจุบันที่ด้านบนของห่วงโซ่ขอบเขตเสมอ เมื่อเสร็จสิ้นแล้ว (บริบทการดำเนินการปัจจุบัน) จะถูกลบออกจากด้านบนของสแต็กและการควบคุมจะถูกส่งกลับไปยังบริบทการดำเนินการก่อนหน้า ตัวอย่างเช่น:
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่นแรก () {
ที่สอง();
ฟังก์ชั่นวินาที () {
ที่สาม();
ฟังก์ชั่นที่สาม () {
ที่สี่();
ฟังก์ชั่นที่สี่ () {
//ทำอะไรสักอย่าง
-
-
-
-
อันดับแรก();
การเรียกใช้โค้ดก่อนหน้าจะทำให้ฟังก์ชันที่ซ้อนกันถูกดำเนินการจากบนลงล่างจนถึงฟังก์ชันที่สี่ ในขณะนี้ ห่วงโซ่ขอบเขตจากบนลงล่างคือ: สี่ สาม ที่สอง แรก ทั่วโลก ฟังก์ชันที่สี่สามารถเข้าถึงตัวแปรร่วมและตัวแปรใดๆ ที่กำหนดไว้ในฟังก์ชันที่หนึ่ง ที่สอง และที่สามได้เหมือนกับตัวแปรของตัวเอง เมื่อฟังก์ชันที่สี่ดำเนินการเสร็จสิ้น บริบทที่สี่จะถูกลบออกจากด้านบนของห่วงโซ่ขอบเขต และการดำเนินการจะกลับสู่ฟังก์ชันที่สาม กระบวนการนี้จะดำเนินต่อไปจนกว่าโค้ดทั้งหมดจะเสร็จสิ้นการดำเนินการ
ข้อขัดแย้งในการตั้งชื่อตัวแปรระหว่างบริบทการดำเนินการที่แตกต่างกันจะได้รับการแก้ไขโดยการไต่ระดับห่วงโซ่ขอบเขตจากท้องถิ่นไปสู่ระดับโลก ซึ่งหมายความว่าตัวแปรท้องถิ่นที่มีชื่อเดียวกันจะมีลำดับความสำคัญสูงกว่าในห่วงโซ่ขอบเขต
พูดง่ายๆ ก็คือ ทุกครั้งที่คุณพยายามเข้าถึงตัวแปรในบริบทการดำเนินการฟังก์ชัน กระบวนการค้นหาจะเริ่มต้นจากออบเจ็กต์ตัวแปรของตัวเองเสมอ หากไม่พบตัวแปรที่คุณกำลังมองหาในออบเจ็กต์ตัวแปรของคุณเอง ให้ค้นหาห่วงโซ่ขอบเขตต่อไป มันจะไต่ระดับห่วงโซ่ขอบเขตและตรวจสอบออบเจ็กต์ตัวแปรบริบทการดำเนินการแต่ละรายการเพื่อค้นหาค่าที่ตรงกับชื่อตัวแปร
ปิด
การปิดจะเกิดขึ้นเมื่อมีการเข้าถึงฟังก์ชันที่ซ้อนกันอยู่นอกคำจำกัดความ (ขอบเขต) เพื่อให้สามารถดำเนินการได้หลังจากที่ฟังก์ชันภายนอกส่งคืน มัน (การปิด) รักษา (ในฟังก์ชันภายใน) การเข้าถึงตัวแปรท้องถิ่น อาร์กิวเมนต์ และการประกาศฟังก์ชันในฟังก์ชันภายนอก การห่อหุ้มช่วยให้เราสามารถซ่อนและปกป้องบริบทการดำเนินการจากขอบเขตภายนอก ในขณะเดียวกันก็เปิดเผยอินเทอร์เฟซสาธารณะซึ่งสามารถดำเนินการต่อไปได้ ตัวอย่างง่ายๆ มีลักษณะดังนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่นฟู(){
var local = 'ตัวแปรส่วนตัว';
แถบฟังก์ชันส่งคืน () {
กลับท้องถิ่น
-
-
var getLocalVariable = foo();
getLocalVariable() // ตัวแปรส่วนตัว
การปิดประเภทหนึ่งที่ได้รับความนิยมมากที่สุดคือรูปแบบโมดูลที่รู้จักกันดี ช่วยให้คุณสามารถเยาะเย้ยสมาชิกสาธารณะ ส่วนตัว และสิทธิพิเศษ:
คัดลอกรหัสรหัสดังต่อไปนี้:
โมดูล var = (ฟังก์ชั่น(){
var privateProperty = 'ฟู';
ฟังก์ชั่น privateMethod (args) {
//ทำอะไรสักอย่าง
-
กลับ {
คุณสมบัติสาธารณะ: "",
publicMethod: ฟังก์ชั่น (args) {
//ทำอะไรสักอย่าง
-
สิทธิพิเศษวิธีการ: ฟังก์ชั่น (args) {
วิธีการส่วนตัว(args);
-
-
-
โมดูลจริงๆ แล้วค่อนข้างคล้ายกับซิงเกิลตัน โดยเพิ่มวงเล็บคู่ที่ส่วนท้ายและดำเนินการทันทีหลังจากที่ล่ามแปลเสร็จ (ดำเนินการฟังก์ชันทันที) สมาชิกภายนอกที่มีอยู่เฉพาะของบริบทการดำเนินการปิดคือวิธีการสาธารณะและคุณสมบัติในวัตถุที่ส่งคืน (เช่น Module.publicMethod) อย่างไรก็ตาม คุณสมบัติและวิธีการส่วนตัวทั้งหมดจะคงอยู่ตลอดวงจรชีวิตของโปรแกรม เนื่องจากบริบทการดำเนินการได้รับการป้องกัน (การปิด) และการโต้ตอบกับตัวแปรจะดำเนินการผ่านวิธีการสาธารณะ
การปิดอีกประเภทหนึ่งเรียกว่านิพจน์ฟังก์ชันที่เรียกใช้ทันที IIFE ซึ่งไม่มีอะไรมากไปกว่าฟังก์ชันที่ไม่ระบุตัวตนที่เรียกใช้ด้วยตนเองในบริบทของหน้าต่าง
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่น (หน้าต่าง) {
var a = 'foo', b = 'บาร์';
ฟังก์ชั่นส่วนตัว () {
//ทำอะไรสักอย่าง
-
หน้าต่างโมดูล = {
สาธารณะ: ฟังก์ชั่น () {
//ทำอะไรสักอย่าง
-
-
})(นี้);
นิพจน์นี้มีประโยชน์มากในการปกป้องเนมสเปซส่วนกลาง ตัวแปรทั้งหมดที่ประกาศภายในเนื้อหาของฟังก์ชันคือตัวแปรในเครื่องและคงอยู่ตลอดสภาพแวดล้อมรันไทม์ทั้งหมดผ่านการปิด วิธีการห่อหุ้มซอร์สโค้ดนี้เป็นที่นิยมอย่างมากสำหรับทั้งโปรแกรมและเฟรมเวิร์ก โดยปกติแล้วจะเปิดเผยอินเทอร์เฟซระดับโลกเดียวเพื่อโต้ตอบกับโลกภายนอก
โทรและสมัคร
วิธีการง่ายๆ ทั้งสองนี้รวมอยู่ในฟังก์ชันทั้งหมด ช่วยให้สามารถเรียกใช้ฟังก์ชันในบริบทที่กำหนดเองได้ ฟังก์ชันการโทรต้องการรายการพารามิเตอร์ ในขณะที่ฟังก์ชัน Apply อนุญาตให้คุณส่งพารามิเตอร์เป็นอาร์เรย์:
คัดลอกรหัสรหัสดังต่อไปนี้:
ผู้ใช้ฟังก์ชัน(แรก, สุดท้าย, อายุ){
//ทำอะไรสักอย่าง
-
user.call(หน้าต่าง, 'จอห์น', 'โด', 30);
user.apply(หน้าต่าง, ['จอห์น', 'Doe', 30]);
ผลลัพธ์ของการดำเนินการจะเหมือนกัน มีการเรียกใช้ฟังก์ชันผู้ใช้ในบริบทของหน้าต่างและมีพารามิเตอร์สามตัวที่เหมือนกัน
ECMAScript 5 (ES5) นำเสนอเมธอด Function.prototype.bind เพื่อควบคุมบริบท ซึ่งส่งคืนฟังก์ชันใหม่ที่ถูกผูกไว้อย่างถาวรกับพารามิเตอร์แรกของวิธีการผูก โดยไม่คำนึงถึงวิธีการเรียกใช้ฟังก์ชัน แก้ไขบริบทของฟังก์ชันด้วยการปิด นี่คือวิธีแก้ปัญหาสำหรับเบราว์เซอร์ที่ไม่รองรับ:
คัดลอกรหัสรหัสดังต่อไปนี้:
if(!('bind' ใน Function.prototype)){
Function.prototype.bind = ฟังก์ชั่น(){
var fn = สิ่งนี้, บริบท = อาร์กิวเมนต์ [0], args = Array.prototype.slice.call (อาร์กิวเมนต์, 1);
ฟังก์ชันส่งคืน () {
กลับ fn.apply (บริบท, args);
-
-
-
มักใช้ในการสูญเสียบริบท: การประมวลผลเชิงวัตถุและเหตุการณ์ นี่เป็นสิ่งจำเป็นเนื่องจากเมธอด addEventListener ของโหนดจะรักษาบริบทการดำเนินการของฟังก์ชันเป็นโหนดที่ผูกกับตัวจัดการเหตุการณ์เสมอ ซึ่งเป็นสิ่งสำคัญ อย่างไรก็ตาม ถ้าคุณใช้เทคนิคเชิงวัตถุขั้นสูง และจำเป็นต้องรักษาบริบทของฟังก์ชันการเรียกกลับเป็นอินสแตนซ์ของวิธีการ คุณต้องปรับบริบทด้วยตนเอง นี่คือความสะดวกสบายที่เกิดจากการผูกมัด:
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่น MyClass(){
this.element = document.createElement('div');
this.element.addEventListener('คลิก', this.onClick.bind(สิ่งนี้), เท็จ);
-
MyClass.prototype.onClick = ฟังก์ชั่น(e){
//ทำอะไรสักอย่าง
-
เมื่อมองย้อนกลับไปที่ซอร์สโค้ดของฟังก์ชันการผูก คุณอาจสังเกตเห็นบรรทัดโค้ดที่ค่อนข้างง่ายต่อไปนี้ ซึ่งเรียกใช้เมธอดบน Array:
คัดลอกรหัสรหัสดังต่อไปนี้:
Array.prototype.slice.call (อาร์กิวเมนต์, 1);
สิ่งที่น่าสนใจคือสิ่งสำคัญที่ควรทราบในที่นี้ว่าอ็อบเจ็กต์อาร์กิวเมนต์นั้นไม่ใช่อาร์เรย์จริงๆ อย่างไรก็ตาม มักถูกอธิบายว่าเป็นอ็อบเจ็กต์ที่มีลักษณะคล้ายอาร์เรย์ เหมือนกับรายการ nodelist (ผลลัพธ์ที่ส่งคืนโดยเมธอด document.getElementsByTagName()) มีคุณสมบัติด้านความยาวและสามารถจัดทำดัชนีค่าได้ แต่ก็ยังไม่ใช่อาร์เรย์เนื่องจากไม่รองรับวิธีการอาร์เรย์ดั้งเดิมเช่นสไลซ์และพุช อย่างไรก็ตาม เนื่องจากเมธอดเหล่านี้มีพฤติกรรมคล้ายกับอาร์เรย์ คุณจึงสามารถเรียกและจี้เมธอดอาร์เรย์ได้ หากคุณต้องการดำเนินการวิธีการอาร์เรย์ในบริบทที่เหมือนอาร์เรย์ ให้ทำตามตัวอย่างด้านบน
เทคนิคการเรียกวิธีการของอ็อบเจ็กต์อื่น ๆ นี้ยังใช้กับเชิงวัตถุด้วย เมื่อจำลองการสืบทอดแบบคลาสสิก (การสืบทอดคลาส) ใน JavaScript:
คัดลอกรหัสรหัสดังต่อไปนี้:
MyClass.prototype.init = ฟังก์ชั่น(){
// เรียกใช้เมธอด superclass init ในบริบทของอินสแตนซ์ "MyClass"
MySuperClass.prototype.init.apply (นี่คือข้อโต้แย้ง);
-
เราสามารถสร้างรูปแบบการออกแบบอันทรงพลังนี้ขึ้นมาใหม่ได้โดยการเรียกเมธอดของซูเปอร์คลาส (MySuperClass) ในอินสแตนซ์ของคลาสย่อย (MyClass)
สรุปแล้ว
เป็นสิ่งสำคัญมากที่จะต้องเข้าใจแนวคิดเหล่านี้ก่อนที่จะเริ่มเรียนรู้รูปแบบการออกแบบขั้นสูง เนื่องจากขอบเขตและบริบทมีบทบาทสำคัญในพื้นฐานใน JavaScript สมัยใหม่ ไม่ว่าเราจะพูดถึงการปิดตัว เชิงวัตถุ และการสืบทอด หรือการใช้งานแบบเนทีฟต่างๆ บริบทและขอบเขตก็มีบทบาทสำคัญ หากเป้าหมายของคุณคือการเรียนรู้ภาษา JavaScript และทำความเข้าใจส่วนประกอบต่างๆ อย่างลึกซึ้ง ขอบเขตและบริบทควรเป็นจุดเริ่มต้น
ส่วนเสริมของผู้แปล
ฟังก์ชันการผูกที่ผู้เขียนนำมาใช้ไม่สมบูรณ์ ไม่สามารถส่งพารามิเตอร์ได้เมื่อเรียกใช้ฟังก์ชันที่ส่งคืนโดยการผูก
คัดลอกรหัสรหัสดังต่อไปนี้:
if(!('bind' ใน Function.prototype)){
Function.prototype.bind = ฟังก์ชั่น(){
var fn = สิ่งนี้, บริบท = อาร์กิวเมนต์ [0], args = Array.prototype.slice.call (อาร์กิวเมนต์, 1);
ฟังก์ชันส่งคืน () {
return fn.apply (บริบท, args.concat (อาร์กิวเมนต์)); // แก้ไขแล้ว
-
-
-