Clases, prototipos y módulos en JavaScript (con jsc)
Curso de JavaScript 4 en jsc: prototipos, class vs function, campos #privados, el this perdido, imports circulares y 40 vs 249 bytes por objeto.
Serie: curso de JavaScript 4/6 (intermedio). Esta clase es para ti si ya escribes funciones y usas arreglos y objetos, y ahora te encuentras class, extends, #private e import en código real sin saber qué pasa por debajo: en el proyecto de React del bootcamp, en el backend que heredaste como freelancer o en la base de código de tu primer trabajo. Al terminar vas a poder predecir dónde se encuentra una propiedad, elegir entre una clase y un objeto simple, evitar la caída del this perdido y dividir el código en módulos ES sin sorpresas por imports circulares.
Si vienes de Java, C# o Swift (o de la materia de programación orientada a objetos de la universidad), las clases de JavaScript te parecen conocidas, y ahí está la trampa: por debajo no hay planos de clase, solo objetos enlazados con otros objetos. Si vienes de scripts sueltos, el código con class de un framework parece magia. Esta clase quita la magia ejecutando cada pieza en un motor real, mostrando la salida real y los mensajes de error exactos que te vas a encontrar.
Entorno: JavaScriptCore tal como viene en macOS 26.6.2 (versión del framework 21624.5.1.11.3), ejecutado con su consola jsc en una Mac con Apple Silicon (M1 Pro). No se ejecutaron Node.js, Deno ni navegadores; donde se comportan distinto, lo decimos. Si hiciste la clase 1 ya tienes el alias:
$ alias jsc=/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Helpers/jsc
jsc imprime con print(); en esta consola no existe console.
La cadena de prototipos: dónde se encuentra de verdad una propiedad
Cada objeto tiene un enlace oculto llamado [[Prototype]] hacia otro objeto (o hacia null). Cuando lees obj.name y obj no tiene name propio, el motor sigue ese enlace y vuelve a intentar, hasta encontrar el nombre o llegar a null. Ese es todo el modelo de herencia. Object.create(p) crea un objeto nuevo cuyo prototipo es p:
// proto.js — the prototype chain, step by step
const animal = { legs: 4, describe() { return `${this.name} has ${this.legs} legs`; } };
const cat = Object.create(animal); // cat's prototype is animal
cat.name = "Tama";
print(cat.describe()); // found on animal, this = cat
print(Object.hasOwn(cat, "legs"), "legs" in cat);
print(Object.getPrototypeOf(cat) === animal);
print(Object.getPrototypeOf(animal) === Object.prototype);
print(Object.getPrototypeOf(Object.prototype));
cat.legs = 3; // shadows, does not change animal
print(cat.legs, animal.legs);
const bird = Object.create(animal); bird.name = "Piyo";
animal.legs = 2; // every object without its own legs sees this
print(bird.describe());
for (const k in cat) print("for-in:", k);
print(Object.keys(cat));
$ jsc proto.js
Tama has 4 legs
false true
true
true
null
3 4
Piyo has 2 legs
for-in: name
for-in: legs
for-in: describe
name,legs
Tres cosas que sacar de esta salida:
- Propia contra heredada.
Object.hasOwn(cat, "legs")diofalsemientras que"legs" in catdiotrue:inrecorre la cadena,hasOwn(ES2022) no. - Escribir nunca recorre la cadena.
cat.legs = 3creó una propiedad propia nueva encat(a esto se le llama shadowing, sombrear);animal.legssiguió en 4. Pero cambiaranimal.legsdespués cambió lo que vebird, porquebirdno tienelegspropio. Los datos mutables compartidos en un prototipo son una fuente clásica de bugs. for...inincluye claves enumerables heredadas (apareciódescribe),Object.keysno. UsaObject.keys/Object.entriespara datos.
this dentro de describe era cat, no animal: this es el objeto sobre el que se llamó el método, sin importar dónde se encontró el método.
class es una función, con cuatro diferencias que pudimos observar
Antes de ES2015, las "clases" eran funciones constructoras más métodos en .prototype. La palabra clave class produce la misma forma, por eso las dos versiones de abajo devuelven 5:
function PointFn(x, y) { this.x = x; this.y = y; }
PointFn.prototype.len = function () { return Math.hypot(this.x, this.y); };
class Point { constructor(x, y) { this.x = x; this.y = y; } len() { return Math.hypot(this.x, this.y); } }
print(typeof Point, typeof PointFn);
print(new PointFn(3, 4).len(), new Point(3, 4).len());
for (const k in new PointFn(3, 4)) print("fn for-in:", k);
for (const k in new Point(3, 4)) print("cls for-in:", k);
print(Object.getOwnPropertyDescriptor(Point.prototype, "len").enumerable,
Object.getOwnPropertyDescriptor(PointFn.prototype, "len").enumerable);
const p = PointFn(1, 2); // forgot new (sloppy mode)
print(p, typeof globalThis.x, globalThis.x);
try { Point(1, 2); } catch (e) { print(e); }
$ jsc classvsfn.js
function function
5 5
fn for-in: x
fn for-in: y
fn for-in: len
cls for-in: x
cls for-in: y
false true
undefined number 1
TypeError: Cannot call a class constructor Point without |new|
Las diferencias, cada una visible arriba o en una sección posterior:
- Olvidar
new. Con la función antigua,PointFn(1, 2)devolvióundefinedy escribió en silencioxeyen el objeto global (globalThis.xpasó a valer 1). Una clase se niega con unTypeError. Eso solo ya es una buena razón para preferirclass. - Los métodos no son enumerables.
lense coló enfor...inen la versión con función, no en la clase. - El cuerpo de la clase está en modo estricto. Vas a ver qué significa eso para
thisen la sección delthisperdido. Point.prototypees de solo lectura. Lo mostramos en la sección deinstanceof.
Campos #privados, getters y bloques static
ES2022 agregó privacidad real: los nombres que empiezan con # existen solo dentro del cuerpo de la clase. Aquí va una pequeña cuenta bancaria que usa todas las novedades de clase de ES2022 a la vez:
class Account {
#balance = 0; // private field (ES2022)
static #count = 0; // private static
static { print("static block ran"); } // static initialization block (ES2022)
constructor(owner) { this.owner = owner; Account.#count++; }
deposit(n) { this.#check(n); this.#balance += n; return this; }
#check(n) { if (!(n > 0)) throw new RangeError(`bad amount: ${n}`); }
get balance() { return this.#balance; } // read-only from outside
static isAccount(o) { return #balance in o; } // brand check (ES2022)
static get count() { return Account.#count; }
}
const a = new Account("kim").deposit(100).deposit(50);
print(a.balance, Account.count);
a.balance = 999; // no setter: silently ignored in sloppy mode
print(a.balance);
print(Object.keys(a), JSON.stringify(a));
print(Account.isAccount(a), Account.isAccount({ balance: 1 }));
try { a.deposit(-5); } catch (e) { print(e); }
try { Account.prototype.deposit.call({}, 10); } catch (e) { print(e); }
$ jsc account.js
static block ran
150 1
150
owner {"owner":"kim"}
true false
RangeError: bad amount: -5
TypeError: Cannot access private method or acessor (evaluating 'this.#check')
Lee la salida línea por línea:
a.balance = 999no hizo nada y no avisó nada, porque hay getter pero no setter y la asignación estaba en código de script no estricto (sloppy). En código estricto la misma asignación lanza un error:jsc -e '"use strict"; ...'imprimióTypeError: Attempted to assign to readonly property.Bugs así se esconden en scripts viejos que no son módulos.- Los campos privados son invisibles para
Object.keysyJSON.stringify. Si serializas una clase con estado privado, escribe un métodotoJSON(). #balance in ocomprueba si un objeto lo construyó de verdad esta clase, algo que el duck typing ("balance" in o) no puede saber.- Llamar a un método sobre un objeto ajeno falla en el primer acceso privado. Ojo: el mensaje de JavaScriptCore dice de verdad "acessor" (sic). Búscalo exactamente así si te aparece.
Acceder a un nombre privado desde fuera ni siquiera es un error en tiempo de ejecución. Es un SyntaxError que se detecta al parsear, así que no se ejecuta ni una línea del archivo:
$ cat outside.js
print("first line runs?");
class A { #x = 1 }
const a = new A();
print(a.#x);
$ jsc outside.js
Exception: SyntaxError: Cannot reference undeclared private names: "#x"
at outside.js:5
$ echo $?
3
"first line runs?" nunca se imprimió. (jsc reportó la línea 5, una más que la última; el error es sobre el archivo entero.) Y a["#x"] imprimió undefined: una clave string "#x" es una propiedad completamente distinta y ordinaria.
Error común 1: el setter que se llama a sí mismo
Lo que se suele escribir: un setter que recorta los espacios de la entrada y la guarda con el mismo nombre.
class User {
set name(v) { this.name = v.trim(); } // BUG: calls itself
}
try { new User().name = " lee "; } catch (e) { print(e); }
Qué pasa en realidad: this.name = ... dentro del setter vuelve a llamar al setter, para siempre.
$ jsc setter_loop.js
RangeError: Maximum call stack size exceeded.
Solución: guarda el valor en otro lugar, idealmente un campo privado: #name; set name(v) { this.#name = v.trim(); } get name() { return this.#name; }.
Error común 2: el método que perdió su this
Lo que se suele escribir: pasar un método como callback (a setTimeout, a un event listener, a array.map) o copiarlo en una variable.
class Counter {
count = 0;
inc() { this.count++; return this.count; }
incArrow = () => { this.count++; return this.count; };
}
const c = new Counter();
const f = c.inc;
try { f(); } catch (e) { print(e); }
setTimeout(c.inc, 0); // same bug, later
const g = c.incArrow; print(g(), g());
const h = c.inc.bind(c); print(h());
print(Object.keys(c));
Qué pasa en realidad:
$ jsc lostthis.js
TypeError: undefined is not an object (evaluating 'this.count')
1 2
3
count,incArrow
Exception: TypeError: undefined is not an object (evaluating 'this.count')
inc@lostthis.js:3:15
$ echo $?
3
this lo fija la llamada, no el lugar donde se definió la función. f() no tiene ningún objeto delante del punto y, como los cuerpos de clase son estrictos, this es undefined en vez del objeto global. La versión con setTimeout se cae después, sin que nadie atrape el error, y termina el programa con código de salida 3. Chrome y Node imprimen un mensaje con otras palabras (Cannot read properties of undefined (reading 'count')); no los ejecutamos, así que busca las dos variantes.
Solución: c.inc.bind(c), una flecha envolvente () => c.inc() o un campo de clase con función flecha. Fíjate en la última línea: incArrow es una propiedad propia de cada instancia (Object.keys la muestra), mientras que inc vive una sola vez en el prototipo. Eso tiene un costo, que medimos a continuación.
Medición: cuánto cuesta por objeto un campo con función flecha
Creamos 100,000 instancias vivas de la misma clase diminuta, escrita de cuatro formas, y leímos el tamaño del heap después de una recolección de basura completa. fullGC() y gcHeapSize() son funciones auxiliares exclusivas de jsc; en Node usarías --expose-gc y process.memoryUsage() (no ejecutado aquí).
// mem.js — heap cost per instance: prototype method vs per-instance function
// usage: jsc mem.js -- <variant> (run each variant in a fresh process)
class ProtoMethod { constructor(i) { this.i = i; } get() { return this.i; } }
class ArrowField { get = () => this.i; constructor(i) { this.i = i; } }
class BoundInCtor { constructor(i) { this.i = i; this.get = this.get.bind(this); } get() { return this.i; } }
class PrivateField { #i; constructor(i) { this.#i = i; } get() { return this.#i; } }
const variants = { ProtoMethod, ArrowField, BoundInCtor, PrivateField };
const N = 100_000;
const name = arguments[0];
const C = variants[name];
fullGC(); const before = gcHeapSize();
const t0 = performance.now();
const keep = new Array(N);
for (let i = 0; i < N; i++) keep[i] = new C(i);
const t1 = performance.now();
fullGC(); const after = gcHeapSize();
let sum = 0; for (const o of keep) sum += o.get(); // all instances still alive and usable
print(`${name.padEnd(12)} bytes/instance=${((after - before) / N).toFixed(1)} create=${(t1 - t0).toFixed(1)}ms sum=${sum}`);
Cinco rondas, cada variante en su propio proceso nuevo (se muestra la primera ronda; la cantidad de bytes fue idéntica en las cinco):
$ for v in ProtoMethod ArrowField BoundInCtor PrivateField; do jsc mem.js -- $v; done
ProtoMethod bytes/instance=40.4 create=2.4ms sum=4999950000
ArrowField bytes/instance=249.4 create=5.7ms sum=4999950000
BoundInCtor bytes/instance=136.4 create=5.4ms sum=4999950000
PrivateField bytes/instance=72.4 create=2.2ms sum=4999950000
| Estilo | Bytes por instancia | Crear 100,000 (mediana de 5) |
|---|---|---|
| Método en el prototipo | 40.4 | 1.8 ms |
#i privado + método en el prototipo |
72.4 | 2.2 ms |
bind() en el constructor |
136.4 | 5.4 ms |
| Campo de clase con función flecha | 249.4 | 5.7 ms |
Los números incluyen el espacio de 8 bytes en el arreglo keep. Un campo flecha crea un objeto función nuevo (y su closure) por cada instancia: unas seis veces la memoria de un método de prototipo aquí, y tres veces más lento de crear. Para 50 componentes de interfaz eso es ruido; para 1,000,000 de registros de datos son unos 200 MB. Usa campos flecha en objetos cuyos métodos se pasan como callbacks (manejadores de eventos) y métodos de prototipo en objetos que son más bien datos. Los campos privados cuestan unos 32 bytes más por objeto en JavaScriptCore; no investigamos por qué, y en V8 puede ser distinto.
extends, super e instanceof
class Shape {
constructor(name) { this.name = name; }
area() { return 0; }
toString() { return `${this.name}(${this.area().toFixed(2)})`; }
}
class Circle extends Shape {
constructor(r) { super("circle"); this.r = r; }
area() { return Math.PI * this.r ** 2; }
}
class Square extends Shape {
constructor(s) { super("square"); this.s = s; }
area() { return this.s ** 2; }
toString() { return "[" + super.toString() + "]"; }
}
const shapes = [new Circle(1), new Square(2)];
print(shapes.join(" "));
const c = shapes[0];
print(c instanceof Circle, c instanceof Shape, c instanceof Object, c instanceof Square);
print(Object.getPrototypeOf(Circle.prototype) === Shape.prototype, Object.getPrototypeOf(Circle) === Shape);
class Broken extends Shape { constructor() { this.x = 1; super("b"); } }
try { new Broken(); } catch (e) { print(e); }
class NoSuper extends Shape { constructor() { } }
try { new NoSuper(); } catch (e) { print(e); }
class Stack extends Array { peek() { return this[this.length - 1]; } }
const s = Stack.from([1, 2, 3]);
const m = s.map(x => x * 10);
print(s.peek(), m.peek(), m instanceof Stack, Array.isArray(m));
$ jsc inherit.js
circle(3.14) [square(4.00)]
true true true false
true true
ReferenceError: 'super()' must be called in derived constructor before accessing |this| or returning non-object.
ReferenceError: 'super()' must be called in derived constructor before accessing |this| or returning non-object.
3 30 true true
Shape.toString llama a this.area(), y se ejecuta la versión de la subclase: la búsqueda empieza en el objeto, así que gana el método más específico. Tocar this antes de super(), o no llamar nunca a super(), da el mismo ReferenceError. Extender Array funciona, e incluso map sobre un Stack devuelve un Stack.
instanceof no mira el nombre de la clase. Pregunta: "¿está Circle.prototype en algún punto de la cadena de este objeto?". Eso tiene consecuencias:
class Duck {}
const d = new Duck();
print(d instanceof Duck);
Duck.prototype = {}; // ignored: class prototype is non-writable
print(d instanceof Duck);
function Goose() {}
const g = new Goose();
Goose.prototype = {}; // allowed for plain functions
print(g instanceof Goose);
// another realm (like an iframe): jsc's createGlobalObject()
const other = createGlobalObject();
const arr = other.eval("[1, 2, 3]");
print(arr instanceof Array, Array.isArray(arr));
// duck typing via Symbol.hasInstance
class Iterable { static [Symbol.hasInstance](o) { return o != null && typeof o[Symbol.iterator] === "function"; } }
print("abc" instanceof Iterable, new Map() instanceof Iterable, {} instanceof Iterable);
try { print(d instanceof {}); } catch (e) { print(e); }
$ jsc instanceof.js
true
true
false
false true
true true false
TypeError: {} is not a function. (evaluating 'd instanceof {}')
Un arreglo que viene de otro objeto global (un "realm": en navegadores, un iframe o un worker; createGlobalObject() es un helper exclusivo de jsc que simula uno) no es instanceof Array, porque tiene otro Array.prototype. Array.isArray es la comprobación confiable. Lo mismo pasa en navegadores con objetos que cruzan de un realm a otro por caminos del estilo de postMessage (no ejecutado aquí).
Módulos: qué puede ejecutar jsc y qué no
Un módulo es un archivo con su propio scope que comparte valores solo mediante export e import (ES2015). jsc ejecuta un archivo como módulo cuando termina en .mjs o cuando le pasas -m. Dos archivos:
// counter.mjs
export let count = 0;
export function inc() { count++; }
export default class Greeter { hi(n) { return `hi ${n}`; } }
print("counter.mjs evaluated");
// main.mjs
import Greeter, { count, inc } from "./counter.mjs";
import * as again from "./counter.mjs"; // same module, not evaluated twice
print(count); inc(); inc();
print(count, again.count); // live binding
try { count = 10; } catch (e) { print(e); }
print(new Greeter().hi("module"));
print(typeof this, typeof globalThis);
const cfg = await Promise.resolve({ debug: true }); // top-level await
print("tla:", cfg.debug);
const lazy = await import("./counter.mjs");
print(lazy.count, lazy === again);
print(import.meta.url.replace(/^.*\/mod\//, "~/demo/"));
$ jsc main.mjs
counter.mjs evaluated
0
2 2
TypeError: Attempted to assign to readonly property.
hi module
undefined object
tla: true
2 true
~/demo/main.mjs
Lo que muestra esto: un módulo se ejecuta una sola vez sin importar cuántas veces se importe; un let importado es un live binding (pasó de 0 a 2 cuando el módulo que lo exporta lo cambió) pero de solo lectura para quien lo importa; el this de nivel superior es undefined; el await de nivel superior (ES2022) e import() (ES2020) funcionan. La ruta absoluta de import.meta.url la recortamos nosotros.
Los errores que vas a ver al mezclar estilos:
$ jsc classic.js # line 1: import { inc } from "./counter.mjs";
Exception: SyntaxError: Unexpected token '{'. import call expects one or two arguments.
at classic.js:1
$ jsc -m classic.js # same file, as a module: works
counter.mjs evaluated
1
$ jsc -e 'const fs = require("fs")'
Exception: ReferenceError: Can't find variable: require
global code@[Command Line]:1:19
$ jsc noext.mjs # import { inc } from "./counter";
Exception: Error: Could not open file '~/demo/mod/counter'.
$ jsc badname.mjs # import { incr } from "./counter.mjs";
Exception: SyntaxError: Importing binding name 'incr' is not found.
$ jsc sloppy.mjs # x = 5;
Exception: ReferenceError: Can't find variable: x
module code@~/demo/mod/sloppy.mjs:1:2
Los módulos siempre son estrictos, así que asignar a una variable no declarada es un error en vez de crear una global. Los navegadores también exigen el nombre completo del archivo con extensión; los bundlers y el resolvedor CommonJS de Node adivinan extensiones, y por eso "./counter" funciona en muchos proyectos y aquí falla.
CommonJS (require(), module.exports) es el sistema de módulos más antiguo de Node. No existe en jsc y no ejecutamos Node, así que lo siguiente solo se describe: CommonJS carga de forma síncrona y copia module.exports en el momento del require, mientras que los bindings de ESM son vivos; Node decide entre los dos por la extensión .mjs/.cjs o por el campo "type" de package.json.
Error común 3: imports circulares y la TDZ
Lo que se suele escribir: dos módulos que importan las constantes del otro en el nivel superior.
// a.mjs
import { B_NAME } from "./b.mjs";
export const A_NAME = "A";
print("a.mjs sees", B_NAME);
// b.mjs
import { A_NAME } from "./a.mjs";
export const B_NAME = "B";
print("b.mjs sees", A_NAME); // a.mjs has not run yet
Qué pasa en realidad: jsc empieza con a.mjs, que primero necesita b.mjs, que lee A_NAME antes de que a.mjs haya ejecutado su línea const. El binding existe pero sigue en su zona muerta temporal (TDZ, de la clase 2):
$ jsc a.mjs
Exception: ReferenceError: Cannot access 'A_NAME' before initialization.
module code@~/demo/mod/b.mjs:3:27
Solución: rompe el ciclo (mueve las constantes compartidas a un tercer módulo), o lee el valor importado más tarde, dentro de una función que se ejecute cuando los dos módulos ya terminaron. Con b.mjs exportando function hello() { return "b.mjs sees " + A_NAME; } y a.mjs llamándola, la salida pasó a ser a.mjs sees B | b.mjs sees A.
Tabla de decisión
| Situación | Usa | Por qué |
|---|---|---|
| Datos con comportamiento, muchas instancias | class con métodos en el prototipo |
Una sola copia de cada método; 40 B/instancia en nuestra prueba |
| Método que se pasa como callback (handlers de UI) | Campo flecha o bind una vez |
Conserva this; cuesta memoria por instancia |
| Estado de verdad oculto, comprobación de marca | Campos #private, #x in obj |
Inalcanzables, no se serializan |
| Solo una bolsa de valores | Objeto literal | No hace falta clase; ida y vuelta por JSON limpia |
| Compartir código entre archivos | Módulos ES (.mjs o -m aquí) |
Live bindings, modo estricto, funciona en navegadores |
| "¿Esto es un arreglo?" | Array.isArray |
instanceof falla entre realms |
| Dos módulos necesitan las constantes del otro | Un tercer módulo compartido | Evita el ReferenceError de la TDZ |
Ejercicios
- Sin ejecutarlo, predice la salida y luego compruébala en jsc:
const base = { greet() { return "hi " + this.n; } }; const o = Object.create(base); o.n = "A"; const fn = o.greet; print(o.greet()); print(typeof fn.call({ n: "B" })); - Reescribe el setter del error común 1 para que
u.name = " lee "guarde"lee",u.namelo devuelva yJSON.stringify(u)imprima{"name":"lee"}.
Respuestas
hi A, luegostring.fn.call({ n: "B" })devuelve"hi B";callfijathisde forma explícita.- Una solución (comprobada en jsc):
class User {
#name = "";
set name(v) { this.#name = String(v).trim(); }
get name() { return this.#name; }
toJSON() { return { name: this.#name }; }
}
const u = new User(); u.name = " lee ";
print(u.name, JSON.stringify(u)); // lee {"name":"lee"}
Límites honestos y notas de versión
- Un solo motor. Todo se ejecutó únicamente en JavaScriptCore de macOS 26.6.2. Los mensajes de error dependen del motor (V8, en Chrome y Node, los redacta distinto); el comportamiento mostrado lo define la especificación ECMAScript.
- Versiones:
class,extends,super,Symbol.hasInstancey los módulos ES son de ES2015.import()dinámico eimport.metason de ES2020. Los campos públicos y privados, los métodos privados, los bloques static,#x in obj,Object.hasOwny elawaitde nivel superior son de ES2022. Todo funcionó en este jsc; no probamos motores más antiguos. - Helpers exclusivos de jsc:
print,fullGC,gcHeapSize,createGlobalObjecty la opción-mexisten solo en la consola jsc. Las cifras de memoria vienen de la contabilidad del heap de JavaScriptCore y serán distintas en V8. - No ejecutado aquí: Node.js y su cargador CommonJS, navegadores,
<script type="module">, bundlers, TypeScript. Las reglas de extensiones y los mensajes de error de esos entornos se describen, no se demuestran.
Anterior: Clase 3: objetos, arreglos y sus métodos · Siguiente: Clase 5: async, promesas y el event loop
Serie: 1 tipos e igualdad · 2 funciones, scope y closures · 6 rendimiento, memoria y números · Relacionados: clases, records e interfaces en Java (en inglés) · clases, mixins y extensiones en Dart (en inglés) · la hoja de ruta para aprender a programar
Preguntas frecuentes
¿Una clase de JavaScript es solo una función con prototype?
Casi, pero no del todo. typeof de una clase da "function" y sus métodos viven en NombreDeClase.prototype, igual que en el patrón antiguo de función constructora. Pero hay diferencias reales: llamar a una clase sin new lanza "Cannot call a class constructor Point without |new|", los métodos de clase no son enumerables y for...in los salta, el cuerpo de la clase siempre está en modo estricto y NombreDeClase.prototype no se puede reasignar.
¿Por qué me sale "undefined is not an object (evaluating 'this.count')"?
Sacaste un método de su objeto (const f = obj.inc, o setTimeout(obj.inc, 0)) y lo llamaste suelto. this depende de cómo se llama la función, así que dentro del cuerpo de la clase, que es estricto, quedó en undefined. Arréglalo con obj.inc.bind(obj), una flecha envolvente (() => obj.inc()) o un campo de clase con función flecha; este último cuesta memoria por instancia (249 contra 40 bytes por objeto en nuestra medición).
¿La consola jsc de JavaScriptCore puede ejecutar módulos ES y require()?
Ejecuta módulos ES: archivos que terminan en .mjs, o cualquier archivo con jsc -m, incluidos import/export, live bindings, import() dinámico y await de nivel superior. No tiene require, module ni exports, así que CommonJS falla con "ReferenceError: Can't find variable: require". En este artículo no se ejecutó Node.js.