ज़्यादातर लोग चीज़ें बनाना शुरू करके JavaScript सीखते हैं। आप एक बटन को वायर करते हैं, कुछ डेटा फेच करते हैं, और DOM में बदलाव देखते हैं। फिर एब्स्ट्रैक्शन लीक होने लगते हैं। ऐसे बग सामने आते हैं जिनका कोई मतलब नहीं निकलता: वेरिएबल्स अपने डिक्लेरेशन से पहले ही मौजूद होते हैं, फंक्शन्स उन वेरिएबल्स को याद रखते हैं जिन्हें उनके पास एक्सेस नहीं होना चाहिए, और this कीवर्ड विंडो, एक बटन, या कुछ भी नहीं की ओर इशारा करता है। आमतौर पर यही वह क्षण होता है जब आपको एहसास होता है कि आपको इंजन के अंदर की कार्यप्रणाली को समझने की ज़रूरत है।
एग्जीक्यूशन कॉन्टेक्स्ट: दो-चरणों वाला सेटअप (Execution Context: The Two-Phase Setup)
जब आपके ब्राउज़र या Node.js में JavaScript इंजन किसी स्क्रिप्ट का सामना करता है, तो वह किसी व्यक्ति द्वारा पेज पढ़ने की तरह फ़ाइल को ऊपर से नीचे तक नहीं पढ़ता है। इसके बजाय, यह एक एग्जीक्यूशन कॉन्टेक्स्ट (execution context) बनाता है, जो एक कंटेनर की तरह है जिसमें कोड के किसी विशेष हिस्से को चलाने के लिए आवश्यक सभी चीज़ें होती हैं। प्रत्येक एग्जीक्यूशन कॉन्टेक्स्ट दो अलग-अलग चरणों से होकर गुजरता है।
मेमोरी क्रिएशन फेज (Memory Creation Phase)। इस शुरुआती पास के दौरान, इंजन पूरे स्कोप को स्कैन करता है और उसे मिलने वाले प्रत्येक वेरिएबल और फंक्शन डिक्लेरेशन के लिए मेमोरी एलोकेट करता है। यदि उसे var दिखता है, तो वह जगह सुरक्षित कर लेता है और प्लेसहोल्डर के रूप में undefined को स्टोर करता है। यदि उसे फंक्शन डिक्लेरेशन दिखता है, तो वह पूरे फंक्शन बॉडी को स्टोर कर लेता है। यही कारण है कि पारंपरिक function कीवर्ड के साथ डिक्लेअर किया गया फंक्शन उसी स्कोप में उन लाइनों से भी पहले कॉल किया जा सकता है जो ऊपर दिखाई देती हैं। इंजन इसके निष्पादन (execution) शुरू करने से पहले ही इसके बारे में जान जाता है।
कोड एग्जीक्यूशन फेज (Code Execution Phase)। अब इंजन आपके कोड को लाइन-दर-लाइन चलाता है। असाइनमेंट यहीं होते हैं। एक्सप्रेशंस को इवैल्यूएट किया जाता है। फंक्शन्स को इनवोक किया जाता है। यदि आपने var name = "Alice"; लिखा है, तो पहले चरण का प्लेसहोल्डर अंततः स्ट्रिंग से बदल दिया जाता है। इस दो-पास (two-pass) व्यवहार को समझने से बहुत सारी उलझनें दूर हो जाती हैं। इंजन लापरवाह नहीं है; वह एक सख्त सेटअप रूटीन का पालन कर रहा है।
वेरिएबल्स और टेम्पोरल डेड ज़ोन (Variables and the Temporal Dead Zone)
var, let, और const के बीच चुनाव करना केवल एक शैलीगत पसंद (stylistic preference) नहीं है। var फंक्शन-स्कोप वाला होता है, जिसका अर्थ है कि यह कर्ली ब्रेसेस (braces) को पूरी तरह से अनदेखा कर देता है। इसे किसी if ब्लॉक के अंदर डिक्लेअर करें, और यह बाहर लीक हो जाता है। यह व्यवहार शुरुआती JavaScript में तर्कसंगत हो सकता था, लेकिन आधुनिक एप्लिकेशन्स में यह मेंटेनेंस की समस्याएँ पैदा करता है। let और const ब्लॉक-स्कोप वाले होते हैं। वे कर्ली ब्रेसेस का सम्मान करते हैं और ब्लॉक समाप्त होने पर गायब हो जाते हैं।
होस्टिंग (hoisting) उन्हें कैसे ट्रीट करती है, इसमें भी एक सूक्ष्म लेकिन महत्वपूर्ण अंतर है। var डिक्लेरेशन होस्ट किए जाते हैं और तुरंत undefined के साथ इनिशियलाइज़ हो जाते हैं। let और const भी तकनीकी रूप से होस्ट किए जाते हैं; इंजन डिक्लेरेशन लाइन तक पहुँचने से पहले ही जानता है कि वे मौजूद हैं। लेकिन वे इनिशियलाइज़ नहीं होते हैं। वे टेम्पोरल डेड ज़ोन (temporal dead zone) नामक एक अनिश्चित स्थिति में रहते हैं। यदि आप डिक्लेरेशन लाइन के निष्पादित होने से पहले उन्हें पढ़ने की कोशिश करते हैं, तो आपको एक छिपे हुए undefined के बजाय एक स्पष्ट ReferenceError मिलता है। वह क्रैश वास्तव में मददगार है। यह अन-इनिशियलाइज़्ड डेटा के आधार पर लॉजिक को आगे बढ़ने से रोकता है।
लेक्सिकल स्कोप और क्लोजर्स (Lexical Scope and Closures)
स्कोप एक सरल प्रश्न का उत्तर देता है: मैं इस वेरिएबल को कहाँ एक्सेस कर सकता हूँ? JavaScript लेक्सिकल स्कोप (lexical scope) का उपयोग करता है, जिसका अर्थ है कि एक फंक्शन के एक्सेस अधिकार इस बात से तय होते हैं कि उसे सोर्स कोड में भौतिक रूप से कहाँ लिखा गया था, न कि इस बात से कि उसे कहाँ कॉल किया जा रहा है। यदि आप एक फंक्शन को दूसरे फंक्शन के अंदर परिभाषित करते हैं, तो अंदर वाला फंक्शन अपने पैरेंट से वेरिएबल्स पढ़ने के लिए बाहर तक पहुँच सकता है। बाहरी फंक्शन अंदर तक नहीं पहुँच सकता। यह संबंध स्थिर (static) है। आप उस अंदरूनी फंक्शन को मॉड्यूल्स के पार भेज सकते हैं, इसे एक ग्लोबल वेरिएबल में स्टोर कर सकते हैं, और इसे पूरी तरह से अलग फ़ाइल से कॉल कर सकते हैं। यह अभी भी उन वेरिएबल्स को याद रखता है जो उस स्कोप में थे जहाँ इसका जन्म हुआ था।
यह व्यवहार स्वाभाविक रूप से क्लोजर्स (closures) पैदा करता है। एक क्लोजर तब बनता है जब एक अंदरूनी फंक्शन अपने बाहरी स्कोप के किसी वेरिएबल का रेफरेंस रखता है। बाहरी फंक्शन के निष्पादित होने और उसके लोकल वेरिएबल्स के गार्बेज कलेक्ट (garbage collected) होने के बाद भी, JavaScript उन्हें मेमोरी में सुरक्षित रखता है क्योंकि अंदरूनी फंक्शन को अभी भी उनकी आवश्यकता होती है। अंदरूनी फंक्शन अपने आसपास के वातावरण (surrounding environment) को अपने साथ लेकर चलता है।
यह केवल एक अकादमिक विवरण नहीं है। क्लोजर्स आपको एक ऐसी भाषा में प्राइवेट स्टेट (private state) बनाने का व्यावहारिक तरीका देते हैं जिसमें स्पष्ट एक्सेस मॉडिफायर्स (access modifiers) की कमी है।
function makeCounter() {
let count = 0;
return function() {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
यहाँ, count छिपा हुआ है। रिटर्न किए गए फंक्शन के बाहर कुछ भी इसे रीसेट नहीं कर सकता या सीधे इसे पढ़ नहीं सकता। यह केवल स्कोप मैकेनिक्स से बना एक प्राइवेट वेरिएबल है।
प्रैक्टिस में होस्टिंग (Hoisting in Practice)
यह सुनना आम है कि JavaScript "declarations को ऊपर ले जाता है।" यह एक उपयोगी मानसिक मॉडल (mental model) है, लेकिन कोड वास्तव में दोबारा नहीं लिखा जाता है। मेमोरी क्रिएशन फेज (memory creation phase) के दौरान, इंजन निष्पादन (execution) शुरू होने से पहले केवल घोषणाओं (declarations) को रजिस्टर करता है। var x = 5; जैसा स्टेटमेंट ऐसा व्यवहार करता है जैसे कि declaration और initialization अलग-अलग हों। var x; वाली declaration को जल्दी प्रोसेस किया जाता है और इसे undefined के रूप में initialize किया जाता है। x = 5; वाला assignment ठीक वहीं रहता है जहाँ आपने इसे लिखा है और execution phase के दौरान चलता है।
इस कारण से, var आश्चर्यजनक परिणाम दे सकता है। किसी फंक्शन के ऊपरी हिस्से में इस्तेमाल किया गया वेरिएबल undefined हो सकता है, भले ही नीचे एक बड़ा assignment मौजूद हो। let और const का उपयोग करने से वह विशेष समस्या (foot-gun) खत्म हो जाती है क्योंकि temporal dead zone आपको अपनी declarations को usage से ऊपर रखने के लिए मजबूर करता है।
this को उसकी वैल्यू कैसे मिलती है
यदि execution context और scope यह तय करते हैं कि वेरिएबल्स कहाँ रहते हैं, तो this यह तय करता है कि वर्तमान में कौन सा ऑब्जेक्ट प्रभारी (in charge) है। Lexical variables के विपरीत, this इस बात से तय नहीं होता कि फंक्शन कहाँ लिखा गया है। यह पूरी तरह से इस बात से तय होता है कि फंक्शन को कैसे कॉल किया गया है।
Default binding तब होती है जब आप एक साधारण, स्टैंडअलोन फंक्शन को इनवोक (invoke) करते हैं। Non-strict mode में, this ग्लोबल ऑब्जेक्ट पर चला जाता है। ब्राउज़र में, वह window है। बिना किसी context के फंक्शन कॉल करने पर, आप अनजाने में ग्लोबल स्टेट को प्रभावित कर सकते हैं।
Implicit binding तब होती है जब आप किसी ऑब्जेक्ट के मेथड के रूप में फंक्शन को कॉल करते हैं। यदि आप user.sayName() लिखते हैं, तो डॉट (dot) चुपचाप इंजन को उस कॉल की अवधि के लिए this को user पर सेट करने के लिए कहता है। कॉल कहाँ की गई है, यह इस बात से अधिक महत्वपूर्ण है कि फंक्शन कहाँ परिभाषित (define) किया गया है।
Explicit binding आपको सब कुछ मैन्युअल रूप से ओवरराइड करने की अनुमति देती है। call() और apply() एक फंक्शन को तुरंत इनवोक करते हैं और साथ ही this को आपके द्वारा दिए गए एक विशिष्ट ऑब्जेक्ट पर सेट करने के लिए मजबूर करते हैं। उनके बीच एकमात्र अंतर यह है कि arguments कैसे पास किए जाते हैं: call कॉमा-सेपरेटेड लिस्ट लेता है, जबकि apply एक array लेता है। bind() अलग तरह से काम करता है। यह फंक्शन को तुरंत इनवोक नहीं करता है। इसके बजाय, यह एक नया फंक्शन लौटाता है जिसमें this स्थायी रूप से आपके द्वारा दी गई वैल्यू पर लॉक रहता है। यह उन callbacks के लिए अमूल्य है जो इधर-उधर पास किए जाने पर अपना context खो सकते हैं।
New binding तब काम आती है जब आप फंक्शन कॉल के सामने new कीवर्ड का उपयोग करते हैं। इंजन एक बिल्कुल नया खाली ऑब्जेक्ट बनाता है, उसके prototype linkage को सेट करता है, और कंस्ट्रक्टर के अंदर this को उस नए इंस्टेंस (instance) की ओर पॉइंट करता है।
ऐसा कोड लिखें जो लंबे समय तक चले
मैकेनिक्स को समझना शिल्प का केवल आधा हिस्सा है। दूसरा आधा हिस्सा ऐसा कोड लिखना है जिसे इंसान छह महीने बाद भी पढ़ सकें।
DRY (Don't Repeat Yourself) सुनने में स्पष्ट लगता है, लेकिन इसका उल्लंघन लगातार होता रहता है। यदि आप खुद को तीन अलग-अलग फाइलों में एक ही वैलिडेशन लॉजिक या API कॉल पैटर्न लिखते हुए पाते हैं, तो उसे निकाल (extract) लें। एक फंक्शन लिखें। 'एक ही सत्य का स्रोत' (One source of truth) का अर्थ है कि जब आवश्यकताएं बदलें तो अपडेट करने के लिए केवल एक ही जगह होगी, और यह समय की इतनी बचत करता है जिसे शब्दों में बयां करना मुश्किल है।
KISS (Keep It Simple, Stupid) अहंकार के खिलाफ एक बचाव है। नेस्टेड टर्नरी (Nested ternaries) और वन-लाइनर क्लोजर (one-liner closures) चालाकी भरे लग सकते हैं, लेकिन डिबगिंग के दौरान वे घंटों बर्बाद करते हैं। मैंने पहले जो क्लोजर पैटर्न दिखाया था वह शक्तिशाली है, फिर भी सिर्फ इसलिए पांच स्तरों तक नेस्टिंग करना कि आप ऐसा कर सकते हैं, एक गलती है। सरल कोड टीम के बदलने, प्रोडक्शन की समस्याओं और आधी रात के उन अलर्ट्स में भी काम आता है जब कुछ टूट जाता है और किसी को याद नहीं रहता कि क्यों।
असली लाभ
निष्पादन (execution) का अध्ययन करना
