JS module pattern keeps values

Viewed 50

I created a module which analyzes a string. It counts e.g. the occurences of the letter o in a string:

export default (function () {
    var analysis = {
        occurencesOfO: 0,
    };

    var util = {
        countOs: function(input) {
            for (let i = 0; i < input.length; i++) {
                analysis.occurencesOfO += input[i] === 'o' ? 1 : 0;
            }
        },
    }

    return {
        analyze: function (input) {
            util.countOs(input);
            return analysis.occurencesOfO;
        }
    };

})();

I use it like this:

import Analysis from './Analysis';

//...

let s = 'foobar';
let result = Analysis.analyze(s);
console.log('should yield: 2 but yields: ' + result);
// should yield: 2 but yields: 2

s = 'foo bar';
result = Analysis.analyze('fooo bar');
console.log('should yield: 3 but yields: ' + result);
// should yield: 3 but yields: 5

s = 'foo bar foobar';
result = Analysis.analyze('foo bar foobar');
console.log('should yield: 4 but yields: ' + result);
// should yield: 4 but yields: 9

I would have expected that analysis gets reset, but it doesn't. Why? And how do I change that?

1 Answers

You're seeing this behavior because, whether intentionally or not, you are using a subtle but important JavaScript feature called function closures. A closure is a function that references variables outside of its own internal scope. Those variables are automatically bundled into the function, but they aren't instantiated new each time the function is called. If something outside of the function modifies the closured variables, those changes will be reflected the next time the function runs.

That's all very abstract, but it's at the heart of what's going on in your example. In your analyze function:

analyze: function (input) {
    util.countOs(input);
    return analysis.occurencesOfO;
}

You refer to the variable analysis. However, this variable is not local to the function itself! Instead, it comes from the broader context of the module. This means that when analyze returns, analysis isn't garbage collected, and neither does it lose its new value. That value is persisted until the next time you run the function.

To prevent this, the approach I would take is to make analysis a variable scoped within the analyze function:

export default (function () {
    var util = {
        countOs: function(input) {
            var analysis = {
                occurencesOfO: 0
            };

            for (let i = 0; i < input.length; i++) {
                analysis.occurencesOfO += input[i] === 'o' ? 1 : 0;
            }

            return analysis;
        },
    }

    return {
        analyze: function (input) {
            var analysis =  util.countOs(input);
            return analysis.occurencesOfO;
        }
    };

})();
Related