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: