Private in JavaScript ES6 (Part 2)

JavaScript ES6 brought a lot of changes like const, let, default arguments, spread operator, classes..
Wait WHAT? JavaScript now is a “class” based OO instead of “prototype” based OO???

Relax! class is just syntactical sugar coating and it’s still “prototype” based OO language

I repeat.. internally JS is NOT class based OO language

Hey, but there is a class keyword! Yes, and it still creates objects in memory.

Everything in JavaScript is an “Object” inside memory

Let’s compare ES5 “class” with ES6 class

How to create a "class" in ES5
// The following is NOT ES6 class keyword
// But if we "imagine" a class
// function looks like a constructor 🙂
// class Account
// {
        function Account(id, name, bal) {

            // Public "fields"
            this.id = id;
            this.name = name;

            // Private "field"
            // NOT stored in the object
            // Stored in a "closure"dictionary
            // Which is visible ONLY inside this scope
            let balance = bal;

            // Operations
            this.deposit = function (amount) {
                
                // Inner functions can access
                // closure variables like balance
                balance += amount;
            }

            this.withdraw = function (amount) {
                // balance is NOT a field
                // But we can access outer variables
                balance -= amount;
            }

            this.print = function () {
                console.log(
                    `ID: ${this.id}`,
                    `Name: ${this.name}`,
                    `Balance: ${balance}`
                )
            }
        }
// }

let eich = new Account(1, "Brendan", 10000);
eich.print();
And here is the output
ID: 1 Name: Brendan Balance: 10000

So how do we create class in ES6?

I repeat.. internally JS is NOT class based OO language
/*
 * ES6 has a class keyword
 * But internally there are no classes
 */

// Using ES6 syntax
class Account {

    // Instead of function Account(...)
    constructor(id, name) {

        // Public "fields"
        this.id = id;
        this.name = name;
    }

    // Member function
    print() {
        console.log(
            `ID: ${this.id}`,
            `Name: ${this.name}`,

        )
    }
}

let eich = new Account(1, "Brendan");
eich.print();
Here is the output
ID: 1 Name: Brendan

Now let’s add a “private” field called balance in our Account class. Hmm.. where?
In our ES6, we just defined a closure variable in the outer function Account and that was available to inner functions like deposit and withdraw. So let’s try that

/*
 * ES6 has a class keyword
 * But internally there are no classes
 */

// Using ES6 syntax
class Account {

     balance = 10000;

    // Instead of function Account(...)
    constructor(id, name) {

        // Public "fields"
        this.id = id;
        this.name = name;
    }

    // Member function
    print() {
        console.log(
            `ID: ${this.id}`,
            `Name: ${this.name}`,
            `Balance: ${balance}`
        )
    }
}

let eich = new Account(1, "Brendan");
eich.print();
And here is the output
`Balance: ${balance}` 
          ^
ReferenceError: balance is not defined

Huh? What just happened? Isn’t balance a “closure” variables like ES5? NO!
class is NOT a function and variables inside are not local variables. In fact, balance=10000 has become a class level static field. It’s NOT in some closure.

Oh ok, SO? Even if it’s part of the class object, should I be able to acccess it? Yes and No.
class object itself is not part of eich object’s prototype chain (topic for another blog?), so it wont show up. Even if it did, there would be just one balance field for ALL objects. Hey, it would be nice to share such Account implementation with some billionaire – YOUR balance is now MINE! 🙂

Ok ok, so how do we create “private” variables in ES6? Unfortunately, there is no direct way!
But we can store data in closure variables, just like ES5, though it would have to be outside the class.

/*
 * ES6 has a class keyword
 * But internally there are no classes
 */

// Local variable to module
let balance = 0;

// Using ES6 syntax
class Account {

    // Instead of function Account(...)
    constructor(id, name, bal) {

        // Public "fields"
        this.id = id;
        this.name = name;

        // Closure variable
        balance = bal;
    }

    // Member function
    print() {
        console.log(
            `ID: ${this.id}`,
            `Name: ${this.name}`,
            `Balance: ${balance}`
        )
    }
}

let eich = new Account(1, "Brendan", 10000);
eich.print(); // Should print 10000

let ray = new Account(2, "Romano", 20000);
ray.print(); // Should print 20000

eich.print(); // ?
And here is the output
ID: 1 Name: Brendan Balance: 10000
ID: 2 Name: Romano Balance: 20000
ID: 1 Name: Brendan Balance: 20000

Oops.. eich seems to be using ray’s balance!
Well, that’s because there is only ONE closure variable called balance, so whoever sets it last.. wins!

