Let objects inherit from JavaScript class prototypes

This commit is contained in:
Chris Tate
2026-10-23 23:43:30 -05:00
parent fe3fd0de44
commit 3b951ef4e2
15 changed files with 439 additions and 23 deletions
@@ -1,5 +1,10 @@
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`.
The inherited methods and accessors run with the inheriting object as `this`.
Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class.
`C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons.
Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported.
Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
@@ -82,6 +82,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"util.isDeepStrictEqual": "scr_util_is_deep_strict_equal",
"util.styleText": "scr_util_style_text",
"util.inherits": "scr_util_inherits",
"util.inheritsPrototype": "scr_util_inherits_prototype",
"util.deprecate": "scr_util_deprecate",
"util.callbackify": "scr_util_callbackify",
"util.promisifyDyn": "scr_util_promisify",
@@ -4105,6 +4105,94 @@ export class ClassDynamicDispatch {
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
}
/** `%dyn.class.bagHasOwn[OrPrototype]`: whether an instance's property bag
* holds `key` as its own property, or, with `prototype`, whether the bag's
* prototype object does. False before the instance has a bag. */
private readBagHasOwn(
lowerer: Lowerer,
bag: IrExpr,
key: IrExpr,
prototype: boolean,
loc: SrcLoc,
): IrExpr {
const name = prototype ? "%dyn.class.bagHasOwnOrPrototype" : "%dyn.class.bagHasOwn";
let helper = lowerer.liftedFnNamed(name);
if (!helper) {
const value = varRef("p.0", DYN, loc);
const keyRef = varRef("p.1", STRING, loc);
const proto = varRef("proto", DYN, loc);
const hasOwn = (target: IrExpr): IrExpr => ({
kind: "libCall",
fn: "dyn.hasOwn",
args: [target, keyRef],
type: BOOL,
loc,
});
const returnFalse: IrStmt = {
kind: "return",
value: { kind: "boolLit", value: false, type: BOOL, loc },
loc,
};
helper = {
name,
params: [
{ localId: "p.0", name: "bag", type: DYN },
{ localId: "p.1", name: "key", type: STRING },
],
returnType: BOOL,
locals: [
{ id: "p.0", name: "bag", type: DYN, mutable: false },
{ id: "p.1", name: "key", type: STRING, mutable: false },
...(prototype ? [{ id: "proto", name: "proto", type: DYN, mutable: false }] : []),
],
body: [
{
kind: "if",
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
then: [returnFalse],
else_: null,
loc,
},
...(prototype
? ([
{
kind: "if",
cond: hasOwn(value),
then: [
{
kind: "return",
value: { kind: "boolLit", value: true, type: BOOL, loc },
loc,
},
],
else_: null,
loc,
},
{
kind: "varDecl",
localId: "proto",
init: { kind: "libCall", fn: "dyn.getPrototype", args: [value], type: DYN, loc },
loc,
},
{
kind: "if",
cond: { kind: "dynTest", test: "nullish", value: proto, type: BOOL, loc },
then: [returnFalse],
else_: null,
loc,
},
{ kind: "return", value: hasOwn(proto), loc },
] satisfies IrStmt[])
: [{ kind: "return", value: hasOwn(value), loc } satisfies IrStmt]),
],
loc,
};
lowerer.liftedFns.push(helper);
}
this.generated.add(helper);
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
}
/** The hints for which some boxed class declares a conversion method. A
* string conversion calls toString first, so without a declared toString
* or Symbol.toPrimitive it answers Object.prototype.toString's
@@ -5866,12 +5954,18 @@ export class ClassDynamicDispatch {
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN);
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
// `constructor` is an own property of every class prototype, so an
// ancestor's prototype never answers for an instance of this class:
// the instance's own property answers, then one this class's
// prototype object defines (a reflected prototype, or a write to
// `C.prototype.constructor`), and otherwise the class itself.
const answered = this.readBagHasOwn(lowerer, bag, key, hasClassPrototypeData(info), loc);
return [
{
kind: "return",
value: {
kind: "ternary",
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
cond: answered,
then: {
kind: "libCall",
fn: "dyn.bagGet",
@@ -13,7 +13,7 @@ import {
type IrStmt,
type SrcLoc,
} from "../../ir/ir.js";
import { locOf } from "../program.js";
import { isJsSourceFile, locOf } from "../program.js";
import {
classMemberNameOf,
classValueRef,
@@ -503,6 +503,69 @@ export function isErrorPrototypeValue(
);
}
/** Whether a bare `C.prototype` read becomes the prototype of another object
* or is only compared or inspected: the argument of `Object.create` or
* `Object.getPrototypeOf` / `Reflect.getPrototypeOf`, the prototype argument
* of `Object.setPrototypeOf` / `Reflect.setPrototypeOf`, or an operand of
* `===` / `!==`. In these positions the complete view of the
* prototype (reflectClassPrototype) answers. Objects that inherit from it
* run the class's methods with themselves as the receiver, which the
* methods' receiver-independent lowering (reflectedClassMethodValue)
* supports. Other positions, such as `Object.setPrototypeOf(C.prototype, X)`,
* which would relink the class, keep the diagnostic. */
function prototypeViewPosition(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
let node: ts.Node = expr;
let parent = node.parent;
while (parent && ts.isParenthesizedExpression(parent)) {
node = parent;
parent = parent.parent;
}
if (!parent) return false;
if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) {
const args = parent.arguments.nodes;
const objectMethod = lowerer.stdlibGlobalMember(parent.expression, "Object");
const reflectMethod = lowerer.stdlibGlobalMember(parent.expression, "Reflect");
if (
objectMethod === "create" ||
objectMethod === "getPrototypeOf" ||
reflectMethod === "getPrototypeOf"
)
return args[0] === node;
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf")
return args[1] === node;
return false;
}
return (
ts.isBinaryExpression(parent) &&
(parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)
);
}
/** A module-level JavaScript class named directly (not a class value that
* may hold a subclass at run time): its prototype has a complete view whose
* methods accept any receiver. */
function viewableClass(lowerer: Lowerer, info: ClassInfo, receiver: ts.Expression): boolean {
return (
info.decl !== null &&
info.localClass === undefined &&
isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
exactClassOfReceiver(lowerer, receiver) === info
);
}
/** The complete prototype view of the class `node` names, when it is a
* viewable class (util.inherits's superCtor); null otherwise. */
export function classPrototypeView(
lowerer: Lowerer,
node: ts.Expression,
loc: SrcLoc,
): IrExpr | null {
const info = exactClassOfReceiver(lowerer, node);
if (!info || !viewableClass(lowerer, info, node)) return null;
return reflectClassPrototype(lowerer, info, loc, classValueRef(lowerer, info, node));
}
/** Named data and method slots have native descriptors. Bare reflection and
* accessor replacement still require a complete view of the prototype. */
export function lowerClassPrototypeData(
@@ -566,6 +629,12 @@ export function lowerClassPrototypeData(
});
return reflectClassPrototype(lowerer, info, locOf(expr), receiver);
}
if (
name === null &&
prototypeViewPosition(lowerer, expr) &&
viewableClass(lowerer, info, expr.expression)
)
return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr));
if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) {
lowerer.unsupported(
"SC1090",
@@ -1,5 +1,6 @@
import { keyedHelperName } from "./stable-names.js";
import { perKeyOrder } from "./key-order-witness.js";
import { classPrototypeView } from "./class-prototypes.js";
import {
dynUndefinedExpr,
boolLit,
@@ -3433,8 +3434,14 @@ function lowerUtilCallableCall(
member === "inherits" && expr.arguments.nodes[1] !== undefined
? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
: null;
// A superCtor class whose prototype has a complete view (a module-level
// JavaScript class named directly) passes that view to the runtime.
const superView =
member === "inherits" && builtinBase === null && expr.arguments.nodes[1] !== undefined
? classPrototypeView(lowerer, expr.arguments.nodes[1], loc)
: null;
for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
if (index === 1 && builtinBase !== null) continue;
if (index === 1 && (builtinBase !== null || superView !== null)) continue;
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
lowerer.noLowering(
`util.${member} over a class constructor`,
@@ -3466,7 +3473,9 @@ function lowerUtilCallableCall(
for (const arg of expr.arguments.nodes.slice(arity))
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc });
const type = fn === "util.inherits" ? VOID : DYN;
const result: IrExpr = { kind: "libCall", fn, args, type, loc };
const result: IrExpr = superView
? { kind: "libCall", fn: "util.inheritsPrototype", args: [...args, superView], type, loc }
: { kind: "libCall", fn, args, type, loc };
return { kind: "seqExpr", stmts, result, type: result.type, loc };
}
@@ -591,6 +591,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.styleText",
"util.toUSVString",
"util.inherits",
"util.inheritsPrototype",
"util.deprecate",
"util.callbackify",
"util.promisifyDyn",
@@ -1146,6 +1146,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* arguments (scr_util_callable.c). The function results are native dyn
* wrappers; call sites with a static function type check them back. */
"util.inherits": { argTypes: [DYN, DYN], result: VOID },
"util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID },
"util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
"util.callbackify": { argTypes: [DYN], result: DYN },
/** util.promisify over an untyped function value: a native dyn function
@@ -144,6 +144,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.debuglog",
"util.deprecate",
"util.inherits",
"util.inheritsPrototype",
"util.styleText",
]);
@@ -173,6 +174,7 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.debuglog",
"util.deprecate",
"util.inherits",
"util.inheritsPrototype",
]);
/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8260-class-prototype-views.mjs"
],
"diags": []
}
+42 -1
View File
@@ -2169,6 +2169,40 @@ static bool scr_dyn_prototype_valid(const ScrDyn *prototype) {
}
/* A valid receiver or prototype whose kind this operation does not handle. */
/* Compiled class prototypes. Compiled code binds a class's methods, its
* base chain and its instanceof answers when it compiles, so the
* prototype's own [[Prototype]] is fixed: scr_dyn_set_prototype refuses to
* change it. Each module-level class prototype registers when it is
* created (scr_dyn_name_class_prototype) and stays alive until its thread
* exits, as the class does. */
static SCR_TL ScrDyn **scr_dyn_fixed_prototypes;
static SCR_TL size_t scr_dyn_fixed_count, scr_dyn_fixed_cap;
static void scr_dyn_fixed_prototypes_cleanup(void) {
for (size_t i = 0; i < scr_dyn_fixed_count; i++) scr_dyn_release(scr_dyn_fixed_prototypes[i]);
free(scr_dyn_fixed_prototypes);
scr_dyn_fixed_prototypes = NULL;
scr_dyn_fixed_count = scr_dyn_fixed_cap = 0;
}
static void scr_dyn_fix_prototype(ScrDyn *prototype) {
if (scr_dyn_fixed_count == scr_dyn_fixed_cap) {
if (scr_dyn_fixed_cap == 0) scr_atexit(scr_dyn_fixed_prototypes_cleanup);
size_t cap = scr_dyn_fixed_cap ? scr_dyn_fixed_cap * 2 : 8;
ScrDyn **grown = realloc(scr_dyn_fixed_prototypes, cap * sizeof *grown);
if (!grown) scr_json_oom();
scr_dyn_fixed_prototypes = grown;
scr_dyn_fixed_cap = cap;
}
scr_dyn_fixed_prototypes[scr_dyn_fixed_count++] = scr_dyn_retain(prototype);
}
static bool scr_dyn_prototype_fixed(const ScrDyn *object) {
for (size_t i = 0; i < scr_dyn_fixed_count; i++)
if (scr_dyn_fixed_prototypes[i] == object) return true;
return false;
}
static void scr_dyn_prototype_unsupported(const char *operation) {
ScrJsonBuf b;
scr_jb_init(&b);
@@ -2285,6 +2319,11 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) {
return NULL;
}
if (!scr_dyn_prototype_valid(prototype)) return NULL;
if (object->kind == SCR_DYN_OBJ && scr_dyn_fixed_count && scr_dyn_prototype_fixed(object) &&
!(prototype->kind == SCR_DYN_NULL ? object->null_proto : prototype == object->prototype)) {
scr_dyn_prototype_unsupported("Object.setPrototypeOf");
return NULL;
}
bool typed = scr_dyn_typed_ref_prototype(prototype);
if (scr_exc_pending()) return NULL;
if ((object->kind != SCR_DYN_OBJ && object->kind != SCR_DYN_ARR) ||
@@ -6354,7 +6393,9 @@ static void scr_dyn_name_prototype_chain(const ScrDyn *object) {
}
void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) {
if (prototype->kind != SCR_DYN_OBJ || prototype->v.obj.class_name || name->len == 0) return;
if (prototype->kind != SCR_DYN_OBJ) return;
if (!scr_dyn_prototype_fixed(prototype)) scr_dyn_fix_prototype(prototype);
if (prototype->v.obj.class_name || name->len == 0) return;
scr_dyn_stamp_prototype_name(prototype, name->data, name->len);
}
+2
View File
@@ -62,6 +62,8 @@ bool scr_util_terminal_colors(void);
* (scr_util_promisify.c). Arguments are borrowed; function results are +1
* native dyn functions, or NULL with Node's TypeError pending. */
void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor);
void scr_util_inherits_prototype(struct ScrDyn *ctor, struct ScrDyn *super_ctor,
struct ScrDyn *super_prototype);
struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code,
struct ScrDyn *options);
struct ScrDyn *scr_util_callbackify(struct ScrDyn *original);
+37 -17
View File
@@ -149,23 +149,9 @@ static bool uc_define(ScrDyn *target, const char *key, ScrDyn *descriptor) {
* Node 24's lib/util.js: nullish arguments and an undefined
* superCtor.prototype throw ERR_INVALID_ARG_TYPE; then super_ becomes a
* writable, configurable, non-enumerable own property and
* ctor.prototype's prototype becomes superCtor.prototype. */
void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) {
if (uc_nullish(ctor)) {
scr_dyn_arg_type_fail("ctor", "of type function", ctor);
return;
}
if (uc_nullish(super_ctor)) {
scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor);
return;
}
if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) ||
(super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) {
uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends");
return;
}
ScrDyn *super_prototype = uc_get(super_ctor, "prototype");
if (!super_prototype) return;
* ctor.prototype's prototype becomes superCtor.prototype. super_prototype
* is superCtor.prototype (owned), or NULL when superCtor is no object. */
static void uc_inherits(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) {
if (super_prototype->kind == SCR_DYN_UNDEF) {
scr_dyn_prop_type_fail("superCtor.prototype", "of type object", super_prototype);
scr_dyn_release(super_prototype);
@@ -198,6 +184,40 @@ void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) {
scr_dyn_release(super_prototype);
}
void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) {
if (uc_nullish(ctor)) {
scr_dyn_arg_type_fail("ctor", "of type function", ctor);
return;
}
if (uc_nullish(super_ctor)) {
scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor);
return;
}
if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) ||
(super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) {
uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends");
return;
}
ScrDyn *super_prototype = uc_get(super_ctor, "prototype");
if (!super_prototype) return;
uc_inherits(ctor, super_ctor, super_prototype);
}
/* util.inherits(ctor, C) where C is a compiled class whose prototype the
* compiler exposes as super_prototype (borrowed): the class value's own
* properties do not hold it. ctor itself may not be a class. */
void scr_util_inherits_prototype(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) {
if (uc_nullish(ctor)) {
scr_dyn_arg_type_fail("ctor", "of type function", ctor);
return;
}
if (ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) {
uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends");
return;
}
uc_inherits(ctor, super_ctor, scr_dyn_retain(super_prototype));
}
/* ── util.deprecate ────────────────────────────────────────────────────
* The wrapper warns once (and once per code across wrappers) before each
* call reaches fn with the call's receiver and arguments; construction
+134
View File
@@ -0,0 +1,134 @@
// A class's prototype as the prototype of other objects: ES5 constructors
// that inherit from a class through Object.create, Object.setPrototypeOf and
// util.inherits, and plain objects that adopt a class prototype. Inherited
// methods, accessors and calls between methods run with the object as the
// receiver.
import util from "node:util";
class Shape {
constructor(name) {
this.name = name;
}
get label() {
return `${this.name} (${this.sides()} sides)`;
}
sides() {
return 0;
}
describe() {
return `${this.label}: area ${this.area().toFixed(2)}`;
}
area() {
return 0;
}
}
class Polygon extends Shape {
sides() {
return this.points.length;
}
perimeter() {
let total = 0;
for (let i = 0; i < this.points.length; i++) {
const [x1, y1] = this.points[i];
const [x2, y2] = this.points[(i + 1) % this.points.length];
total += Math.hypot(x2 - x1, y2 - y1);
}
return total;
}
}
// Object.create(Class.prototype) for an ES5 subclass.
function Square(size) {
this.name = "square";
this.size = size;
this.points = [
[0, 0],
[size, 0],
[size, size],
[0, size],
];
}
Square.prototype = Object.create(Polygon.prototype);
Square.prototype.constructor = Square;
Square.prototype.area = function () {
return this.size * this.size;
};
const square = new Square(3);
console.log(square.describe());
console.log(square.perimeter(), square.sides(), square.label);
console.log(square instanceof Square, square instanceof Polygon, square instanceof Shape);
console.log(Object.getPrototypeOf(Square.prototype) === Polygon.prototype);
console.log(Object.getPrototypeOf(Polygon.prototype) === Shape.prototype);
console.log(Object.keys(square).join(","));
const inherited = [];
for (const key in square) inherited.push(key);
console.log(inherited.join(","));
// Object.setPrototypeOf on an ES5 constructor's prototype.
function Circle(radius) {
this.name = "circle";
this.radius = radius;
}
Circle.prototype.area = function () {
return Math.PI * this.radius * this.radius;
};
Object.setPrototypeOf(Circle.prototype, Shape.prototype);
const circle = new Circle(2);
console.log(circle.describe(), circle instanceof Shape, circle instanceof Polygon);
// util.inherits with a class as the superCtor.
function Triangle(a, b, c) {
this.name = "triangle";
this.points = [a, b, c];
}
util.inherits(Triangle, Polygon);
Triangle.prototype.area = function () {
const [[x1, y1], [x2, y2], [x3, y3]] = this.points;
return Math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2);
};
const triangle = new Triangle([0, 0], [4, 0], [0, 3]);
console.log(Triangle.super_ === Polygon, triangle instanceof Polygon);
console.log(triangle.describe(), triangle.perimeter());
// A plain object that adopts a class prototype (rehydrating parsed data).
const parsed = JSON.parse('{"name":"lot","points":[[0,0],[2,0],[2,1]]}');
Object.setPrototypeOf(parsed, Polygon.prototype);
console.log(parsed.sides(), parsed.perimeter().toFixed(3), parsed.label, parsed instanceof Polygon);
const fresh = Object.create(Shape.prototype);
fresh.name = "blank";
console.log(fresh.describe(), fresh instanceof Shape, Object.getPrototypeOf(fresh) === Shape.prototype);
// Native instances keep their own methods and answers.
const native = new Polygon("native");
native.points = [
[0, 0],
[1, 0],
[0, 1],
];
console.log(native.describe(), native.perimeter().toFixed(3), native instanceof Polygon);
// util.inspect names each object by its prototype's constructor.
function Plain() {
this.v = 1;
}
Object.setPrototypeOf(Plain.prototype, Shape.prototype);
console.log(new Square(1).size, fresh, new Plain(), Object.prototype.toString.call(fresh));
console.log(util.inspect(new Plain()), util.inspect(parsed, { depth: 0 }));
// A subclass keeps its own constructor below a base whose prototype other
// objects inherit: `new this.constructor()` in a base method builds the
// subclass, also while the subclass's prototype holds a replaceable method.
class Disc extends Shape {
area() {
return 3;
}
rebuild() {
return new this.constructor("copy of " + this.name);
}
}
const areaValue = Shape.prototype.area;
const disc = new Disc("d");
const rebuilt = disc.rebuild();
console.log(disc.constructor === Disc, rebuilt instanceof Disc, rebuilt.area(), rebuilt.name, typeof areaValue);
@@ -0,0 +1 @@
{ "plain": 8037, "san": 6172 }
+30
View File
@@ -2190,3 +2190,33 @@ for (const run of runs) {
);
});
});
describe("class prototype relinks (JS lane)", () => {
test("relinking a class prototype reached through an instance throws", async () => {
// Compiled code binds a class's inherited methods, so its prototype's
// [[Prototype]] cannot change at run time. Node would print "other";
// the native program refuses instead of answering A's method.
const r = await compileAndRun(
"class-prototype-relink",
`'use strict';
class A {
a() {
return 'A.a';
}
}
class B extends A {}
const prototype = Object.getPrototypeOf(new B());
Object.setPrototypeOf(prototype, Object.getPrototypeOf(prototype));
console.log('same prototype', new B().a());
Object.setPrototypeOf(prototype, { a: () => 'other' });
console.log(new B().a());
`,
"cjs",
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("same prototype A.a\n");
expect(r.stderr).toBe(
"Uncaught Error: Object.setPrototypeOf with this value is not supported yet\n",
);
});
});