लूप में क्लोज़र: वेरिएबल कैप्चर करते समय पागल कैसे न हों

शुरुआती लोगों के लिए ऑनलाइन Python प्रशिक्षक

Python आसानी से सीखें बिना सिद्धांत के बोझ के। स्वचालित जांच के साथ व्यावहारिक कार्य हल करें और ब्राउज़र में सीधे कोड लिखें।

पाठ्यक्रम शुरू करें

आपका JavaScript कोड खराब फोन की तरह क्यों व्यवहार करता है?

\n

कल्पना करें: आप एक सरल लूप लिखते हैं, बटनों पर इवेंट हैंडलर जोड़ते हैं, और अचानक - बम! - सभी बटन एक ही संख्या दिखाते हैं। परिचित लगता है? यदि आपने कभी JavaScript लूप में क्लोज़र का सामना किया है, तो आप संज्ञानात्मक असंगति के इस क्षण को जानते हैं। आपको यकीन है कि कोड सही है, लेकिन ब्राउज़र हठपूर्वक कहता है: "नहीं दोस्त, तुमने कुछ गड़बड़ कर दी है।"

\n

Stack Overflow पर इस प्रश्न को सैकड़ों हज़ारों बार देखा गया है। और कोई आश्चर्य नहीं: लूप क्लोज़र में वेरिएबल कैप्चर करना शुरुआती और अनुभवी डेवलपर्स के लिए सबसे आम जाल में से एक है। आइए समझते हैं कि ऐसा क्यों होता है और, सबसे महत्वपूर्ण बात, प्रदर्शन को ध्यान में रखते हुए इसे कैसे ठीक किया जाए।

\n\n

क्लासिक समस्या: var और एसिंक्रोनसिटी

\n

इस हानिरहित कोड को देखें:

\n
for (var i = 0; i < 5; i++) {\n    setTimeout(function() {\n        console.log(i); // क्या आउटपुट होगा?\n    }, 100);\n}
\n

यदि आपने "0, 1, 2, 3, 4" उत्तर दिया - बधाई हो, आप आशावादी हैं। वास्तविकता क्रूर है: कंसोल 5, 5, 5, 5, 5 दिखाएगा। क्यों? क्योंकि var का फंक्शनल स्कोप होता है, ब्लॉक स्कोप नहीं। जब तक setTimeout निष्पादित होता है, तब तक लूप समाप्त हो चुका होता है, और i का मान 5 होगा। सभी पाँच क्लोज़र एक ही वेरिएबल को संदर्भित करते हैं।

\n

यह JavaScript लूप में क्लोज़र समस्या का एक क्लासिक उदाहरण है। और समाधान सतह पर है: प्रत्येक पुनरावृत्ति के लिए एक नया स्कोप बनाने की आवश्यकता है।

\n\n

समाधान #1: IIFE - पुराना अच्छा कामचलाऊ उपाय

\n

ES6 के आने से पहले, डेवलपर्स तुरंत निष्पादित फंक्शन (IIFE) का उपयोग करते थे:

\n
for (var i = 0; i < 5; i++) {\n    (function(j) {\n        setTimeout(function() {\n            console.log(j);\n        }, 100);\n    })(i);\n}
\n

यहाँ j, IIFE को कॉल करने के समय i की एक कॉपी है। प्रत्येक क्लोज़र अपना मान कैप्चर करता है। क्या यह काम करता है? हाँ। सुंदर? बहुत नहीं। क्लोज़र ऑप्टिमाइज़ेशन के दृष्टिकोण से, यह प्रत्येक पुनरावृत्ति पर एक अतिरिक्त फंक्शन बनाता है, जो बड़े लूप में मेमोरी को प्रभावित कर सकता है।

\n\n

समाधान #2: let - ES6 से उद्धारकर्ता

\n

let के आगमन के साथ, समस्या को सुरुचिपूर्ण ढंग से हल किया गया:

\n
for (let i = 0; i < 5; i++) {\n    setTimeout(function() {\n        console.log(i); // 0, 1, 2, 3, 4\n    }, 100);\n}
\n

let लूप के प्रत्येक पुनरावृत्ति के लिए एक नया बाइंडिंग बनाता है। यह ECMAScript विनिर्देश में निहित एक अंतर्निहित व्यवहार है। प्रत्येक क्लोज़र को i की अपनी कॉपी मिलती है। प्रदर्शन? उत्कृष्ट - इंजन इसे कंपाइलेशन स्तर पर ऑप्टिमाइज़ करता है।

\n

हालाँकि, एक बारीक बात है: यदि आप लूप के अंदर let का उपयोग करते हैं, लेकिन for ब्लॉक के बाहर, तो व्यवहार भिन्न हो सकता है। let को हमेशा लूप हेडर के अंदर रखें।

\n\n

समाधान #3: bind - कार्यात्मक दृष्टिकोण

\n

bind विधि आर्गुमेंट्स को फिक्स करने की अनुमति देती है:

\n
for (var i = 0; i < 5; i++) {\n    setTimeout(function(j) {\n        console.log(j);\n    }.bind(null, i), 100);\n}
\n

यहाँ bind एक पूर्व निर्धारित आर्गुमेंट के साथ एक नया फंक्शन बनाता है। यह काम करता है, लेकिन प्रत्येक पुनरावृत्ति पर एक अतिरिक्त फंक्शन बनाता है। प्रदर्शन के मामले में, bind आंतरिक जाँचों के कारण IIFE की तुलना में थोड़ा धीमा है।

\n\n

समाधान #4: forEach और अन्य ऐरे विधियाँ

\n

कभी-कभी समस्या से बचने का सबसे अच्छा तरीका for का उपयोग न करना है:

\n
[0, 1, 2, 3, 4].forEach(function(i) {\n    setTimeout(function() {\n        console.log(i);\n    }, 100);\n});
\n

forEach कॉलबैक के माध्यम से प्रत्येक पुनरावृत्ति के लिए एक नया स्कोप बनाता है। यह न केवल समस्या का समाधान करता है

ब्लॉग

पुस्तक अनुशंसाएँ