[Tutorial] Encapsulation & "static" - Object Oriented Programming

Hey everyone!

I've created an in depth video about how encapsulation which is one of the four main pillars of OOP, works in Javascript. We also take a look at the "static" keyword within a class and what it means for us when it comes to the Prototype. There's also a set of exercises you can try that can get quite tricky to see how well you understand the concepts 😊

Encapsulation and "static" concepts: https://youtu.be/WAo7Mx8Nnos
Exercises to practice the concepts: https://youtu.be/dVpSNOGj1uk

I hope you find these helpful and as always, I'd love any feedback and to know if it helped you!

[Tutorial] Encapsulation & "static" - Object Oriented Programming

16 comments · 95% upvoted · other discussions

comments

senocular

Even though its in the prototype chain above us, we cannot do super.#initDBConnection() for this...

Fun fact about private methods is that they're not inherited through the prototype chain like public methods are. Instead they're added directly to the instance during construction like fields. Because of this, you wouldn't be able to access a private member through super since that is reaching up in the prototype chain for a value which doesn't exist there.

It is, however, possible for subclass to access a private method through this. The difficulty is gaining access to the private name used to define the method. Private names are scoped to the class block in which they were defined. So if a subclass wants to have access to a private method defined by its superclass, it would also need to be defined within the scope of that superclass.

class Database {
    #initDBConnection() { // "#initDBConnection" name scoped to Database block
        console.log("Called: #initDBConnection()")
    }

    static { 
        class CustomDB extends Database {
            // During construction, Database adds #initDBConnection()
            // method to each new CustomDB instance as part of super()

            saveData () {
                this.#initDBConnection() // OK since "#initDBConnection" is in scope
            }
        }

        const db = new CustomDB()
        db.saveData() // Called: #initDBConnection()
    }
}

This scoping of the private name is what makes private members private. Being scoped to the class block prevents anything outside that class block from referring to that private member while at the same time allowing any code within it to have access - even if it is code not part of the class itself.

kap89

Fun fact about private methods is that they're not inherited through the prototype chain like public methods are. Instead they're added directly to the instance during construction like fields.

With one very important difference - while private methods are not a part of the prototype, they are still shared between instances of a class, just like public methods:

class Example {
  pubAsProp = function () {};

  pub() {}

  #priv() {}

  get priv() {
    return this.#priv;
  }
}

const a = new Example();
const b = new Example();

console.log({ isPubMethodShared: a.pub === b.pub }); // true
console.log({ isPrivMethodShared: a.priv === b.priv }); // true
console.log({ isFuncAsPropShared: a.pubAsProp === b.pubAsProp }); // false
Nooder [OP]

Hey Kap, very nice addition and clarification! 🥳 It's truly quite amazing all the nuances in JS. It's funny because it makes you realize that it's actually more involved than the more "traditional" OOP langs, because of how everything works through the prototype system 🤓

Nooder [OP]

Hey Senocular!

Very nice explanation and good catch! You're right technically the way I explained it as being in the chain is a bit wishy washy. Thinking about it in the context of scope makes way more sense. 😃

I've never seen such an example that really shows how it's truly the scope that matters, nice one - very cool! It's funny how there's so much going on in that example too, like how it has closure hence the access to the scope later 🤓

senocular

You're right technically the way I explained it as being in the chain is a bit wishy washy.

I wouldn't worry about that. You still explain things very well :) and this little detail is not something many people know about (or need to know about) anyway. Even as kap89 pointed out, the methods are still shared between instances so even though its not happening through prototype the behavior appears very similar.

jack_waugh

Complexities like this just reconfirm my determination to never use class when I want a class.

jack_waugh

As I mentioned recently in the OO context, I can show you all about cloning of objects, if you are interested.

Nooder [OP]

Hey Jack! I'd love to see a nice vanilla method for cloning if you have one. There are several ways and some are way more convoluted than other, haha 😃

jack_waugh

There's an option (in my opinion) about how to treat functions encountered during cloning. You can link to them, unmodified, from the new object, or you can bind them over to the new object. Whether you want to do that depends on whether you intend to use them in many places in your code as callbacks. Do you want me to show the clone with, or without rebinding of functions?

Here's another binary option: - procedure takes any number of arguments and always returns an object that has no parent (its .__proto__ is null); - cloning procedure takes just one argument and the clone has the same parent as the original.

Which would you like me to show?

Nooder [OP]

Hey Jack!
Nice, so detailed haha. Ideally a deep clone that preserves the prototype of the original object but is a perfect deep copy. It should have the same property descriptors and this bindings and not have any references to the original object apart to its prototype. Hope that isn't too complicated 😄

jack_waugh

I don't usually do a deep copy and call it a "clone". To me, the notion of "clone" implies a shallow copy. The term is used that way in the Self language, where the usual way to produce an instance at runtime is to clone a prototype (the prototype would often be a fixed part of the program, and it likely would have one or more parents, which would also be fixed artifacts of programming). I did code something like a deep copy for debugging purposes or some other special purpose. I think that there is no such thing as a generally-useful deep-copying procedure. I suspect that all uses of deep copying are special to some particular purpose and what that purpose is would drive the detailed choices about the deep-copying algorithm. In particular, if I found myself considering using a deep copy that would replicate looping structures, I would think twice about whether some earlier decisions in the engineering design should be rethought so that such copying wouldn't be necessary.

Nooder [OP]

Thanks for the detailed response! What's interesting is there's actually a new Web API to do this both ways: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

A deep copy would definitely need recursion since we wouldn't know the amount of nesting. This would be very similar to how JSON.stringify and JSON.parse would work.

jack_waugh

Also, it would need to use a Map to catch duplicate references, unless those are just forbidden.

Nooder [OP]

Yes, cyclical references would cause an error in this method as well as things like JSON.parse/stringify. They have methods to track if these exist and throw 🤓

jack_waugh

I have a procedure that will attach an attribute to an object such that the attribute will modify, to meet the object's needs, any value stored in it. Usually, the value would be a function, and the modification would be to bind it to the object. The reason to want to do that is to make the function (now a method) easy to use as a callback. One can write things like setTimeout(foo.bar) instead of setTimeout(() => foo.bar()).

Here is a unit test for the procedure to set up such attributes (assuming t is an object available for testing and u is an object available for utilities):

t.o0 = Object.create(null);
u.tgtKeyGroom(t.o0, 'greeting', string => `I said, ${string}`);
t.o1 = Object.create(t.o0);
t.threw = false;
t.failed = `Unit test failed!`;
try {
  t.o1.greeting
} catch (why) {
  t.threw = true;
  if (`What greeting?` !== why.message) throw Error(t.failed);
};
if (! t.threw) throw Error(t.failed);
t.o1.greeting = "Hello!";
if ("I said, Hello!" !== t.o1.greeting) throw Error(t.failed);
if ("I said, Hello!" !== t.o1.greeting) throw Error(t.failed);
t.o3 = Object.create(null);
u.tgtKeyGroom( t.o3, 'isAnOperatingThetan', function (raw) {
  return `Groomed to ${this.id}.`
});
t.o3.isAnOperatingThetan = true;
t.o4 = Object.create(t.o3);
t.o4.id = 'o4';
if ("Groomed to o4." !== t.o4.isAnOperatingThetan) throw Error(t.failed);
Nooder [OP]

Interesting, thanks for posting this! 🤔