Indeed they’re the same. My version uses “short-circuit evaluation”:
That is, Object.preventExtensions(instance), which is the 2nd operand of &&, is only evaluated if isBottomClass, which is the 1st operand, happens to be “truthy”:
That would be a more direct comparison, skipping caching it in the local variable isBottomClass.
The reason I’ve chosen to have a local variable is b/c I wanted to return it from the function, flagging whether the object sealing operation was a success.
Even if you do, you’d still need to use extends Object in order to inherit it.
instead, what about having a @protected super class Sealer containing _sealInstance()?
#!/usr/bin/env node
// @ts-check
// https://Discourse.Processing.org/t/
// control-variables-and-method-created-while-a-program-is-running/47995/26
"use strict";
class Sealer {
/**
* @protected
* @throws {TypeError}
*/
constructor() {
if (Sealer._sealInstance(this)) throw TypeError(
"Sealer is an abstract utility super class and cannot be instantiated!");
}
/**
* Seals `instance` if its *constructor* is the invoking class itself.
* Prevents further property additions after constructor completes.
*
* @protected
*
* @param {Sealer} instance - object to be conditionally sealed
*
* @returns {boolean} whether `instance` got sealed or not
*/
static _sealInstance(instance) {
const isBottomClass = instance.constructor == this;
isBottomClass && Object.preventExtensions(instance);
return isBottomClass;
}
}
class A extends Sealer {
/**
* Creates a new being with the given name.
*
* @param {string} name - the name for the being
*/
constructor(name) {
super();
/** the name for the being */ this.name = name;
A._sealInstance(this); // Seal it only if this instance is exactly type A!
}
}
class B extends A {
/**
* Creates a new animal with name, type and favorite food.
*
* @param {string} name - the name for the animal
* @param {string} animal - kind of animal
* @param {string} food - favorite food of the animal
*/
constructor(name, animal, food) {
super(name);
// Redundant re-assignment to change prop name's IntelliSense description:
/** the name for the animal */ this.name = this.name;
/** kind of animal */ this.animal = animal;
/** favorite food of the animal */ this.food = food;
B._sealInstance(this); // Seal it only if this instance is exactly type B!
}
}
class Pet extends B {
/**
* Creates a new pet with name, type and favorite food.
*
* @param {string} name - the name for the pet
* @param {string} animal - kind of pet
* @param {string} food - favorite food of the pet
*/
constructor(name, animal, food) {
super(name, animal, food);
Pet._sealInstance(this); // Seal it only if this instance is type Pet!
}
feed() {
const { name, animal, food } = this;
console.info(name, "the", animal, "eats the delicious", food);
}
pet() {
const { name, animal } = this;
console.info(name, "the", animal, "loves to be petted!");
}
}
const a = new A("Thor");
try {
a.weapon = "hammer"; // Prop 'weapon' does not exist on type 'A' ts(2339)
} catch (err) {
console.warn("Class A is sealed & prohibits additional properties!\n", err);
}
console.log(a.name); // "Thor"
console.table(a);
const b = new B("Pengo", "penguin", "fish");
// 1. IntelliSense recognizes props "name", "animal" & "food" as strings:
console.log('\n', b.name, b.animal, b.food, '\n'); // "Pengo" "penguin" "fish"
// 2. Sealed object safety works! Property "color" is rejected by the linter:
try {
b.color = "gray"; // Property 'color' does not exist on type 'B' ts(2339)
} catch (err) {
console.warn("Class B is sealed & prohibits additional properties!\n", err);
}
// 3. Runtime works:
// The object is sealed, and no class fields were generated to crash it.
console.table(b);
const { name: pet, animal, food } = b;
const c = new Pet(pet, animal, food);
try {
c.sleep = function () { // Prop 'sleep' doesn't exist on type 'Pet' ts(2339)
const { name, animal } = this;
console.info(name, "the", animal, "is dreaming right now!");
};
} catch (err) {
console.warn("Class Pet is sealed & prohibits additional props!\n", err);
}
console.table(c);
c.feed(); c.pet();
The throw inside Sealer’s constructor() is so no1 would accidentally attempt to directly instantiate it thinking it does anything by itself; even though it’s only to be subclassed for it’s utility static method.
The 3 try {} / catch () {} is to prove we can’t add any additional properties to neither subclasses after their instantiation.
The directive comment // @ts-check enables an IDE’s TS linter to catch those 3 disallowed property additions even before running the example btW:
A small modification to my code with some additional notes
class Sealer { // See Documentation Annex
static sealInstance(instance) {
// MUTABILITY - IMMUTABILITY
// Nothing : anything can be modified in a class outside of the constructor
// Object.preventExtensions(instance) : instances variables and functions cannot be added after
// Object.seal(instance) : + instances variables and functions cannot be deleted with delete keyword after
// Object.freeze(instance) : + instances variables values cannot be modified ("real" const !)
const isBottomClass = (instance.constructor == this);
if (isBottomClass) Object.seal(instance);
return isBottomClass;
}
constructor() {
if (Sealer.sealInstance(this)) throw TypeError(
"Sealer is an abstract utility super class and cannot be instantiated!");
}
}
N.B. The very principle of my project being that the game engine and graphical interface code is directly accessible by the user, the use of preventExtensions() or seal() in the Sealer class is not so important. It is above all an opportunity to learn about their existence by going through the code!