Closures, Encapsulation, and Information Hiding

2016. 6. 27.

Closures, Encapsulation, and Information Hiding

Closures and Objects

I remember struggling to understand closures when I first encountered them. Properly grasping closures requires a solid understanding of JavaScript's core concepts. Personally, I once summarized closures as entities that delay the termination of their scope until they disappear themselves—essentially, they terminate their scope when they vanish. This is why they are aptly named Closure. While this explanation is simplistic, it isn't entirely incorrect. For a deeper dive into closures, you can refer to this article, which provides an excellent explanation. For a quick and concise understanding, I recommend reading the introductory section of MDN's closure documentation.

Closures are functions that refer to independent (free) variables (variables that are used locally but defined in an enclosing scope). In other words, these functions 'remember' the environment in which they were created.

In essence, closures are functions that remember the environment in which they were created. This environment refers to the scope chain itself, meaning variables or functions accessible through the scope chain persist even when the scope should be released. Such scopes can be used to implement encapsulation and information hiding—properties typically associated with objects. When a closure is created, it bundles state and behavior, making it another way to create objects (depending on how you define objects). Unlike objects that access context through this, closures access context through their scope.

This article explores several ways to leverage closures.

Implementing a Counter

Counters are often used as examples to explain closures because they are straightforward yet effective for this purpose. Let's use a counter implementation to illustrate the concept.

Counter Using Regular Objects

A counter can be implemented using regular objects as shown below:

var counter = {
    _count: 0,
    count: function() {
        return this._count += 1;
    }
};

console.log(counter.count()); // 1
console.log(counter.count()); // 2

Here, an object literal is used to create an object with an _count attribute for counting numbers. While this approach is simple, it only allows for one counter. To create multiple counters, we can use a constructor function:

function Counter() {
    this._count = 0;
}

Counter.prototype.count = function() {
    return this._count += 1;
};

var counter = new Counter();
var counter2 = new Counter();

console.log(counter.count()); // 1
console.log(counter.count()); // 2
console.log(counter2.count()); // 1

This approach uses this to access context within regular objects. The key components here are:

  • _count: A member variable for storing numbers.
  • count(): A member function for incrementing _count.

Counter Using Closures

Now let's implement the same functionality using closures:

var counter = (function() {
    var _count = 0;

    return function() {
        return _count += 1;
    };
})();

console.log(counter());
console.log(counter());

Although the code differs slightly, the resulting object behaves similarly. Instead of accessing _count via this, it uses scope. The components remain the same—a variable _count for storing numbers and a function for incrementing them. This approach aligns with encapsulation and information hiding principles.

To create multiple counters, we can use a factory function:

function counterFactory() {
    var _count = 0;

    return function() {
        _count += 1;
        return _count;
    };
}

var counter = counterFactory();
var counter2 = counterFactory();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter2()); // 1

While there may be concerns about performance due to the number of generated functions, the focus here is on implementation rather than optimization.

Counters created using closures do not require this for context access. They can be attached to any object or used as event listeners while maintaining consistent context:

var counter = counterFactory();

var app = { counter: counter };
var app2 = { counter: counter };

console.log(app.counter()); // 1
console.log(app.counter()); // 2

console.log(app2.counter()); // 3

From an information-hiding perspective, closures excel because variables like _count cannot be accessed externally.

Counter Using Currying

Let's modify our factory to allow custom increment values using currying:

function counterFactoryMaker(incValue) {
    return function factory(initValue) {
        var _count = initValue;

        return function counter() {
            return _count += incValue;
        };
    };
}

var counterFactory = counterFactoryMaker(2);
var counter = counterFactory(0);
var counter2 = counterFactory(1);

console.log(counter()); // 2
console.log(counter()); // 4

console.log(counter2()); // 3
console.log(counter2()); // 5

Here, currying enables us to create counters with different increment values.

Combining Objects and Closures

In JavaScript development, combining objects and closures is often necessary. For example, event listeners benefit from closures while objects provide structure. By combining both approaches, we can mimic access modifiers not natively supported by JavaScript:

function counterFactory2() {
    var _count = 0;

    function count(value) {
        _count = value || _count;
        return _count;
    }

    return {
        count: count,
        inc: function() { return count(count() + 1); },
        dec: function() { return count(count() - 1); }
    };
}

var counter = counterFactory2();

console.log(counter.inc());
console.log(counter.inc());
console.log(counter.dec());

This implementation follows the Module Pattern, exposing only necessary methods while keeping _count private.

We can extend such counters by overriding methods:

function counterFactory2Ext() {
    var counter = counterFactory2();
    var count = counter.count;

    counter.inc = function() { return count(count() + 2); };

    return counter;
}

var counterExt = counterFactory2Ext();

console.log(counterExt.inc()); // 2
console.log(counterExt.inc()); // 4
console.log(counterExt.inc()); // 6

This combines a private closure variable with methods in an object.

Conclusion

While calling closures "objects" might be an oversimplification, understanding them as tools for encapsulation and information hiding makes them more approachable. Combining closures with objects allows developers to leverage JavaScript's strengths in implementing object-oriented programming concepts. In future articles, we will explore closures from a memory performance perspective.

References

♥ Support writer ♥
with kakaopay

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

shiren • © 2025Sungho Kim