JavaScript เป็นภาษาที่เน้นฟังก์ชั่นมาก มันทำให้เรามีอิสระมาก คุณสามารถสร้างฟังก์ชันได้ทุกเมื่อ ส่งผ่านเป็นอาร์กิวเมนต์ไปยังฟังก์ชันอื่น จากนั้นจึงเรียกจากตำแหน่งโค้ดที่แตกต่างกันโดยสิ้นเชิงในภายหลัง
เรารู้อยู่แล้วว่าฟังก์ชันสามารถเข้าถึงตัวแปรภายนอกได้ (“ตัวแปรภายนอก”)
แต่จะเกิดอะไรขึ้นหากตัวแปรภายนอกเปลี่ยนแปลงไปนับตั้งแต่สร้างฟังก์ชันขึ้นมา? ฟังก์ชั่นจะได้รับค่าที่ใหม่กว่าหรือเก่าหรือไม่
และจะเกิดอะไรขึ้นถ้าฟังก์ชันถูกส่งผ่านไปเป็นอาร์กิวเมนต์และเรียกจากตำแหน่งอื่นของโค้ด ฟังก์ชันนั้นจะสามารถเข้าถึงตัวแปรภายนอกที่ตำแหน่งใหม่ได้หรือไม่?
มาขยายความรู้ของเราเพื่อทำความเข้าใจสถานการณ์เหล่านี้และสถานการณ์ที่ซับซ้อนยิ่งขึ้น
เราจะพูดถึงตัวแปร let/const
ที่นี่
ใน JavaScript มี 3 วิธีในการประกาศตัวแปร: let
, const
(อันที่ทันสมัย) และ var
(ส่วนที่เหลือของอดีต)
ในบทความนี้ เราจะใช้ตัวแปร let
ในตัวอย่าง
ตัวแปรที่ประกาศด้วย const
จะมีพฤติกรรมเหมือนกัน ดังนั้นบทความนี้จึงเกี่ยวกับ const
เช่นกัน
var
แบบเก่ามีความแตกต่างที่เห็นได้ชัดเจน ซึ่งจะกล่าวถึงในบทความ The old "var"
หากมีการประกาศตัวแปรภายในบล็อกโค้ด {...}
ตัวแปรนั้นจะมองเห็นได้ภายในบล็อกนั้นเท่านั้น
ตัวอย่างเช่น:
- // ทำงานบางอย่างกับตัวแปรท้องถิ่นที่ไม่ควรเห็นภายนอก ให้ข้อความ = "สวัสดี"; // มองเห็นได้เฉพาะในบล็อกนี้เท่านั้น การแจ้งเตือน(ข้อความ); // สวัสดี - การแจ้งเตือน(ข้อความ); // ข้อผิดพลาด: ไม่ได้กำหนดข้อความ
เราสามารถใช้สิ่งนี้เพื่อแยกส่วนของโค้ดที่ทำหน้าที่ของตัวเอง โดยมีตัวแปรที่อยู่ในโค้ดนั้นเท่านั้น:
- //แสดงข้อความ ให้ข้อความ = "สวัสดี"; การแจ้งเตือน(ข้อความ); - - // แสดงข้อความอื่น ให้ข้อความ = "ลาก่อน"; การแจ้งเตือน(ข้อความ); -
จะมีข้อผิดพลาดหากไม่มีการบล็อก
โปรดทราบว่าหากไม่มีบล็อกแยกกัน จะเกิดข้อผิดพลาด หากเราใช้ let
กับชื่อตัวแปรที่มีอยู่:
//แสดงข้อความ ให้ข้อความ = "สวัสดี"; การแจ้งเตือน(ข้อความ); // แสดงข้อความอื่น ให้ข้อความ = "ลาก่อน"; // ข้อผิดพลาด: ประกาศตัวแปรแล้ว การแจ้งเตือน(ข้อความ);
สำหรับ if
, for
, while
และอื่นๆ ตัวแปรที่ประกาศใน {...}
จะมองเห็นได้เฉพาะภายในเท่านั้น:
ถ้า (จริง) { ให้วลี = "สวัสดี!"; การแจ้งเตือน(วลี); // สวัสดี! - การแจ้งเตือน(วลี); // เกิดข้อผิดพลาด ไม่มีตัวแปรดังกล่าว!
if
เสร็จสิ้น alert
ด้านล่างจะไม่เห็น phrase
ดังนั้นจึงเกิดข้อผิดพลาด
เยี่ยมมากเพราะช่วยให้เราสร้างตัวแปรแบบบล็อกโลคอลได้ โดยเฉพาะสำหรับสาขา if
สิ่งที่คล้ายกันนี้ถือเป็นจริงสำหรับ for
และ while
loops:
สำหรับ (ให้ i = 0; i <3; i++) { // ตัวแปร i มองเห็นได้เฉพาะภายในนี้เท่านั้น การแจ้งเตือน (i); // 0 จากนั้น 1 และ 2 - การแจ้งเตือน (i); // เกิดข้อผิดพลาด ไม่มีตัวแปรดังกล่าว
สายตา let i
อยู่นอก {...}
แต่ for
build มีความพิเศษที่นี่: ตัวแปรที่ประกาศอยู่ข้างในนั้นถือว่าเป็นส่วนหนึ่งของบล็อก
ฟังก์ชันจะเรียกว่า "ซ้อนกัน" เมื่อถูกสร้างขึ้นภายในฟังก์ชันอื่น
สามารถทำได้ง่ายๆ ด้วย JavaScript
เราสามารถใช้เพื่อจัดระเบียบโค้ดของเราได้ดังนี้:
ฟังก์ชั่น sayHiBye (ชื่อ, นามสกุล) { // ฟังก์ชันซ้อนกันของตัวช่วยที่จะใช้ด้านล่าง ฟังก์ชัน getFullName() { กลับชื่อ + " " + นามสกุล; - alert( "สวัสดี" + getFullName() ); alert( "ลาก่อน" + getFullName() ); -
นี่คือฟังก์ชัน ที่ซ้อนกัน getFullName()
ถูกสร้างขึ้นเพื่อความสะดวก สามารถเข้าถึงตัวแปรภายนอกและสามารถส่งคืนชื่อเต็มได้ ฟังก์ชั่นที่ซ้อนกันเป็นเรื่องปกติใน JavaScript
สิ่งที่น่าสนใจกว่านั้นคือฟังก์ชันที่ซ้อนกันสามารถส่งคืนได้: เป็นคุณสมบัติของอ็อบเจ็กต์ใหม่หรือเป็นผลจากตัวมันเอง จากนั้นจึงนำไปใช้ที่อื่นได้ ไม่ว่าจะอยู่ที่ไหนก็ยังสามารถเข้าถึงตัวแปรภายนอกเหมือนเดิมได้
ด้านล่างนี้ makeCounter
สร้างฟังก์ชัน "ตัวนับ" ที่ส่งคืนหมายเลขถัดไปในการเรียกใช้แต่ละครั้ง:
ฟังก์ชั่น makeCounter() { ให้นับ = 0; ฟังก์ชันส่งคืน () { จำนวนการส่งคืน++; - - ให้เคาน์เตอร์ = makeCounter(); การแจ้งเตือน( ตัวนับ() ); // 0 การแจ้งเตือน( ตัวนับ() ); // 1 การแจ้งเตือน( ตัวนับ() ); // 2
แม้ว่าโค้ดนั้นจะเรียบง่าย แต่มีการปรับเปลี่ยนเล็กน้อยของโค้ดนั้นก็มีประโยชน์ในทางปฏิบัติ เช่น เป็นตัวสร้างตัวเลขสุ่มเพื่อสร้างค่าสุ่มสำหรับการทดสอบอัตโนมัติ
มันทำงานอย่างไร? ถ้าเราสร้างเคาน์เตอร์หลายตัว มันจะเป็นอิสระจากกันหรือไม่? เกิดอะไรขึ้นกับตัวแปรตรงนี้?
การทำความเข้าใจสิ่งเหล่านี้เป็นสิ่งที่ดีสำหรับความรู้โดยรวมของ JavaScript และเป็นประโยชน์สำหรับสถานการณ์ที่ซับซ้อนมากขึ้น เรามาเจาะลึกกันสักหน่อย
นี่จะเป็นมังกร!
คำอธิบายทางเทคนิคเชิงลึกรออยู่ข้างหน้า
เท่าที่ฉันต้องการหลีกเลี่ยงรายละเอียดภาษาระดับต่ำ ความเข้าใจใดๆ ที่ไม่มีรายละเอียดเหล่านั้นจะขาดและไม่สมบูรณ์ ดังนั้นเตรียมตัวให้พร้อม
เพื่อความชัดเจน การอธิบายจึงแบ่งออกเป็นหลายขั้นตอน
ใน JavaScript ทุกฟังก์ชันที่ทำงานอยู่ บล็อกโค้ด {...}
และสคริปต์โดยรวมมีอ็อบเจ็กต์ที่เกี่ยวข้องภายใน (ซ่อน) ที่เรียกว่า Lexical Environment
ออบเจ็กต์ Lexical Environment ประกอบด้วยสองส่วน:
บันทึกสภาพแวดล้อม – ออบเจ็กต์ที่เก็บตัวแปรท้องถิ่นทั้งหมดเป็นคุณสมบัติ (และข้อมูลอื่น ๆ เช่นค่าของ this
)
การอ้างอิงถึง สภาพแวดล้อมคำศัพท์ภายนอก ที่เกี่ยวข้องกับโค้ดภายนอก
“ตัวแปร” เป็นเพียงคุณสมบัติของวัตถุภายในพิเศษ Environment Record
“การรับหรือเปลี่ยนแปลงตัวแปร” หมายถึง “การรับหรือเปลี่ยนแปลงคุณสมบัติของวัตถุนั้น”
ในโค้ดง่ายๆ ที่ไม่มีฟังก์ชันนี้ จะมี Lexical Environment เพียงอันเดียวเท่านั้น:
นี่คือสิ่งที่เรียกว่า Global Lexical Environment ซึ่งเกี่ยวข้องกับสคริปต์ทั้งหมด
ในภาพด้านบน สี่เหลี่ยมผืนผ้าหมายถึงบันทึกสภาพแวดล้อม (การจัดเก็บตัวแปร) และลูกศรหมายถึงการอ้างอิงภายนอก Global Lexical Environment ไม่มีการอ้างอิงภายนอก นั่นคือสาเหตุที่ลูกศรชี้ไปที่ null
เมื่อโค้ดเริ่มทำงานและดำเนินต่อไป สภาพแวดล้อมของคำศัพท์จะเปลี่ยนไป
นี่คือโค้ดที่ยาวกว่านี้อีกเล็กน้อย:
สี่เหลี่ยมทางด้านขวามือแสดงให้เห็นว่าสภาพแวดล้อมของคำศัพท์ทั่วโลกเปลี่ยนแปลงไปอย่างไรในระหว่างการดำเนินการ:
เมื่อสคริปต์เริ่มทำงาน Lexical Environment จะถูกเติมไว้ล่วงหน้าด้วยตัวแปรที่ประกาศไว้ทั้งหมด
ในตอนแรก พวกเขาอยู่ในสถานะ "ไม่ได้เตรียมใช้งาน" นั่นเป็นสถานะภายในพิเศษ หมายความว่าเอ็นจิ้นรู้เกี่ยวกับตัวแปร แต่ไม่สามารถอ้างอิงได้จนกว่าจะมีการประกาศด้วย let
มันเกือบจะเหมือนกับว่าไม่มีตัวแปรอยู่
จากนั้น let phrase
ปรากฏขึ้น ยังไม่มีการกำหนด ดังนั้นค่าของมันจึง undefined
เราสามารถใช้ตัวแปรตั้งแต่จุดนี้เป็นต้นไป
phrase
ได้รับการกำหนดค่า
phrase
จะเปลี่ยนค่า
ทุกอย่างดูเรียบง่ายในตอนนี้ใช่ไหม?
ตัวแปรเป็นคุณสมบัติของอ็อบเจ็กต์ภายในพิเศษ ที่เกี่ยวข้องกับบล็อก/ฟังก์ชัน/สคริปต์ที่ดำเนินการอยู่ในปัจจุบัน
การทำงานกับตัวแปรเป็นการทำงานกับคุณสมบัติของวัตถุนั้นจริงๆ
Lexical Environment เป็นวัตถุข้อกำหนด
“สภาพแวดล้อมของคำศัพท์” เป็นวัตถุข้อกำหนด: มีอยู่เฉพาะ “ตามทฤษฎี” ในข้อกำหนดภาษาเพื่ออธิบายวิธีการทำงานของสิ่งต่าง ๆ เราไม่สามารถรับวัตถุนี้ในโค้ดของเราและจัดการมันได้โดยตรง
เอ็นจิ้น JavaScript ยังอาจปรับให้เหมาะสม ละทิ้งตัวแปรที่ไม่ได้ใช้เพื่อประหยัดหน่วยความจำ และดำเนินการลูกเล่นภายในอื่น ๆ ตราบใดที่พฤติกรรมที่มองเห็นยังคงอยู่ตามที่อธิบายไว้
ฟังก์ชันก็เป็นค่าเช่นเดียวกับตัวแปร
ข้อแตกต่างก็คือการประกาศฟังก์ชันจะเริ่มทำงานโดยสมบูรณ์ทันที
เมื่อมีการสร้างสภาพแวดล้อมของคำศัพท์ การประกาศฟังก์ชันจะกลายเป็นฟังก์ชันที่พร้อมใช้งานทันที (ต่างจาก let
ซึ่งไม่สามารถใช้งานได้จนกว่าจะมีการประกาศ)
นั่นเป็นเหตุผลที่เราสามารถใช้ฟังก์ชันที่ประกาศเป็นการประกาศฟังก์ชันได้ แม้กระทั่งก่อนที่จะมีการประกาศด้วยซ้ำ
ตัวอย่างเช่น นี่คือสถานะเริ่มต้นของ Global Lexical Environment เมื่อเราเพิ่มฟังก์ชัน:
โดยปกติแล้ว ลักษณะการทำงานนี้จะใช้ได้กับการประกาศฟังก์ชันเท่านั้น ไม่ใช่ Function Expressions ที่เรากำหนดฟังก์ชันให้กับตัวแปร เช่น let say = function(name)...
เมื่อฟังก์ชันทำงาน เมื่อเริ่มต้นการโทร Lexical Environment ใหม่จะถูกสร้างขึ้นโดยอัตโนมัติเพื่อจัดเก็บตัวแปรภายในเครื่องและพารามิเตอร์ของการโทร
ตัวอย่างเช่น สำหรับ say("John")
ดูเหมือนว่านี้ (การดำเนินการอยู่ที่บรรทัด โดยมีป้ายกำกับด้วยลูกศร):
ในระหว่างการเรียกใช้ฟังก์ชัน เรามี Lexical Environment สองแบบ: แบบภายใน (สำหรับการเรียกใช้ฟังก์ชัน) และแบบด้านนอก (สากล):
สภาพแวดล้อมคำศัพท์ภายในสอดคล้องกับการดำเนินการปัจจุบันของ say
มันมีคุณสมบัติเดียว: name
อาร์กิวเมนต์ของฟังก์ชัน เราเรียกว่า say("John")
ดังนั้นค่าของ name
คือ "John"
สภาพแวดล้อมของคำศัพท์ภายนอกคือสภาพแวดล้อมของคำศัพท์ทั่วโลก มี phrase
และฟังก์ชันในตัวมันเอง
สภาพแวดล้อมคำศัพท์ภายในมีการอ้างอิงถึง outer
เมื่อโค้ดต้องการเข้าถึงตัวแปร - ระบบจะค้นหา Lexical Environment ภายในก่อน จากนั้นจึงค้นหาอันนอก จากนั้นจึงค้นหาอันนอกมากขึ้นเรื่อยๆ จนกระทั่งเป็นโกลบอล
หากไม่พบตัวแปรที่ใดเลย นั่นเป็นข้อผิดพลาดในโหมดเข้มงวด (หากไม่ use strict
การกำหนดให้กับตัวแปรที่ไม่มีอยู่จะสร้างตัวแปรโกลบอลใหม่ เพื่อให้เข้ากันได้กับโค้ดเก่า)
ในตัวอย่างนี้ การค้นหาจะดำเนินการดังนี้:
สำหรับตัวแปร name
alert
ภายใน say
จะค้นหาทันทีใน Lexical Environment ภายใน
เมื่อต้องการเข้าถึง phrase
จะไม่มี phrase
ในเครื่อง ดังนั้นจึงเป็นไปตามการอ้างอิงถึงสภาพแวดล้อมคำศัพท์ภายนอกและค้นหาที่นั่น
กลับไปที่ตัวอย่าง makeCounter
กัน
ฟังก์ชั่น makeCounter() { ให้นับ = 0; ฟังก์ชันส่งคืน () { จำนวนการส่งคืน++; - - ให้เคาน์เตอร์ = makeCounter();
ที่จุดเริ่มต้นของการเรียก makeCounter()
แต่ละครั้ง จะมีการสร้างอ็อบเจ็กต์ Lexical Environment ใหม่เพื่อจัดเก็บตัวแปรสำหรับการรัน makeCounter
นี้
ดังนั้นเราจึงมี Lexical Environments ที่ซ้อนกันสองรายการ เหมือนกับในตัวอย่างด้านบน:
สิ่งที่แตกต่างก็คือ ในระหว่างการดำเนินการ makeCounter()
ฟังก์ชันที่ซ้อนกันเล็กๆ จะถูกสร้างขึ้นจากบรรทัดเดียวเท่านั้น: return count++
เรายังไม่ได้ดำเนินการ เพียงสร้างเท่านั้น
ฟังก์ชั่นทั้งหมดจะจดจำสภาพแวดล้อมของคำศัพท์ที่พวกเขาสร้างขึ้น ในทางเทคนิคแล้ว ไม่มีเวทย์มนตร์ที่นี่: ฟังก์ชั่นทั้งหมดมีคุณสมบัติที่ซ่อนอยู่ชื่อ [[Environment]]
ซึ่งเก็บการอ้างอิงถึงสภาพแวดล้อมคำศัพท์ที่ฟังก์ชั่นถูกสร้างขึ้น:
ดังนั้น counter.[[Environment]]
มีการอ้างอิงถึง {count: 0}
สภาพแวดล้อมคำศัพท์ นั่นคือวิธีที่ฟังก์ชันจดจำตำแหน่งที่สร้างขึ้น ไม่ว่าจะเรียกที่ไหนก็ตาม การอ้างอิง [[Environment]]
ได้รับการตั้งค่าเพียงครั้งเดียวและตลอดไป ณ เวลาที่สร้างฟังก์ชัน
ต่อมาเมื่อมีการเรียก counter()
จะมีการสร้าง Lexical Environment ใหม่สำหรับการโทร และการอ้างอิง Lexical Environment ภายนอกจะถูกนำมาจาก counter.[[Environment]]
:
ตอนนี้เมื่อโค้ดภายใน counter()
ค้นหาตัวแปร count
ก่อนอื่นมันจะค้นหา Lexical Environment ของตัวเอง (ว่างเปล่าเนื่องจากไม่มีตัวแปรในเครื่อง) จากนั้นจึงค้นหา Lexical Environment ของการเรียก makeCounter()
ภายนอก ซึ่งมันจะค้นหาและเปลี่ยนแปลงมัน .
ตัวแปรได้รับการอัปเดตใน Lexical Environment ที่ตัวแปรนั้นอยู่
นี่คือสถานะหลังจากการประหารชีวิต:
หากเราเรียก counter()
หลายครั้ง ตัวแปร count
จะเพิ่มขึ้นเป็น 2
, 3
และต่อๆ ไปในตำแหน่งเดียวกัน
ปิด
มีคำศัพท์การเขียนโปรแกรมทั่วไปว่า "การปิด" ที่นักพัฒนาโดยทั่วไปควรรู้
การปิดเป็นฟังก์ชันที่จดจำตัวแปรภายนอกและสามารถเข้าถึงได้ ในบางภาษานั้นเป็นไปไม่ได้ หรือฟังก์ชันควรเขียนด้วยวิธีพิเศษเพื่อให้เกิดขึ้นได้ แต่ตามที่อธิบายไว้ข้างต้น ใน JavaScript ฟังก์ชันทั้งหมดจะถูกปิดตามธรรมชาติ (มีข้อยกเว้นเพียงข้อเดียวเท่านั้นที่จะกล่าวถึงในไวยากรณ์ "ฟังก์ชันใหม่")
นั่นคือ: พวกเขาจะจำโดยอัตโนมัติว่าพวกเขาถูกสร้างขึ้นที่ไหนโดยใช้คุณสมบัติ [[Environment]]
ที่ซ่อนอยู่ จากนั้นโค้ดของพวกเขาจะสามารถเข้าถึงตัวแปรภายนอกได้
ในการสัมภาษณ์ นักพัฒนาส่วนหน้าจะได้รับคำถามเกี่ยวกับ "การปิดคืออะไร" คำตอบที่ถูกต้องคือคำจำกัดความของการปิดและคำอธิบายว่าฟังก์ชันทั้งหมดใน JavaScript คือการปิด และอาจมีคำอีกสองสามคำเกี่ยวกับรายละเอียดทางเทคนิค: คุณสมบัติ [[Environment]]
และวิธีการทำงานของสภาพแวดล้อมของคำศัพท์
โดยปกติแล้ว Lexical Environment จะถูกลบออกจากหน่วยความจำพร้อมกับตัวแปรทั้งหมดหลังจากการเรียกใช้ฟังก์ชันเสร็จสิ้น นั่นเป็นเพราะไม่มีการอ้างอิงถึงมัน เนื่องจากออบเจ็กต์ JavaScript ใดๆ มันจะถูกเก็บไว้ในหน่วยความจำในขณะที่สามารถเข้าถึงได้เท่านั้น
อย่างไรก็ตาม หากมีฟังก์ชันแบบซ้อนที่ยังคงสามารถเข้าถึงได้หลังจากสิ้นสุดฟังก์ชัน ฟังก์ชันนั้นจะมีคุณสมบัติ [[Environment]]
ที่อ้างอิงถึงสภาพแวดล้อมของคำศัพท์
ในกรณีนั้น Lexical Environment ยังคงสามารถเข้าถึงได้แม้หลังจากฟังก์ชันเสร็จสิ้นแล้ว ดังนั้นมันจึงยังคงอยู่
ตัวอย่างเช่น:
ฟังก์ชัน ฉ() { ให้ค่า = 123; ฟังก์ชันส่งคืน () { การแจ้งเตือน (ค่า); - - ให้ g = f(); // g.[[สิ่งแวดล้อม]] เก็บการอ้างอิงถึงสภาพแวดล้อมของคำศัพท์ // ของการเรียก f() ที่สอดคล้องกัน
โปรดทราบว่าหาก f()
ถูกเรียกหลายครั้ง และฟังก์ชันผลลัพธ์ถูกบันทึก ออบเจ็กต์ Lexical Environment ที่เกี่ยวข้องทั้งหมดจะถูกเก็บไว้ในหน่วยความจำด้วย ในโค้ดด้านล่างทั้ง 3 รายการ:
ฟังก์ชัน ฉ() { ให้ค่า = Math.random(); ฟังก์ชั่นส่งคืน () { การแจ้งเตือน (ค่า); - - // 3 ฟังก์ชันในอาร์เรย์ โดยแต่ละฟังก์ชันจะลิงก์ไปยัง Lexical Environment // จากการรัน f() ที่สอดคล้องกัน ให้ arr = [f(), f(), f()];
วัตถุสภาพแวดล้อมคำศัพท์จะตายเมื่อไม่สามารถเข้าถึงได้ (เช่นเดียวกับวัตถุอื่น ๆ ) กล่าวอีกนัยหนึ่ง มันมีอยู่เฉพาะเมื่อมีฟังก์ชันที่ซ้อนกันอย่างน้อยหนึ่งฟังก์ชันที่อ้างอิงถึงฟังก์ชันนั้น
ในโค้ดด้านล่าง หลังจากลบฟังก์ชันที่ซ้อนกันแล้ว Lexical Environment ที่ล้อมรอบไว้ (และด้วยเหตุนี้ value
) จะถูกล้างออกจากหน่วยความจำ:
ฟังก์ชัน ฉ() { ให้ค่า = 123; ฟังก์ชันส่งคืน () { การแจ้งเตือน (ค่า); - - ให้ g = f(); // ในขณะที่มีฟังก์ชัน g ค่าจะยังคงอยู่ในหน่วยความจำ ก. = โมฆะ; // ...และตอนนี้หน่วยความจำก็ถูกล้างแล้ว
ดังที่เราได้เห็นมาแล้ว ในทางทฤษฎีในขณะที่ฟังก์ชันยังมีชีวิตอยู่ ตัวแปรภายนอกทั้งหมดจะยังคงอยู่เช่นกัน
แต่ในทางปฏิบัติ เอ็นจิ้น JavaScript พยายามปรับให้เหมาะสม พวกเขาวิเคราะห์การใช้งานตัวแปร และหากเห็นได้ชัดจากโค้ดว่าไม่ได้ใช้ตัวแปรภายนอก ตัวแปรนั้นก็จะถูกลบออก
ผลข้างเคียงที่สำคัญใน V8 (Chrome, Edge, Opera) ก็คือตัวแปรดังกล่าวจะไม่พร้อมใช้งานในการดีบัก
ลองเรียกใช้ตัวอย่างด้านล่างใน Chrome โดยเปิดเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์
เมื่อหยุดชั่วคราวในประเภทคอนโซล alert(value)
ฟังก์ชัน ฉ() { ให้ค่า = Math.random(); ฟังก์ชั่น g() { ดีบักเกอร์; // ในคอนโซล: พิมพ์ alert(value); ไม่มีตัวแปรดังกล่าว! - กลับกรัม; - ให้ g = f(); ก.();
อย่างที่คุณเห็น – ไม่มีตัวแปรดังกล่าว! ตามทฤษฎีแล้ว มันควรจะสามารถเข้าถึงได้ แต่เครื่องยนต์ได้ปรับให้เหมาะสมที่สุด
นั่นอาจนำไปสู่ปัญหาการแก้ไขข้อบกพร่องที่ตลก (หากไม่ใช้เวลานานขนาดนั้น) หนึ่งในนั้น – เราสามารถเห็นตัวแปรภายนอกที่มีชื่อเดียวกันแทนที่จะเป็นตัวแปรที่คาดไว้:
ให้ค่า = "เซอร์ไพรส์!"; ฟังก์ชัน ฉ() { ให้ค่า = "ค่าที่ใกล้เคียงที่สุด"; ฟังก์ชั่น g() { ดีบักเกอร์; // ในคอนโซล: พิมพ์ alert(value); เซอร์ไพรส์! - กลับกรัม; - ให้ g = f(); ก.();
ฟีเจอร์นี้ของ V8 เป็นเรื่องที่น่ารู้ หากคุณกำลังแก้ไขจุดบกพร่องด้วย Chrome/Edge/Opera ไม่ช้าก็เร็วคุณจะพบกับมัน
นั่นไม่ใช่จุดบกพร่องในตัวดีบักเกอร์ แต่เป็นคุณสมบัติพิเศษของ V8 บางทีมันอาจจะมีการเปลี่ยนแปลงในบางครั้ง คุณสามารถตรวจสอบได้ตลอดเวลาโดยเรียกใช้ตัวอย่างในหน้านี้
ความสำคัญ: 5
ฟังก์ชัน sayHi ใช้ชื่อตัวแปรภายนอก เมื่อฟังก์ชันทำงานจะใช้ค่าใด
ให้ชื่อ = "จอห์น"; ฟังก์ชั่น sayHi() { alert("สวัสดี" + ชื่อ); - ชื่อ = "พีท"; พูดว่าสวัสดี(); // จะแสดงอะไร "จอห์น" หรือ "พีท"?
สถานการณ์ดังกล่าวเป็นเรื่องปกติทั้งในการพัฒนาเบราว์เซอร์และฝั่งเซิร์ฟเวอร์ ฟังก์ชันอาจถูกกำหนดเวลาให้ดำเนินการช้ากว่าที่ถูกสร้างขึ้น เช่น หลังจากการกระทำของผู้ใช้หรือคำขอเครือข่าย
ดังนั้นคำถามคือ: มันรับการเปลี่ยนแปลงล่าสุดหรือไม่?
คำตอบคือ: พีท
ฟังก์ชันรับตัวแปรภายนอกตามที่เป็นอยู่ในปัจจุบัน โดยจะใช้ค่าล่าสุด
ค่าตัวแปรเก่าจะไม่ถูกบันทึกไว้ที่ใดเลย เมื่อฟังก์ชันต้องการตัวแปร ฟังก์ชันจะใช้ค่าปัจจุบันจาก Lexical Environment ของตัวเองหรือค่าภายนอก
ความสำคัญ: 5
ฟังก์ชัน makeWorker
ด้านล่างสร้างฟังก์ชันอื่นและส่งคืนฟังก์ชันนั้น ฟังก์ชันใหม่นั้นสามารถเรียกใช้จากที่อื่นได้
มันจะสามารถเข้าถึงตัวแปรภายนอกจากตำแหน่งที่สร้าง หรือตำแหน่งการร้องขอ หรือทั้งสองอย่างหรือไม่?
ฟังก์ชั่น makeWorker() { ให้ชื่อ = "พีท"; ฟังก์ชันส่งคืน () { การแจ้งเตือน (ชื่อ); - - ให้ชื่อ = "จอห์น"; // สร้างฟังก์ชั่น ปล่อยให้ทำงาน = makeWorker(); //เรียกมันว่า งาน(); //มันจะแสดงอะไร?
จะแสดงค่าใด? “พีท” หรือ “จอห์น”?
คำตอบคือ: พีท
ฟังก์ชัน work()
ในโค้ดด้านล่างได้รับ name
จากแหล่งกำเนิดผ่านการอ้างอิงสภาพแวดล้อมคำศัพท์ภายนอก:
ผลลัพธ์ก็คือ "Pete"
นั่นเอง
แต่ถ้าไม่มี let name
ใน makeWorker()
การค้นหาก็จะออกไปข้างนอกและรับตัวแปรโกลบอลดังที่เราเห็นจากห่วงโซ่ด้านบน ในกรณีนี้ผลลัพธ์จะเป็น "John"
ความสำคัญ: 5
ที่นี่เราสร้างตัวนับสองตัว: counter
และ counter2
โดยใช้ฟังก์ชัน makeCounter
เดียวกัน
พวกเขาเป็นอิสระหรือไม่? ตัวนับที่สองจะแสดงอะไร? 0,1
หรือ 2,3
หรืออย่างอื่น?
ฟังก์ชั่น makeCounter() { ให้นับ = 0; ฟังก์ชันส่งคืน () { จำนวนการส่งคืน++; - - ให้เคาน์เตอร์ = makeCounter(); ให้ counter2 = makeCounter(); การแจ้งเตือน( ตัวนับ() ); // 0 การแจ้งเตือน( ตัวนับ() ); // 1 การแจ้งเตือน( counter2() ); - การแจ้งเตือน( counter2() ); -
คำตอบ: 0,1
ฟังก์ชัน counter
และ counter2
ถูกสร้างขึ้นโดยการเรียกใช้ makeCounter
ที่แตกต่างกัน
ดังนั้นพวกมันจึงมีสภาพแวดล้อมคำศัพท์ภายนอกที่เป็นอิสระ โดยแต่ละตัวมี count
ของมันเอง
ความสำคัญ: 5
ที่นี่วัตถุตัวนับถูกสร้างขึ้นด้วยความช่วยเหลือของฟังก์ชันตัวสร้าง
มันจะได้ผลไหม? มันจะแสดงอะไร?
ฟังก์ชั่นตัวนับ () { ให้นับ = 0; นี้.up = ฟังก์ชั่น() { ส่งคืน ++ นับ; - นี้.ลง = ฟังก์ชั่น() { กลับ --นับ; - - ให้เคาน์เตอร์ = ตัวนับใหม่ (); การแจ้งเตือน( counter.up() ); - การแจ้งเตือน( counter.up() ); - การแจ้งเตือน( counter.down() ); -
แน่นอนมันจะทำงานได้ดี
ฟังก์ชันที่ซ้อนกันทั้งสองฟังก์ชันถูกสร้างขึ้นภายใน Lexical Environment ภายนอกเดียวกัน ดังนั้นจึงแชร์การเข้าถึงตัวแปร count
เดียวกัน:
ฟังก์ชั่นตัวนับ () { ให้นับ = 0; นี้.up = ฟังก์ชั่น() { ส่งคืน ++ นับ; - นี้.ลง = ฟังก์ชั่น() { กลับ --นับ; - - ให้เคาน์เตอร์ = ตัวนับใหม่ (); การแจ้งเตือน( counter.up() ); // 1 การแจ้งเตือน( counter.up() ); // 2 การแจ้งเตือน( counter.down() ); // 1
ความสำคัญ: 5
ดูรหัสสิ การโทรสายสุดท้ายจะมีผลอย่างไร?
ให้วลี = "สวัสดี"; ถ้า (จริง) { ให้ผู้ใช้ = "จอห์น"; ฟังก์ชั่น sayHi() { alert(`${วลี}, ${user}`); - - พูดว่าสวัสดี();
ผลลัพธ์ที่ได้คือ ข้อผิดพลาด
ฟังก์ชัน sayHi
ได้รับการประกาศไว้ภายใน if
ดังนั้นฟังก์ชันจะอยู่ภายในฟังก์ชันนั้นเท่านั้น ไม่มี sayHi
ข้างนอก
ความสำคัญ: 4
เขียนฟังก์ชัน sum
ที่ทำงานดังนี้: sum(a)(b) = a+b
ใช่ ด้วยวิธีนี้ โดยใช้วงเล็บคู่ (ไม่ใช่การพิมพ์ผิด)
ตัวอย่างเช่น:
ผลรวม(1)(2) = 3 ผลรวม(5)(-1) = 4
เพื่อให้วงเล็บที่สองใช้งานได้ วงเล็บแรกจะต้องส่งคืนฟังก์ชัน
แบบนี้:
ผลรวมฟังก์ชัน (a) { ฟังก์ชั่นส่งคืน (b) { กลับ + b; // รับ "a" จากสภาพแวดล้อมคำศัพท์ภายนอก - - การแจ้งเตือน( ผลรวม(1)(2) ); // 3 การแจ้งเตือน( ผลรวม(5)(-1) ); // 4
ความสำคัญ: 4
รหัสนี้จะส่งผลอย่างไร?
ให้ x = 1; ฟังก์ชั่น func() { console.log(x); - ให้ x = 2; - ฟังก์ชั่น();
PS มีข้อผิดพลาดในงานนี้ วิธีแก้ปัญหาไม่ชัดเจน
ผลลัพธ์คือ: ข้อผิดพลาด
ลองเรียกใช้:
ให้ x = 1; ฟังก์ชั่น func() { console.log(x); // ReferenceError: ไม่สามารถเข้าถึง 'x' ก่อนการเริ่มต้น ให้ x = 2; - ฟังก์ชั่น();
ในตัวอย่างนี้ เราสามารถสังเกตความแตกต่างที่แปลกประหลาดระหว่างตัวแปร "ไม่มีอยู่" และ "ไม่ได้กำหนดค่าเริ่มต้น"
ตามที่คุณอาจได้อ่านในบทความ Variable scope, closure, ตัวแปรจะเริ่มต้นในสถานะ "ไม่ได้เตรียมใช้งาน" นับจากช่วงเวลาที่การดำเนินการเข้าสู่บล็อกโค้ด (หรือฟังก์ชัน) และมันจะคงสถานะไม่เริ่มต้นจนกระทั่งคำสั่ง let
สอดคล้องกัน
กล่าวอีกนัยหนึ่ง ตัวแปรนั้นมีอยู่ในทางเทคนิค แต่ไม่สามารถนำมาใช้ก่อน let
ได้
รหัสด้านบนแสดงให้เห็น
ฟังก์ชั่น func() { // ตัวแปรโลคัล x เป็นที่รู้จักของเอ็นจิ้นตั้งแต่เริ่มต้นฟังก์ชัน // แต่ "ไม่ได้กำหนดค่าเริ่มต้น" (ใช้ไม่ได้) จนกระทั่งให้ ("โซนตาย") // ดังนั้นข้อผิดพลาด console.log(x); // ReferenceError: ไม่สามารถเข้าถึง 'x' ก่อนการเริ่มต้น ให้ x = 2; -
โซนของความไม่สามารถใช้งานได้ชั่วคราวของตัวแปรนี้ (ตั้งแต่จุดเริ่มต้นของบล็อกโค้ดจนถึง let
) บางครั้งเรียกว่า "โซนตาย"
ความสำคัญ: 5
เรามีเมธอด arr.filter(f)
ในตัวสำหรับอาร์เรย์ มันกรององค์ประกอบทั้งหมดผ่านฟังก์ชัน f
หากส่งคืนค่า true
แสดงว่าองค์ประกอบนั้นจะถูกส่งกลับในอาร์เรย์ผลลัพธ์
สร้างชุดตัวกรอง "พร้อมใช้งาน":
inBetween(a, b)
– ระหว่าง a
และ b
หรือเท่ากับ (รวม)
inArray([...])
– ในอาร์เรย์ที่กำหนด
การใช้งานต้องเป็นดังนี้:
arr.filter(inBetween(3,6))
– เลือกเฉพาะค่าระหว่าง 3 ถึง 6
arr.filter(inArray([1,2,3]))
– เลือกเฉพาะองค์ประกอบที่ตรงกับหนึ่งในสมาชิกของ [1,2,3]
ตัวอย่างเช่น:
/* .. รหัสของคุณสำหรับ inBetween และ inArray */ ให้ arr = [1, 2, 3, 4, 5, 6, 7]; การแจ้งเตือน ( arr.filter (ระหว่าง (3, 6)) ); // 3,4,5,6 การแจ้งเตือน( arr.filter(inArray([1, 2, 10])) ); // 1,2
เปิดแซนด์บ็อกซ์พร้อมการทดสอบ
ฟังก์ชั่นระหว่าง (a, b) { ฟังก์ชั่นส่งคืน (x) { กลับ x >= a && x <= b; - - ให้ arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(ระหว่าง(3, 6)) ); // 3,4,5,6
ฟังก์ชั่น inArray (arr) { ฟังก์ชันส่งคืน (x) { กลับ arr.includes (x); - - ให้ arr = [1, 2, 3, 4, 5, 6, 7]; การแจ้งเตือน( arr.filter(inArray([1, 2, 10])) ); // 1,2
เปิดโซลูชันพร้อมการทดสอบในแซนด์บ็อกซ์
ความสำคัญ: 5
เรามีอาร์เรย์ของวัตถุที่จะจัดเรียง:
ให้ผู้ใช้ = [ { ชื่อ: "จอห์น" อายุ: 20 นามสกุล: "จอห์นสัน" } { ชื่อ: "พีท" อายุ: 18 นามสกุล: "ปีเตอร์สัน" } { ชื่อ: "แอน" อายุ: 19 นามสกุล: "แฮธาเวย์" } -
วิธีปกติในการทำเช่นนั้นจะเป็น:
// ตามชื่อ (แอน, จอห์น, พีท) users.sort((a, b) => a.name > b.name ? 1 : -1); // ตามอายุ (พีท, แอน, จอห์น) users.sort((a, b) => a.age > b.age ? 1 : -1);
เราทำให้มันละเอียดน้อยลงแบบนี้ได้ไหม?
users.sort(byField('ชื่อ')); users.sort(byField('อายุ'));
ดังนั้น แทนที่จะเขียนฟังก์ชัน ให้ใส่ byField(fieldName)
แทน
เขียนฟังก์ชัน byField
ที่สามารถใช้สำหรับสิ่งนั้นได้
เปิดแซนด์บ็อกซ์พร้อมการทดสอบ
ฟังก์ชั่นโดยField (ชื่อฟิลด์) { กลับ (a, b) => a[fieldName] > b[fieldName] ? 1 : -1; -
เปิดโซลูชันพร้อมการทดสอบในแซนด์บ็อกซ์
ความสำคัญ: 5
รหัสต่อไปนี้สร้างอาร์เรย์ของ shooters
ทุกฟังก์ชั่นมีไว้เพื่อส่งออกหมายเลขของมัน แต่มีบางอย่างผิดปกติ…
ฟังก์ชั่น makeArmy() { ให้นักกีฬา = []; ให้ฉัน = 0; ในขณะที่ (ฉัน < 10) { ให้ Shooter = function() { // สร้างฟังก์ชัน Shooter แจ้งเตือน (ฉัน); // นั่นควรแสดงหมายเลขของมัน - Shooters.push (นักกีฬา); // และเพิ่มเข้าไปในอาร์เรย์ ฉัน++; - // ...และส่งคืนอาร์เรย์ของนักกีฬา นักกีฬากลับ; - ให้กองทัพ = makeArmy(); // นักกีฬาทุกคนแสดง 10 แทนที่จะเป็นตัวเลข 0, 1, 2, 3... กองทัพบก[0](); // 10 จากมือปืนหมายเลข 0 กองทัพบก[1](); // 10 จากมือปืนหมายเลข 1 กองทัพ[2](); // 10 ...และอื่นๆ
ทำไมนักกีฬาทุกคนถึงแสดงค่าเท่ากัน?
แก้ไขโค้ดเพื่อให้ทำงานได้ตามที่ตั้งใจไว้
เปิดแซนด์บ็อกซ์พร้อมการทดสอบ
มาตรวจสอบว่าเกิดอะไรขึ้นภายใน makeArmy
กัน แล้ววิธีแก้ปัญหาจะชัดเจน
มันสร้าง shooters
อาเรย์ว่างเปล่า:
ให้นักกีฬา = [];
เติมฟังก์ชันผ่าน shooters.push(function)
ในลูป
ทุกองค์ประกอบเป็นฟังก์ชัน ดังนั้นอาร์เรย์ผลลัพธ์จะมีลักษณะดังนี้:
นักกีฬา = [ ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - ฟังก์ชั่น () { การแจ้งเตือน (i); - -
อาร์เรย์จะถูกส่งกลับจากฟังก์ชัน
จากนั้นในภายหลังการเรียกสมาชิกใดๆ เช่น army[5]()
จะได้รับองค์ประกอบ army[5]
จากอาร์เรย์ (ซึ่งเป็นฟังก์ชัน) และเรียกมัน
ทีนี้เหตุใดฟังก์ชันทั้งหมดจึงแสดงค่าเดียวกัน 10
?
นั่นเป็นเพราะว่าไม่มีตัวแปรในเครื่อง i
ภายในฟังก์ชัน shooter
เมื่อฟังก์ชันดังกล่าวถูกเรียกใช้ ฟังก์ชันดังกล่าวจะนำ i
มาจากสภาพแวดล้อมคำศัพท์ภายนอก
แล้ว i
จะมีค่าเท่าไร?
หากเราดูแหล่งที่มา:
ฟังก์ชั่น makeArmy() { - ให้ฉัน = 0; ในขณะที่ (ฉัน < 10) { ให้ Shooter = function() { // ฟังก์ชั่น Shooter การแจ้งเตือน (ฉัน); // ควรแสดงหมายเลขของมัน - Shooters.push (นักกีฬา); // เพิ่มฟังก์ชันให้กับอาร์เรย์ ฉัน++; - - -
เราจะเห็นว่าฟังก์ชั่น shooter
ทั้งหมดถูกสร้างขึ้นในสภาพแวดล้อมคำศัพท์ของฟังก์ชัน makeArmy()
แต่เมื่อเรียก army[5]()
makeArmy
ได้ทำงานเสร็จแล้ว และค่าสุดท้ายของ i
คือ 10
( while
หยุดที่ i=10
)
ด้วยเหตุนี้ ฟังก์ชัน shooter
ทั้งหมดจึงได้รับค่าเดียวกันจากสภาพแวดล้อมคำศัพท์ภายนอก และนั่นคือค่าสุดท้าย i=10
ดังที่คุณเห็นข้างต้น ในการวนซ้ำแต่ละครั้งของบล็อก while {...}
สภาพแวดล้อมคำศัพท์ใหม่จะถูกสร้างขึ้น ดังนั้น เพื่อแก้ไขปัญหานี้ เราสามารถคัดลอกค่า i
ลงในตัวแปรภายในบล็อก while {...}
ได้ดังนี้:
ฟังก์ชั่น makeArmy() { ให้นักกีฬา = []; ให้ฉัน = 0; ในขณะที่ (ฉัน < 10) { ให้เจ = ฉัน; ให้ Shooter = function() { // ฟังก์ชั่น Shooter การแจ้งเตือน (เจ); // ควรแสดงหมายเลขของมัน - Shooters.push (นักกีฬา); ฉัน++; - นักกีฬากลับ; - ให้กองทัพ = makeArmy(); // ตอนนี้โค้ดทำงานได้อย่างถูกต้อง กองทัพ[0](); // 0 กองทัพบก[5](); // 5
let j = i
ประกาศตัวแปร “iteration-local” j
และคัดลอก i
ลงไป พื้นฐานจะถูกคัดลอก "ตามค่า" ดังนั้นเราจึงได้รับสำเนาอิสระของ i
ซึ่งเป็นของการวนซ้ำปัจจุบัน
นักกีฬาทำงานได้อย่างถูกต้องเพราะตอนนี้คุณค่าของ i
อยู่ใกล้ขึ้นอีกนิด ไม่ได้อยู่ใน makeArmy()
Lexical Environment แต่อยู่ใน Lexical Environment ที่สอดคล้องกับการวนซ้ำปัจจุบัน:
ปัญหาดังกล่าวสามารถหลีกเลี่ยงได้หากเราใช้ for
ในตอนแรกเช่นนี้
ฟังก์ชั่น makeArmy() { ให้นักกีฬา = []; สำหรับ (ให้ i = 0; i < 10; i ++) { ให้ Shooter = function() { // ฟังก์ชั่น Shooter แจ้งเตือน (ฉัน); // ควรแสดงหมายเลขของมัน - Shooters.push (นักกีฬา); - นักกีฬากลับ; - ให้กองทัพ = makeArmy(); กองทัพ[0](); // 0 กองทัพบก[5](); // 5
โดยพื้นฐานแล้วจะเหมือนกัน เพราะ for
แต่ละการวนซ้ำจะสร้างสภาพแวดล้อมคำศัพท์ใหม่ โดยมีตัวแปร i
ของตัวเอง ดังนั้น shooter
ที่สร้างขึ้นในการวนซ้ำทุกครั้งจะอ้างอิง i
ของตัวเองจากการวนซ้ำครั้งนั้น
ตอนนี้ เมื่อคุณได้ใช้ความพยายามอย่างมากในการอ่านข้อความนี้ และสูตรสุดท้ายก็ง่ายมาก – แค่ใช้ for
คุณอาจสงสัยว่า – มันคุ้มไหม?
ถ้าคุณตอบคำถามได้ง่าย คุณคงไม่อ่านคำตอบ หวังว่างานนี้คงช่วยให้คุณเข้าใจสิ่งต่าง ๆ ได้ดีขึ้นเล็กน้อย
นอกจากนี้ยังมีกรณีที่เราต้องการ while
ถึง for
และสถานการณ์อื่นๆ ที่ปัญหาดังกล่าวเกิดขึ้นจริง
เปิดโซลูชันพร้อมการทดสอบในแซนด์บ็อกซ์