mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Run private members and super calls over foreign receivers like Node (#1975)
This commit is contained in:
@@ -378,8 +378,33 @@ function reflectedMethodValue(
|
|||||||
!isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
|
!isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
|
||||||
info.localClass !== undefined ||
|
info.localClass !== undefined ||
|
||||||
lowerer.errorHierarchyClassOf(info.def.name);
|
lowerer.errorHierarchyClassOf(info.def.name);
|
||||||
|
// `super.m(...)` in a prototype method reads the base class's prototype,
|
||||||
|
// which lowerSuperMethodCall answers over the call-site receiver when the
|
||||||
|
// base has a prototype object. Other uses of `super` keep the native
|
||||||
|
// receiver.
|
||||||
|
const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base);
|
||||||
|
// Over a dynamic receiver, private fields are brand-checked and read, and private methods
|
||||||
|
// brand-checked and called; a private accessor, or a private method read as a value, would become a
|
||||||
|
// lookup of its name.
|
||||||
|
const privateAccessors = new Set<string>();
|
||||||
|
const privateMethods = new Set<string>();
|
||||||
|
for (const other of declaration.members.nodes)
|
||||||
|
if (other.name !== undefined && ts.isPrivateIdentifier(other.name)) {
|
||||||
|
if (ts.isAccessor(other)) privateAccessors.add(other.name.text);
|
||||||
|
else if (ts.isMethodDeclaration(other)) privateMethods.add(other.name.text);
|
||||||
|
}
|
||||||
ts.walkPreorder(member, (node) => {
|
ts.walkPreorder(member, (node) => {
|
||||||
if (ts.isPrivateIdentifier(node) || node.kind === ts.SyntaxKind.SuperKeyword)
|
if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node)))
|
||||||
|
needsNativeReceiver = true;
|
||||||
|
if (!ts.isPrivateIdentifier(node)) return;
|
||||||
|
// Private members on a static method's receiver (the class value) keep
|
||||||
|
// the native receiver; prototype methods brand-check theirs, except
|
||||||
|
// private accessors and private methods used other than by a call.
|
||||||
|
if (
|
||||||
|
staticMethod ||
|
||||||
|
privateAccessors.has(node.text) ||
|
||||||
|
(privateMethods.has(node.text) && !isPrivateMethodCallee(node))
|
||||||
|
)
|
||||||
needsNativeReceiver = true;
|
needsNativeReceiver = true;
|
||||||
});
|
});
|
||||||
if (needsNativeReceiver) return null;
|
if (needsNativeReceiver) return null;
|
||||||
@@ -403,7 +428,11 @@ function reflectedMethodValue(
|
|||||||
loc: locOf(member),
|
loc: locOf(member),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// The method may be lowered on top of an unrelated caller's frames (a
|
||||||
|
// `super` call in another class's method reflects this class): its `this`
|
||||||
|
// is its own receiver, never an enclosing function's.
|
||||||
const context = newFnCtx(false, null, null, DYN);
|
const context = newFnCtx(false, null, null, DYN);
|
||||||
|
context.thisBoundary = true;
|
||||||
const previousClass = lowerer.currentClass;
|
const previousClass = lowerer.currentClass;
|
||||||
lowerer.currentClass = info;
|
lowerer.currentClass = info;
|
||||||
lowerer.fnStack.push(context);
|
lowerer.fnStack.push(context);
|
||||||
@@ -441,6 +470,55 @@ function reflectedMethodValue(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether `base` and every class above it have a complete prototype object
|
||||||
|
* (reflectClassPrototype): module-level program classes that neither extend
|
||||||
|
* Date nor a runtime-provided class. */
|
||||||
|
function prototypeChainViewable(base: ClassInfo): boolean {
|
||||||
|
for (let c: ClassInfo | null = base; c; c = c.base)
|
||||||
|
if (
|
||||||
|
c.decl === null ||
|
||||||
|
c.localClass !== undefined ||
|
||||||
|
c.generic !== undefined ||
|
||||||
|
c.genericInstance !== undefined ||
|
||||||
|
c.mixinInstance !== undefined ||
|
||||||
|
c.callableBase !== undefined ||
|
||||||
|
c.extendsDate !== undefined ||
|
||||||
|
c.def.runtime ||
|
||||||
|
c.builtinEmitter ||
|
||||||
|
c.builtinStream ||
|
||||||
|
c.builtinError ||
|
||||||
|
c.builtinEventTarget
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `super` as the receiver of a called member: `super.m(...)`. */
|
||||||
|
/** Whether `node`, a private name, is the method a call calls (`x.#m(...)`). */
|
||||||
|
function isPrivateMethodCallee(node: ts.Node): boolean {
|
||||||
|
const access = node.parent;
|
||||||
|
return (
|
||||||
|
access !== undefined &&
|
||||||
|
ts.isPropertyAccessExpression(access) &&
|
||||||
|
access.name === node &&
|
||||||
|
access.parent !== undefined &&
|
||||||
|
ts.isCallExpression(access.parent) &&
|
||||||
|
access.parent.expression === access
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSuperMethodCallee(node: ts.Node): boolean {
|
||||||
|
const access = node.parent;
|
||||||
|
return (
|
||||||
|
access !== undefined &&
|
||||||
|
ts.isPropertyAccessExpression(access) &&
|
||||||
|
access.expression === node &&
|
||||||
|
access.parent !== undefined &&
|
||||||
|
ts.isCallExpression(access.parent) &&
|
||||||
|
access.parent.expression === access
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Complete adapters after all native class bodies and instantiations exist. */
|
/** Complete adapters after all native class bodies and instantiations exist. */
|
||||||
export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void {
|
export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||||
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
||||||
|
|||||||
@@ -423,7 +423,10 @@ export function reflectClassPrototype(
|
|||||||
loc,
|
loc,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// The members lower on top of whatever frames requested the view (a
|
||||||
|
// `super` call in a subclass's method); their `this` is their own receiver.
|
||||||
const context = newFnCtx(false, null, null, DYN);
|
const context = newFnCtx(false, null, null, DYN);
|
||||||
|
context.thisBoundary = true;
|
||||||
lowerer.fnStack.push(context);
|
lowerer.fnStack.push(context);
|
||||||
try {
|
try {
|
||||||
for (const member of info.decl.members.nodes) {
|
for (const member of info.decl.members.nodes) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
|
||||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||||
import { keyedHelperName } from "./stable-names.js";
|
import { keyedHelperName } from "./stable-names.js";
|
||||||
import { perKeyOrder } from "./key-order-witness.js";
|
import { perKeyOrder } from "./key-order-witness.js";
|
||||||
@@ -589,6 +590,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
|||||||
) {
|
) {
|
||||||
return lowerer.lowerSuperMethodCall(expr, expr.expression);
|
return lowerer.lowerSuperMethodCall(expr, expr.expression);
|
||||||
}
|
}
|
||||||
|
// `value.#m(...)` on a checked-dynamic value (`this` in a JavaScript
|
||||||
|
// class's receiver-independent method): the private method of the
|
||||||
|
// brand-checked receiver.
|
||||||
|
if (
|
||||||
|
ts.isPropertyAccessExpression(expr.expression) &&
|
||||||
|
ts.isPrivateIdentifier(expr.expression.name) &&
|
||||||
|
!expr.expression.questionDotToken
|
||||||
|
) {
|
||||||
|
const privateCall = lowerCheckedPrivateMethodCall(lowerer, expr, expr.expression);
|
||||||
|
if (privateCall) return privateCall;
|
||||||
|
}
|
||||||
|
|
||||||
const consoleMember = lowerer.consoleCallMember(expr);
|
const consoleMember = lowerer.consoleCallMember(expr);
|
||||||
const consoleMethod = consoleMember === null ? lowerer.consoleMethodMember(expr) : null;
|
const consoleMethod = consoleMember === null ? lowerer.consoleMethodMember(expr) : null;
|
||||||
@@ -11430,3 +11442,35 @@ function lowerShimIntrinsicCall(
|
|||||||
);
|
);
|
||||||
return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc };
|
return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `value.#m(args)` where `value` lowers to a checked-dynamic value and the
|
||||||
|
* declaration-level class whose body declares `#m` declares it as a method:
|
||||||
|
* the method called on the brand-checked receiver
|
||||||
|
* (checkedPrivateMethodReceiver). Null for any other private member call. */
|
||||||
|
function lowerCheckedPrivateMethodCall(
|
||||||
|
lowerer: Lowerer,
|
||||||
|
call: ts.CallExpression,
|
||||||
|
access: ts.PropertyAccessExpression,
|
||||||
|
): IrExpr | null {
|
||||||
|
if (!ts.isPrivateIdentifier(access.name)) return null;
|
||||||
|
const name = access.name.text;
|
||||||
|
const owner = privateNameClass(lowerer, access.name);
|
||||||
|
const found = owner ? lowerer.findMethodOn(owner, name) : null;
|
||||||
|
if (!owner || !found || found.declarer !== owner || found.sig.abstract === true) return null;
|
||||||
|
const probed = tryLowerExpression(lowerer, access.expression);
|
||||||
|
if (!probed || !checkedDynamicOperand(probed)) return null;
|
||||||
|
const value = checkedDynamicOperand(lowerer.lowerExpr(access.expression));
|
||||||
|
if (!value) return null;
|
||||||
|
const loc = locOf(call);
|
||||||
|
const callee = `%${owner.def.name}.${name}`;
|
||||||
|
lowerer.noteEdge(callee);
|
||||||
|
const receiver = checkedPrivateMethodReceiver(lowerer, owner, name, value, loc);
|
||||||
|
const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call);
|
||||||
|
return { kind: "call", callee, args: [receiver, ...args], type: found.sig.ret, loc };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A checked-dynamic value, or the dynamic value under a checked extraction. */
|
||||||
|
function checkedDynamicOperand(expr: IrExpr): IrExpr | null {
|
||||||
|
if (expr.type.kind === "dyn") return expr;
|
||||||
|
return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ import {
|
|||||||
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||||
import { classPrototypeData } from "./class-prototypes.js";
|
import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js";
|
||||||
import { constructionEscape } from "./construction-escapes.js";
|
import { constructionEscape } from "./construction-escapes.js";
|
||||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||||
@@ -9430,8 +9430,16 @@ export function lowerSuperMethodCall(
|
|||||||
if (!thisLocal) lowerer.unsupported("SC1080", access);
|
if (!thisLocal) lowerer.unsupported("SC1080", access);
|
||||||
const loc = locOf(call);
|
const loc = locOf(call);
|
||||||
const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc };
|
const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc };
|
||||||
if (lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
// A method whose `this` is the call-site receiver (a JavaScript class's
|
||||||
const prototype = classPrototypeData(lowerer, cls.base, loc);
|
// receiver-independent method value, reflectedClassMethodValue) looks the
|
||||||
|
// method up on the base class's complete prototype, as `super` reads
|
||||||
|
// [[HomeObject]].[[Prototype]], and calls it with that receiver.
|
||||||
|
const dynamicThis = thisRef.type.kind === "dyn";
|
||||||
|
if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
||||||
|
const prototype =
|
||||||
|
dynamicThis && cls.base.decl
|
||||||
|
? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access))
|
||||||
|
: classPrototypeData(lowerer, cls.base, loc);
|
||||||
if (prototype) {
|
if (prototype) {
|
||||||
const callee: IrExpr = {
|
const callee: IrExpr = {
|
||||||
kind: "dynKeyGet",
|
kind: "dynKeyGet",
|
||||||
@@ -9440,12 +9448,21 @@ export function lowerSuperMethodCall(
|
|||||||
type: DYN,
|
type: DYN,
|
||||||
loc,
|
loc,
|
||||||
};
|
};
|
||||||
|
// Spread arguments are spread into the dynamic call, as in a dynamic method call.
|
||||||
|
const spreads: { arg: number; what: string }[] = [];
|
||||||
|
const args = call.arguments.nodes.map((arg, index) => {
|
||||||
|
if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN);
|
||||||
|
const spelling = ts.getTextOfNode(arg.expression);
|
||||||
|
spreads.push({ arg: index, what: spelling });
|
||||||
|
return checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc);
|
||||||
|
});
|
||||||
const value: IrExpr = {
|
const value: IrExpr = {
|
||||||
kind: "dynCall",
|
kind: "dynCall",
|
||||||
callee,
|
callee,
|
||||||
receiver: lowerer.coerceToExpected(thisRef, DYN),
|
receiver: lowerer.coerceToExpected(thisRef, DYN),
|
||||||
calleeName: ts.getTextOfNode(access),
|
calleeName: ts.getTextOfNode(access),
|
||||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
args,
|
||||||
|
...(spreads.length > 0 ? { spreads } : {}),
|
||||||
type: DYN,
|
type: DYN,
|
||||||
loc,
|
loc,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import type { Lowerer } from "./lowerer.js";
|
|||||||
import type { ClassInfo } from "./lower-classes.js";
|
import type { ClassInfo } from "./lower-classes.js";
|
||||||
|
|
||||||
/** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and
|
/** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and
|
||||||
* `write` return the receiver as the declaring class for a field access. */
|
* `write` return the receiver as the declaring class for a field access, and
|
||||||
type BrandUse = "in" | "read" | "write";
|
* `call` for a private method call. */
|
||||||
|
type BrandUse = "in" | "read" | "write" | "call";
|
||||||
|
|
||||||
const PREFIX = "%private.";
|
const PREFIX = "%private.";
|
||||||
|
|
||||||
@@ -214,6 +215,35 @@ export function checkedPrivateValue(
|
|||||||
return { kind: "call", callee: helper, args: [receiver], type: resultType, loc };
|
return { kind: "call", callee: helper, args: [receiver], type: resultType, loc };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `value.#m(...)` on a checked-dynamic `value` in a declaration-level class
|
||||||
|
* `info` that declares the private method `#m`: the receiver as an instance
|
||||||
|
* of `info` when the value is one, else Node's TypeError ("Receiver must be
|
||||||
|
* an instance of class C"). */
|
||||||
|
export function checkedPrivateMethodReceiver(
|
||||||
|
lowerer: Lowerer,
|
||||||
|
info: ClassInfo,
|
||||||
|
name: string,
|
||||||
|
receiver: IrExpr,
|
||||||
|
loc: SrcLoc,
|
||||||
|
): IrExpr {
|
||||||
|
const className = info.def.name;
|
||||||
|
const resultType: IrType = { kind: "object", className };
|
||||||
|
const helper = `${PREFIX}call:${className}:${name}`;
|
||||||
|
if (!lowerer.valueHelpers.has(helper)) {
|
||||||
|
lowerer.liftedFns.push({
|
||||||
|
name: helper,
|
||||||
|
params: [{ localId: "value.0", name: "value", type: DYN }],
|
||||||
|
returnType: resultType,
|
||||||
|
locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }],
|
||||||
|
// finalizePrivateBrands fills the body.
|
||||||
|
body: [],
|
||||||
|
loc: helperLoc(loc),
|
||||||
|
});
|
||||||
|
lowerer.valueHelpers.set(helper, helper);
|
||||||
|
}
|
||||||
|
return { kind: "call", callee: helper, args: [receiver], type: resultType, loc };
|
||||||
|
}
|
||||||
|
|
||||||
/** A plain assignment, a destructuring target, or a for-in/of target writes
|
/** A plain assignment, a destructuring target, or a for-in/of target writes
|
||||||
* the field. Compound assignments and updates read it first, and Node
|
* the field. Compound assignments and updates read it first, and Node
|
||||||
* reports the failed read. */
|
* reports the failed read. */
|
||||||
@@ -358,6 +388,22 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[])
|
|||||||
},
|
},
|
||||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||||
);
|
);
|
||||||
|
} else if (use === "call") {
|
||||||
|
// V8 names the class by its own identifier (not a name inferred from
|
||||||
|
// a binding), "anonymous" without one, also for a nullish receiver.
|
||||||
|
const owner = lowerer.classes.get(className);
|
||||||
|
const identifier = owner?.decl?.name;
|
||||||
|
const shown = identifier !== undefined ? identifier.text : "anonymous";
|
||||||
|
for (const unit of ["undefined", "null"] as const) {
|
||||||
|
body.push({
|
||||||
|
kind: "if",
|
||||||
|
cond: { kind: "dynTest", test: unit, value, type: BOOL, loc },
|
||||||
|
then: [typeError(strLit(`Cannot read properties of ${unit} (reading '${shown}')`, loc))],
|
||||||
|
else_: null,
|
||||||
|
loc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
body.push(typeError(strLit(`Receiver must be an instance of class ${shown}`, loc)));
|
||||||
} else {
|
} else {
|
||||||
const verb = use === "read" ? "reading" : "setting";
|
const verb = use === "read" ? "reading" : "setting";
|
||||||
const action = use === "read" ? "Cannot read properties" : "Cannot set properties";
|
const action = use === "read" ? "Cannot read properties" : "Cannot set properties";
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"order": [
|
||||||
|
"<repo>/tests/corpus/7621-class-methods-foreign-receivers.mjs"
|
||||||
|
],
|
||||||
|
"diags": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// Prototype methods called with a receiver that is not an instance of
|
||||||
|
// their class (`C.prototype.m.call(other)`): `#private` members throw
|
||||||
|
// Node's TypeErrors for such a receiver, and `super.m()` looks the method up
|
||||||
|
// on the base class's prototype and calls it with the receiver.
|
||||||
|
function report(label, run) {
|
||||||
|
try {
|
||||||
|
console.log(label, run());
|
||||||
|
} catch (error) {
|
||||||
|
console.log(label, error.constructor.name, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Counter {
|
||||||
|
#count = 0;
|
||||||
|
#step = 1;
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
read() {
|
||||||
|
return this.#count;
|
||||||
|
}
|
||||||
|
bump() {
|
||||||
|
this.#count += this.#step;
|
||||||
|
return this.#count;
|
||||||
|
}
|
||||||
|
reset(value) {
|
||||||
|
this.#count = value;
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
#label() {
|
||||||
|
return `${this.name}#${this.#count}`;
|
||||||
|
}
|
||||||
|
label() {
|
||||||
|
return this.#label();
|
||||||
|
}
|
||||||
|
owns(value) {
|
||||||
|
return #count in value;
|
||||||
|
}
|
||||||
|
ownsSelf() {
|
||||||
|
return #count in this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const counter = new Counter("c");
|
||||||
|
console.log(counter.bump(), counter.bump(), counter.read(), counter.label());
|
||||||
|
console.log(counter.owns(counter), counter.owns({}), counter.ownsSelf(), counter.reset(7), counter.read());
|
||||||
|
|
||||||
|
const plain = { name: "plain" };
|
||||||
|
report("read", () => Counter.prototype.read.call(plain));
|
||||||
|
report("bump", () => Counter.prototype.bump.call(plain));
|
||||||
|
report("reset", () => Counter.prototype.reset.call(plain, 3));
|
||||||
|
report("label", () => Counter.prototype.label.call(plain));
|
||||||
|
report("ownsSelf", () => Counter.prototype.ownsSelf.call(plain));
|
||||||
|
report("owns primitive", () => Counter.prototype.owns.call(counter, 5));
|
||||||
|
report("label undefined", () => Counter.prototype.label.call(undefined));
|
||||||
|
report("label null", () => Counter.prototype.label.call(null));
|
||||||
|
report("label number", () => Counter.prototype.label.call(4));
|
||||||
|
report("read other instance", () => Counter.prototype.read.call(new Counter("d")));
|
||||||
|
report("label other instance", () => Counter.prototype.label.call(new Counter("e")));
|
||||||
|
|
||||||
|
const Anonymous = class {
|
||||||
|
#m() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
call() {
|
||||||
|
return this.#m();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const Named = class Inner {
|
||||||
|
#m() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
call() {
|
||||||
|
return this.#m();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
report("anonymous", () => Anonymous.prototype.call.call({}));
|
||||||
|
report("named", () => Named.prototype.call.call({}));
|
||||||
|
console.log(new Anonymous().call(), new Named().call());
|
||||||
|
|
||||||
|
class Shape {
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
describe() {
|
||||||
|
return `shape ${this.name}`;
|
||||||
|
}
|
||||||
|
area() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Square extends Shape {
|
||||||
|
describe() {
|
||||||
|
return `square: ${super.describe()} area ${this.area()}`;
|
||||||
|
}
|
||||||
|
area() {
|
||||||
|
return this.size * this.size;
|
||||||
|
}
|
||||||
|
both() {
|
||||||
|
return [super.describe(), super.area()].join("|");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Tiny extends Square {
|
||||||
|
#tag = "t";
|
||||||
|
describe() {
|
||||||
|
return `tiny<${super.describe()}>`;
|
||||||
|
}
|
||||||
|
tag() {
|
||||||
|
return `${this.#tag}:${super.both()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const square = new Square("s");
|
||||||
|
square.size = 3;
|
||||||
|
console.log(square.describe(), square.both());
|
||||||
|
const tiny = new Tiny("t");
|
||||||
|
tiny.size = 1;
|
||||||
|
console.log(tiny.describe(), tiny.tag());
|
||||||
|
const foreign = {
|
||||||
|
name: "f",
|
||||||
|
size: 2,
|
||||||
|
area() {
|
||||||
|
return 40;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
report("square describe", () => Square.prototype.describe.call(foreign));
|
||||||
|
report("square both", () => Square.prototype.both.call(foreign));
|
||||||
|
report("tiny describe", () => Tiny.prototype.describe.call(foreign));
|
||||||
|
report("tiny tag", () => Tiny.prototype.tag.call(foreign));
|
||||||
|
report("square describe on tiny", () => Square.prototype.describe.call(tiny));
|
||||||
|
|
||||||
|
// A base method that builds `new this.constructor()` on a subclass instance
|
||||||
|
// builds the subclass, also once the base prototype is complete (a super call
|
||||||
|
// above reads it) and `copy` is read through its prototype slot.
|
||||||
|
class Node3 {
|
||||||
|
constructor() {
|
||||||
|
this.a = 0;
|
||||||
|
}
|
||||||
|
copy(source) {
|
||||||
|
this.a = source.a;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
clone() {
|
||||||
|
return new this.constructor().copy(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Mesh3 extends Node3 {
|
||||||
|
constructor(geometry) {
|
||||||
|
super();
|
||||||
|
this.geometry = geometry;
|
||||||
|
}
|
||||||
|
copy(source) {
|
||||||
|
super.copy(source);
|
||||||
|
this.geometry = source.geometry;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const copyValue = Node3.prototype.copy;
|
||||||
|
const mesh = new Mesh3({ id: 1 });
|
||||||
|
mesh.a = 5;
|
||||||
|
const meshClone = mesh.clone();
|
||||||
|
console.log(
|
||||||
|
meshClone instanceof Mesh3,
|
||||||
|
meshClone.constructor === Mesh3,
|
||||||
|
mesh.constructor === Mesh3,
|
||||||
|
meshClone.a,
|
||||||
|
JSON.stringify(meshClone.geometry),
|
||||||
|
typeof copyValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Methods that use a private accessor or return a private method keep their own receiver, as values too.
|
||||||
|
class Vault {
|
||||||
|
#v = 1;
|
||||||
|
get #value() {
|
||||||
|
return this.#v * 10;
|
||||||
|
}
|
||||||
|
set #value(x) {
|
||||||
|
this.#v = x;
|
||||||
|
}
|
||||||
|
#hidden() {
|
||||||
|
return "hidden " + this.#v;
|
||||||
|
}
|
||||||
|
read() {
|
||||||
|
return this.#value;
|
||||||
|
}
|
||||||
|
bump() {
|
||||||
|
this.#value = this.#v + 1;
|
||||||
|
return this.#v;
|
||||||
|
}
|
||||||
|
hiddenValue() {
|
||||||
|
return this.#hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const vault = new Vault();
|
||||||
|
const readVault = Vault.prototype.read;
|
||||||
|
const bumpVault = Vault.prototype.bump;
|
||||||
|
const hiddenOf = Vault.prototype.hiddenValue;
|
||||||
|
console.log(readVault.call(vault), bumpVault.call(vault), readVault.call(vault), typeof hiddenOf.call(vault));
|
||||||
|
console.log(hiddenOf.call(vault).call(vault));
|
||||||
|
try {
|
||||||
|
readVault.call({});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error.constructor.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A method value whose body calls super with spread arguments.
|
||||||
|
class Summer {
|
||||||
|
add(...xs) {
|
||||||
|
return xs.reduce((sum, x) => sum + x, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Doubler extends Summer {
|
||||||
|
add(...xs) {
|
||||||
|
return super.add(...xs) * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const addValue = Doubler.prototype.add;
|
||||||
|
console.log(addValue.call(new Doubler(), 1, 2, 3), addValue.apply(new Doubler(), [4, 5]));
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "plain": 6415, "san": 10107 }
|
||||||
Reference in New Issue
Block a user