बहुतेक लोक गोष्टी बनवण्यापासून JavaScript शिकायला सुरुवात करतात. तुम्ही एक बटण जोडता, काही डेटा मिळवता (fetch) आणि DOM बदलताना पाहता. मग अॅब्स्ट्रॅक्शन्समध्ये त्रुटी (leak) दिसू लागतात. अशा चुका (bugs) समोर येतात ज्यांचे काहीच अर्थ लागत नाहीत: व्हेरिएबल्स त्यांच्या डिक्लेरेशनच्या आधी अस्तित्वात असतात, फंक्शन्सना अशा व्हेरिएबल्सचा एक्सेस मिळतो ज्यांचा त्यांना नसावा, आणि this कीवर्ड विंडो, बटण किंवा कशावरही पॉइंट करत नाही. साधारणपणे हा तो क्षण असतो जेव्हा तुम्हाला समजते की तुम्हाला इंजिन प्रत्यक्षात काय करत आहे हे समजून घेण्यासाठी त्याच्या अंतर्गत रचनेचा (under the hood) अभ्यास करण्याची गरज आहे.
Execution Context: दोन टप्प्यांमधील सेटअप
जेव्हा तुमच्या ब्राउझरमधील किंवा Node.js मधील JavaScript इंजिनला एखादा स्क्रिप्ट आढळतो, तेव्हा तो एखाद्या व्यक्तीने पान वाचल्याप्रमाणे वरून खाली सरळ वाचत नाही. त्याऐवजी, ते एक execution context तयार करते, जो एक कंटेनर आहे ज्यामध्ये कोडचा विशिष्ट भाग चालवण्यासाठी आवश्यक असलेल्या सर्व गोष्टी साठवल्या जातात. प्रत्येक execution context दोन वेगवेगळ्या टप्प्यांमधून जाते.
Memory Creation Phase. या सुरुवातीच्या टप्प्यात, इंजिन संपूर्ण scope स्कॅन करते आणि त्याला सापडलेल्या प्रत्येक व्हेरिएबल आणि फंक्शन डिक्लेरेशनसाठी मेमरी वाटप (allocate) करते. जर त्याला var दिसले, तर ते जागा राखून ठेवते आणि placeholder म्हणून undefined स्टोअर करते. जर त्याला फंक्शन डिक्लेरेशन दिसले, तर ते संपूर्ण फंक्शन बॉडी स्टोअर करते. म्हणूनच पारंपारिक function कीवर्डने डिक्लेअर केलेले फंक्शन त्याच स्कोपमधील आधीच्या ओळींवरून कॉल केले जाऊ शकते. इंजिन ते कार्यान्वित (execute) करण्यापूर्वीच त्याबद्दल जाणून घेते.
Code Execution Phase. आता इंजिन तुमचा कोड ओळोबाळ (line by line) चालवते. येथे असाइनमेंट्स (assignments) होतात. एक्सप्रेशन्सचे (expressions) मूल्य ठरवले जाते. फंक्शन्स कॉल केले जातात. जर तुम्ही var name = "Alice"; असे लिहिले असेल, तर पहिल्या टप्प्यातील placeholder शेवटी स्ट्रिंगने बदलला जातो. या दोन-टप्प्यांच्या वर्तनामुळे (two-pass behavior) निर्माण होणारा गोंधळ मोठ्या प्रमाणात दूर होतो. इंजिन निष्काळजी नाही; ते एका कडक सेटअप नियमाचे पालन करत आहे.
Variables आणि Temporal Dead Zone
var, let, आणि const मधील निवड केवळ शैलीची (stylistic preference) बाब नाही. var हे function-scoped असते, याचा अर्थ ते ब्रॅकेट्सना (braces) पूर्णपणे दुर्लक्षित करते. जर तुम्ही ते if ब्लॉकच्या आत डिक्लेअर केले, तर ते बाहेर लीक होते. हे वर्तन सुरुवातीच्या JavaScript मध्ये कदाचित योग्य वाटले असेल, परंतु आधुनिक ॲप्लिकेशन्समध्ये यामुळे देखभालीच्या (maintenance) समस्या निर्माण होतात. let आणि const हे block-scoped असतात. ते कर्ली ब्रॅकेट्सचा (curly braces) आदर करतात आणि ब्लॉक संपताच नाहीसे होतात.
'hoisting' त्यांना कशा प्रकारे हाताळते यामध्ये एक सूक्ष्म पण महत्त्वाचा फरक आहे. var डिक्लेरेशन hoist केले जातात आणि लगेच undefined ने इनिशियलाइज केले जातात. let आणि const तांत्रिकदृष्ट्या hoist देखील होतात; डिक्लेरेशनच्या ओळीपर्यंत पोहोचण्यापूर्वीच इंजिनला ते अस्तित्वात आहेत हे माहित असते. परंतु ते इनिशियलाइज केलेले नसतात. ते temporal dead zone नावाच्या स्थितीत असतात. जर तुम्ही डिक्लेरेशनच्या ओळीपूर्वी त्यांना वाचण्याचा प्रयत्न केला, तर तुम्हाला गुपचूप undefined मिळण्याऐवजी थेट ReferenceError मिळेल. हा क्रॅश प्रत्यक्षात उपयुक्त आहे. तो इनिशियलाइज न केलेल्या डेटावर लॉजिक पुढे चालू ठेवण्यापासून रोखतो.
Lexical Scope आणि Closures
स्कोप एका साध्या प्रश्नाचे उत्तर देतो: मी या व्हेरिएबलचा एक्सेस कोठे घेऊ शकतो? JavaScript lexical scope वापरते, याचा अर्थ फंक्शनचे एक्सेस अधिकार ते सोर्स कोडमध्ये प्रत्यक्ष कुठे लिहिले गेले आहे यावरून ठरवले जातात, ते कुठे कॉल केले जाते यावरून नाही. जर तुम्ही एका फंक्शनच्या आत दुसरे फंक्शन परिभाषित केले, तर आतील फंक्शन त्याच्या मूळ (parent) फंक्शनमधील व्हेरिएबल्स वाचण्यासाठी बाहेर पोहोचू शकते. बाहेरील फंक्शन आतमध्ये पोहोचू शकत नाही. हे नाते स्थिर (static) आहे. तुम्ही ते आतील फंक्शन मॉड्यूल्समध्ये पाठवू शकता, ग्लोबल व्हेरिएबलमध्ये स्टोअर करू शकता आणि पूर्णपणे वेगळ्या फाईलमधून कॉल करू शकता. तरीही ते ज्या स्कोपमध्ये तयार झाले होते त्यातील व्हेरिएबल्स लक्षात ठेवते.
हे वर्तन नैसर्गिकरित्या closures तयार करते. जेव्हा एखादे आतील फंक्शन त्याच्या बाहेरील स्कोपमधील व्हेरिएबलचा संदर्भ (reference) जपून ठेवते, तेव्हा closure तयार होते. बाहेरील फंक्शनचे एक्झिक्यूशन संपल्यानंतर आणि त्याचे लोकल व्हेरिएबल्स garbage collected व्हायला हवे असतानाही, JavaScript त्यांना मेमरीमध्ये जतन करते कारण आतील फंक्शनला त्यांची अजूनही गरज असते. आतील फंक्शन त्याच्या सभोवतालचे वातावरण स्वतःसोबत घेऊन जाते.
हे केवळ शैक्षणिक तपशील नाही. ज्या भाषेत स्पष्ट access modifiers नाहीत, तिथे closures तुम्हाला private state तयार करण्याचा व्यावहारिक मार्ग देतात.
function makeCounter() {
let count = 0;
return function() {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
येथे, count लपवलेले आहे. रिटर्न केलेल्या फंक्शनच्या बाहेरून कोणीही ते रिसेट करू शकत नाही किंवा थेट वाचू शकत नाही. हे केवळ स्कोप मेकॅनिक्सचा वापर करून तयार केलेले एक private variable आहे.
Hoisting प्रत्यक्ष वापरात
JavaScript "declarations वरच्या बाजूला हलवते" असे ऐकणे सामान्य आहे. हे एक उपयुक्त मानसिक मॉडेल आहे, परंतु कोड प्रत्यक्षात पुन्हा लिहिला जात नाही. मेमरी क्रिएशन फेज (memory creation phase) दरम्यान, इंजिन अंमलबजावणी (execution) सुरू होण्यापूर्वी केवळ घोषणा (declarations) नोंदवते. var x = 5; सारख्या स्टेटमेंटमध्ये असे वागते की घोषणा आणि इनिशियलायझेशन एकमेकांपासून वेगळे आहेत. var x; ही घोषणा लवकर प्रक्रिया केली जाते आणि undefined म्हणून इनिशियलाइज केली जाते. x = 5; हे असाइनमेंट तुम्ही जिथे लिहिले आहे तिथेच राहते आणि एक्झिक्यूशन फेज दरम्यान चालते.
यामुळे, var मुळे आश्चर्यकारक परिणाम होऊ शकतात. फंक्शनच्या सुरुवातीला वापरलेले व्हेरिएबल undefined असू शकते, जरी खाली मोठ्या प्रमाणात असाइनमेंट दिलेली असली तरीही. let आणि const वापरल्यामुळे अशा चुका टाळता येतात, कारण 'टेम्पोरल डेड झोन' (temporal dead zone) तुम्हाला तुमच्या घोषणा त्यांच्या वापराच्या वर ठेवण्यास भाग पाडतो.
this ला त्याचे मूल्य कसे मिळते
जर एक्झिक्यूशन कॉन्टेक्स्ट आणि स्कोप ठरवतात की व्हेरिएबल्स कुठे राहतील, तर this ठरवतो की सध्या कोणता ऑब्जेक्ट नियंत्रणात आहे. लेक्सिकल व्हेरिएबल्सच्या उलट, this फंक्शन कुठे लिहिले आहे यावरून ठरत नाही. ते पूर्णपणे फंक्शन कसे कॉल केले जाते यावरून ठरवले जाते.
Default binding तेव्हा घडते जेव्हा तुम्ही एखादे साधे, स्वतंत्र फंक्शन कॉल करता. नॉन-स्ट्रिक्ट मोडमध्ये, this ग्लोबल ऑब्जेक्टकडे वळते. ब्राउझरमध्ये, तो window असतो. कोणत्याही कॉन्टेक्स्टशिवाय फंक्शन कॉल केल्यास, तुम्ही नकळत ग्लोबल स्टेटला स्पर्श करू शकता.
Implicit binding तेव्हा घडते जेव्हा तुम्ही एखाद्या ऑब्जेक्टवरील मेथड म्हणून फंक्शन कॉल करता. जर तुम्ही user.sayName() असे लिहिले, तर 'डॉट' (dot) शांतपणे इंजिनला सांगतो की त्या कॉलच्या कालावधीसाठी this ला user म्हणून सेट करावे. फंक्शन कुठे परिभाषित केले आहे यापेक्षा ते कुठे कॉल केले आहे हे अधिक महत्त्वाचे ठरते.
Explicit binding तुम्हाला सर्व काही मॅन्युअली ओव्हरराइड करण्याची परवानगी देते. call() आणि apply() फंक्शन त्वरित कॉल करतात आणि त्याच वेळी this ला तुम्ही दिलेल्या विशिष्ट ऑब्जेक्टमध्ये बदलतात. त्यांच्यातील एकमेव फरक असा आहे की आर्ग्युमेंट्स कसे पास केले जातात: call स्वल्पविराम देऊन वेगळी केलेली यादी घेते, तर apply एक ॲरे (array) घेते. bind() वेगळ्या पद्धतीने काम करते. ते फंक्शन लगेच कॉल करत नाही. त्याऐवजी, ते एक नवीन फंक्शन परत करते ज्यामध्ये this तुम्ही दिलेल्या मूल्यावर कायमस्वरूपी लॉक केले जाते. कॉलबॅक्ससाठी हे अत्यंत उपयुक्त आहे, अन्यथा ते पास करताना त्यांचा कॉन्टेक्स्ट हरवू शकतो.
New binding तेव्हा लागू होते जेव्हा तुम्ही फंक्शन कॉलच्या समोर new कीवर्ड वापरता. इंजिन एक नवीन रिकामी ऑब्जेक्ट तयार करते, त्याचे प्रोटोटाइप लिंकेज सेट करते आणि कन्स्ट्रक्टरमधील this त्या नवीन इन्स्टन्सकडे निर्देशित करते.
टिकणारे कोड लिहिणे
कार्यपद्धती समजून घेणे म्हणजे कलेचा फक्त अर्धा भाग आहे. दुसरा अर्धा भाग म्हणजे असा कोड लिहिणे जो सहा महिन्यांनंतरही माणसांना वाचता येईल.
DRY (Don't Repeat Yourself) हे ऐकायला साधे वाटते, परंतु याचे सतत उल्लंघन होते. जर तुम्हाला तीन वेगवेगळ्या फाईल्समध्ये एकच व्हॅलिडेशन लॉजिक किंवा API कॉल पॅटर्न लिहायचा येत असेल, तर तो वेगळा काढा (extract करा). एक फंक्शन लिहा. 'वन सोर्स ऑफ ट्रुथ' (one source of truth) म्हणजे आवश्यकता बदलल्यावर अपडेट करण्यासाठी एकच जागा असते, ज्यामुळे वेळेची मोठी बचत होते.
KISS (Keep It Simple, Stupid) हा अहंकाराविरुद्धचा बचाव आहे. नेस्टेड टर्नरीज (Nested ternaries) आणि वन-लाइनर क्लोजर्स (one-liner closures) हुशार वाटू शकतात, परंतु डीबगिंग दरम्यान ते तासनतास खर्च करू शकतात. मी आधी दाखवलेला क्लोजर पॅटर्न शक्तिशाली आहे, तरीही केवळ तुम्ही करू शकता म्हणून पाच स्तर खोलवर नेस्टिंग करणे ही चूक आहे. साधा कोड टीममधील बदल, प्रोडक्शनमधील समस्या आणि रात्रीच्या वेळी येणाऱ्या तातडीच्या अलर्ट्समध्येही टिकून राहतो, जेव्हा काहीतरी बिघडते आणि कोणालाही ते का बिघडले हे आठवत नाही.
खरा फायदा
एक्झिक्यूशनचा अभ्यास करणे
