본문 바로가기

0/javascript

What is a Closure?

반응형

A closure is the combination of a function and the lexical environment within which that function was declared.

-- MDN web docs -


Then, what is the lexical environment?


Lexical scoping


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function init(){
 
    var name = "J.Cole";
 
    /*
    printName has no local variables of its own.
    However, because inner functions have access to the variables of outer functions,
    printName() can access the variable name declared in the parent function init().
    */
    function printName(){
        console.log(name);
    }
 
    printName();
 
}
 
init();
cs


This is an example of lexical scoping, which describes how a parser resolves variable names when functions are nested. The word "lexical" refers to the fact that lexical scoping uses the location where a variable is declared within the source code to determine where that variable is available. Nested functions have access to variables declared in their outer scope.




Closure


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function makeEnv(){
 
    var name = "J.Cole";
    
    function printName(){
        console.log(name);
    }
    
    // Returned from the outer function before being executed.
    return printName;
 
}
 
var myFunc = makeEnv();
 
/*
    In this point, you might expect that the name variable would no longer be accessible.
    However, the code still works as expected. 
*/
 
myFunc();
cs


Why this code works?


The reason is that functions in JavaScript form closures. A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time the closure was created. In this case, myFunc is a reference to the instance of the function printName created when makeEnv is run. The instance of printName maintains a reference to its lexical environment, within which the variable name exists. For this reason, when myFunc is invoked, the variable name remains available for use and "J.Cole" is passed to console.log.




Creating closures in loops: A common mistake


1
2
3
4
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function showHelp(help){
    document.getElementById("help").innerHTML = help;
}
 
function setupHelp(){
 
    var helpText = [
      {'id''email''help''Your e-mail address'},
      {'id''name''help''Your full name'},
      {'id''age''help''Your age (you must be over 16)'}
    ];    
 
    /*
        You will expect to hook up an onfocus event to  each one
        that shows the associated help method.
    */    
 
    for (var i=0; i<helpText.length; i++){
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function(){
            showHelp(item.help);
        }
    }
 
}
 
setupHelp();
cs


If you try this code out, you'll see that it doesn't work as expected. No matter what field you focus on, the message about your age will be displayed.


Why?

The reason for this is that the functions assigned to onfocus are closures; they consist of the function definition and the captured environment from the setupHelp function's scope. Three closures have been created by the loop, but each one shares the same single lexical environment, which has a variable with changing values (item.help). The value of item.help is determined when the onfocus callbacks are executed. Because the loop has already run its course by that time, the item variable object (shared by all three closures) has been left pointing to the last entry in the helpText list.


So what should we do?


1. To use more closures.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function showHelp(help){
    document.getElementById("help").innerHTML = help;
}
 
function makeHelpCallback(help){
 
    /*
        Rather than the callbacks all sharing a single lexical environment,
        this function creates a new lexical environment for each callback,
        in which help refers to the correspondnig string from the helpText array.
    */
 
    return function(){
        showHelp(help);
    }
 
}
 
function setupHelp(){
 
    var helpText = [
      {'id''email''help''Your e-mail address'},
      {'id''name''help''Your full name'},
      {'id''age''help''Your age (you must be over 16)'}
    ];    
    
    for (var i=0; i<helpText.length; i++){
        var item = helpText[i];
        /*
        document.getElementById(item.id).onfocus = function(){            
            showHelp(item.help);
        }
        */
        document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
    }
 
}
 
setupHelp();
cs


2. To write the above using anonymous closures.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function showHelp(help){
    document.getElementById("help").innerHTML = help;
}
 
function setupHelp(){
 
    var helpText = [
      {'id''email''help''Your e-mail address'},
      {'id''name''help''Your full name'},
      {'id''age''help''Your age (you must be over 16)'}
    ];    
    
    for (var i=0; i<helpText.length; i++){
 
        // Immediate event listener attachment with the current value of item.
        (function(){
            var item = helpText[i];
            document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
        })();
 
    }
 
}
 
setupHelp();
cs


3. To use the let keyword. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function showHelp(help){
    document.getElementById("help").innerHTML = help;
}
 
function setupHelp(){
 
    var helpText = [
      {'id''email''help''Your e-mail address'},
      {'id''name''help''Your full name'},
      {'id''age''help''Your age (you must be over 16)'}
    ];    
    
    for (var i=0; i<helpText.length; i++){
        let item = helpText[i];
        document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
    }
 
}
 
setupHelp();
cs


So every closure binds the block-scoped variable, meaning that no additional closures are required.


4. To use forEach() to iterate over the helpText array and attach a listener to each <div>.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function showHelp(help){
    document.getElementById("help").innerHTML = help;
}
 
function setupHelp(){
 
    var helpText = [
      {'id''email''help''Your e-mail address'},
      {'id''name''help''Your full name'},
      {'id''age''help''Your age (you must be over 16)'}
    ];    
    
    helpText.forEach(function(text){
        document.getElementById(text.id).onfocus = function() {      
            showHelp(text.help);
        }
    });
 
}
 
setupHelp();
cs



References: 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures


반응형