Fixing it.. with Map!!
/*
 * ES6 has a class keyword
 * But internally there are no classes
 */

// Local variable to module
let balances = new Map();

// Using ES6 syntax
class Account {

    // Instead of function Account(...)
    constructor(id, name, bal) {

        // Public "fields"
        this.id = id;
        this.name = name;

        // Closure map
        // each object (this) has its own balance
        balances.set(this, bal);
    }

    // Member function
    print() {
        console.log(
            `ID: ${this.id}`,
            `Name: ${this.name}`,
            `Balance: ${balances.get(this)}`
        )
    }
}

let eich = new Account(1, "Brendan", 10000);
eich.print(); // Should print 10000

let ray = new Account(2, "Romano", 20000);
ray.print(); // Should print 20000

eich.print(); // Should print 10000
ID: 1 Name: Brendan Balance: 10000
ID: 2 Name: Romano Balance: 20000
ID: 1 Name: Brendan Balance: 10000

There is however ONE problem… balances map will live on forever (technically until the process end). That’s because even after eich and ray objects are long gone, balances map keeps waiting that someday they will return and ask for their balance, but they are dead and long gone!

Hmm.. so what do we do? How can we ensure that if an object dies, it’s balance is also released?

WeakMap!!!

A WeakMap is a Map which does hold reference to other objects, but when it realises that it’s the only one holding on to that reference, it releases it. Problem SOLVED!

/*
 * ES6 has a class keyword
 * But internally there are no classes
 */

// Local variable to module
// WeakMap releases reference
// If it's the only one holding it
let balances = new WeakMap();

// Using ES6 syntax
class Account {

    // Instead of function Account(...)
    constructor(id, name, bal) {

        // Public "fields"
        this.id = id;
        this.name = name;

        // Closure map
        balances.set(this, bal);
    }

    // Member function
    print() {
        console.log(
            `ID: ${this.id}`,
            `Name: ${this.name}`,
            `Balance: ${balances.get(this)}`
        )
    }
}

let eich = new Account(1, "Brendan", 10000);
eich.print(); // Should print 10000

let ray = new Account(2, "Romano", 20000);
ray.print(); // Should print 20000

eich.print(); // Should print 10000

There! It’s done. We have implemented private fields in JavaScript ES6 with help of WeakMaps. Callers cannot access balance, as balance is not a “field” inside the object (ALL object properties are accessible, yes, even if you create them as Symbol, but balance being a closure variable, it’s accessible only within the class which has captured it.

Complete code with all fields as private
/*
 * ES6 has a class keyword
 * But internally there are no classes
 */

// Local variable to module
// WeakMap releases reference
// If it's the only one holding it
let _private = new WeakMap();

// Using ES6 syntax
class Account {

    // Instead of function Account(...)
    constructor(id, name, balance) {

        // Let's create an internal object 
        let _fields = {
            _id: id,
            _name: name,
            _balance: balance
        };

        // Now add it to closure variable
        _private.set(this, _fields);
    }

    // Nice lil touch... getters
    get id() {
        return _private.get(this)._id;
    };

    get name() {
        return _private.get(this)._name;
    };

    get balance() {
        return _private.get(this)._balance;
    }

    set balance(value) {
        throw 'balance is private';
    }

    // Account operations
    withdraw(amount) {
        
        // Modify our private field
        _private.get(this)._balance -= amount;
    }

    deposit(amount) {
        
        // _balance is in closure WeakMap
        // Not accessible from outside
        _private.get(this)._balance += amount;
    }

    // Member function
    print() {

        console.log(
            `ID: ${this.id}`,
            `Name: ${this.name}`,
            `Balance: ${this.balance}`
        )
    }
}

let eich = new Account(1, "Brendan", 10000);
eich.print(); // Should print 10000

let ray = new Account(2, "Romano", 20000);
ray.print(); // Should print 20000

// We can "access" balance thru getters
console.log(eich.id, eich.name, eich.balance);

// Cannot set balance though
eich.balance = 50000;      
console.log(eich.balance);
And here is the output
ID: 1 Name: Brendan Balance: 10000
ID: 2 Name: Romano Balance: 20000
1 Brendan 10000

throw 'balance is private';
^
balance is private

Yay! We have created “private” fields in ES6 class, but how about private functions?

Well, we can store functions also in the _fields object, but that would be wasteful because it will store a copy of the function in EACH object. The SAME function in EVERY object?

There has to be a more efficient way… If you have ideas about that, write them in comment

Private in JavaScript Series

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s