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") dio false mientras que "legs" in cat dio true: in recorre la cadena, hasOwn (ES2022) no.
  • Escribir nunca recorre la cadena. cat.legs = 3 creó una propiedad propia nueva en cat (a esto se le llama shadowing, sombrear); animal.legs siguió en 4. Pero cambiar animal.legs después cambió lo que ve bird, porque bird no tiene legs propio. Los datos mutables compartidos en un prototipo son una fuente clásica de bugs.
  • for...in incluye claves enumerables heredadas (apareció describe), Object.keys no. Usa Object.keys/Object.entries para 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.

Diagrama de la cadena. El objeto c, creado con new Circle(1), tiene como propias las propiedades de datos name y r. Su prototipo es Circle.prototype, que contiene area y constructor. El prototipo de ese es Shape.prototype, que contiene toString y su propio area. Encima está Object.prototype con hasOwnProperty, y encima de él null, donde la búsqueda se detiene y devuelve undefined.
Cómo se encuentran c.area() y c.toString(): el motor mira primero el objeto y luego sigue el enlace [[Prototype]] de cada objeto, subiendo por la cadena hasta encontrar el nombre o llegar a null.

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:

  1. Olvidar new. Con la función antigua, PointFn(1, 2) devolvió undefined y escribió en silencio x e y en el objeto global (globalThis.x pasó a valer 1). Una clase se niega con un TypeError. Eso solo ya es una buena razón para preferir class.
  2. Los métodos no son enumerables. len se coló en for...in en la versión con función, no en la clase.
  3. El cuerpo de la clase está en modo estricto. Vas a ver qué significa eso para this en la sección del this perdido.
  4. Point.prototype es de solo lectura. Lo mostramos en la sección de instanceof.

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 = 999 no 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.keys y JSON.stringify. Si serializas una clase con estado privado, escribe un método toJSON().
  • #balance in o comprueba 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
Gráfico de barras. Un método en el prototipo costó 40.4 bytes por instancia. La misma clase con un #campo privado costó 72.4 bytes. Hacer bind del método en el constructor costó 136.4 bytes, y un campo de clase con función flecha costó 249.4 bytes, unas seis veces el método de prototipo.
Bytes de heap por instancia con 100,000 objetos vivos de la misma clase, escrita de cuatro formas (gcHeapSize tras fullGC, incluye el espacio de 8 bytes del arreglo). Idéntico en 5 ejecuciones, jsc en macOS 26.6.2, Apple M1 Pro.

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

  1. 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" }));
  2. Reescribe el setter del error común 1 para que u.name = " lee " guarde "lee", u.name lo devuelva y JSON.stringify(u) imprima {"name":"lee"}.

Respuestas

  1. hi A, luego string. fn.call({ n: "B" }) devuelve "hi B"; call fija this de forma explícita.
  2. 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.hasInstance y los módulos ES son de ES2015. import() dinámico e import.meta son de ES2020. Los campos públicos y privados, los métodos privados, los bloques static, #x in obj, Object.hasOwn y el await de nivel superior son de ES2022. Todo funcionó en este jsc; no probamos motores más antiguos.
  • Helpers exclusivos de jsc: print, fullGC, gcHeapSize, createGlobalObject y la opción -m existen 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.

  • #javascript
  • #intermedio
  • #clases
  • #prototipos
  • #módulos
  • #jsc
  • #javascriptcore

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

Acerca de · Apps

↑