Proxy Reflect.has() call causes infinite recursion?
#!/JoePea
joe at trusktr.io
Thu Nov 21 21:25:49 UTC 2019
Sorry you all, I realized I should've simplified it. Here's a [simpler
fiddle](https://jsfiddle.net/trusktr/tf6hdn48/6/).
On Thu, Nov 21, 2019 at 1:17 PM #!/JoePea <joe at trusktr.io> wrote:
>
> I was trying to implement "multiple inheritance" in the following code
> ([jsfiddle](https://jsfiddle.net/trusktr/tf6hdn48/)), but it gives a
> max call stack (infinite recursion) error.
>
> However, the infinite recursion does not execute any of my console.log
> statements repeatedly like I'd expect, so it seems that the infinite
> recursion is happening inside the JS engine?
>
> What's going on?
>
> Here's the code for reference, see "PROXY INFINITE RECURSION" comments
> for the site where the problem is happening (as far as I can tell in
> devtools):
>
> ```js
> "use strict";
>
> function multiple(...classes) {
> if (classes.length === 0)
> return Object;
> if (classes.length === 1) {
> const result = classes[0];
> return (typeof result === 'function' ? result : Object);
> }
>
> const FirstClass = classes.shift();
> const __instances__ = new WeakMap();
> const getInstances = (inst) => {
> let result = __instances__.get(inst);
> if (!result)
> __instances__.set(inst, (result = []));
> return result;
> };
>
> class MultiClass extends FirstClass {
> constructor(...args) {
> super(...args);
>
> const protoBeforeMultiClassProto =
> findPrototypeBeforeMultiClassPrototype(this, MultiClass.prototype);
> if (protoBeforeMultiClassProto)
> Object.setPrototypeOf(protoBeforeMultiClassProto,
> newMultiClassPrototype);
>
> const instances = getInstances(this);
>
> for (const Ctor of classes)
> instances.push(new Ctor(...args));
> }
> }
>
> let count = 0;
> const newMultiClassPrototype = new Proxy({
> __proto__: MultiClass.prototype,
> }, {
> get(target, key, self) {
> if (count++ < 500) console.log(' --------------- get', key);
>
> if (Reflect.has(MultiClass.prototype, key))
> return Reflect.get(MultiClass.prototype, key, self);
>
> for (const instance of getInstances(self))
> if (Reflect.has(instance, key))
> return Reflect.get(instance, key, self);
>
> return undefined;
> },
>
> set(target, key, value, self) {
> console.log(' ----- set1', key, value);
>
> // PROXY INFINITE RECURSION HERE:
> console.log('hmmmmmmmmmmmmmmmm?',
> Reflect.has(MultiClass.prototype, key));
>
> console.log(' ----- set1.5', key, value);
>
> if (Reflect.has(MultiClass.prototype, key)) {
> return Reflect.set(target, key, value, self);
> }
>
> const instances = getInstances(self);
>
> for (const instance of instances) {
> if (Reflect.has(instance, key)) {
> return Reflect.set(instance, key, value, self);
> }
> }
>
> return Reflect.set(target, key, value, self);
> },
> });
>
> return MultiClass;
> }
>
> function findPrototypeBeforeMultiClassPrototype(obj, multiClassPrototype) {
> let previous = obj;
> let current = Object.getPrototypeOf(obj);
> while (current) {
> // debugger
> if (current === multiClassPrototype)
> return previous;
> previous = current;
> current = Object.getPrototypeOf(current);
> }
> return null;
> }
>
> async function test() {
> await new Promise(r => setTimeout(r, 3000));
> console.log('-------------------------------------');
> const R1 = multiple();
> const r1 = new R1();
> console.log(Object.keys(r1));
> console.log('-------------------------------------');
> class Foo {
> constructor() {
> this.f = false;
> }
> }
> const R2 = multiple(Foo);
> const r2 = new R2();
> console.log(r2.hasOwnProperty);
> console.log('f', r2.f);
> console.log('-------------------------------------');
> class Bar {
> constructor() {
> this.b = 'asf';
> }
> }
> const R3 = multiple(Foo, Bar);
> const r3 = new R3();
> console.log(r3.hasOwnProperty);
> console.log('f', r3.f);
> console.log('b', r3.b);
> console.log('-------------------------------------');
> class Baz {
> constructor() {
> this.z = 1;
> }
> }
> const R4 = multiple(Foo, Bar, Baz);
> const r4 = new R4();
> console.log(r4.hasOwnProperty);
> console.log('f', r4.f);
> console.log('b', r4.b);
> console.log('z', r4.z);
> class One {
> constructor(arg) {
> this.one = 1;
> console.log('One constructor');
> // this.one = arg
> }
> foo() {
> console.log('foo', this.one);
> }
> setVar() {
> this.var = 'bright';
> }
> }
> class Two {
> constructor(arg) {
> this.two = 2;
> console.log('Two constructor');
> // this.two = arg
> }
> bar() {
> console.log('bar', this.two);
> }
> readVar() {
> console.log(this.var); // should be "bright"
> }
> }
> class Three extends Two {
> constructor(arg1, arg2) {
> super(arg1);
> this.three = 3;
> console.log('Three constructor');
> // this.three = arg2
> }
> baz() {
> console.log(' - baz: call super.bar');
> super.bar();
> console.log('baz', this.three, this.two);
> }
> }
> class FooBar extends multiple(Three, One) {
> constructor(...args) {
> super();
> // call each constructor. We can pas specific args to each
> constructor if we like.
> //
> // XXX The following is not allowed with ES6 classes,
> class constructors are not callable. :[ How to solve?
> // One.call(this, ...args)
> // Three.call(this, ...args)
> //
> // XXX Solved with the callSuperConstructor helper.
> // ;(this as any).callSuperConstructor(One, args[0])
> // ;(this as any).callSuperConstructor(Three, args[1], args[2])
> }
> yeah() {
> console.log(' -- yeah', this.one, this.two, this.three);
> super.baz();
> super.foo();
> }
> }
> let f = new FooBar(1, 2, 3);
> // this shows that the modifications to `this` by each constructor worked:
> console.log(f.one, f.two, f.three); // logs "1 2 3"
> console.log(' ---- call methods:');
> // all methods work:
> f.foo();
> f.bar();
> f.baz();
> f.yeah();
> f.setVar();
> f.readVar();
> console.log(' --------------------------- ');
> class Lorem {
> constructor() {
> this.lo = 'rem';
> }
> }
> // class Ipsum extends multiple(Lorem, FooBar) {
> class Ipsum extends multiple(FooBar, Lorem) {
> constructor() {
> super(...arguments);
> // THIS LINE TRIGGER PROXY INFINITE RECURSION
> this.ip = 'sum';
> }
> test() {
> console.log(' -- Ipsum: call super.bar()');
> console.log(super.foo);
> console.log(super.bar);
> console.log(super.baz);
> console.log(super.yeah);
> console.log(super.setVar);
> console.log(super.readVar);
> super.bar();
> console.log(this.lo, this.ip);
> }
> }
> const i = new Ipsum();
> i.foo();
> i.bar();
> i.baz();
> i.yeah();
> i.setVar();
> i.readVar();
> i.test();
> function createOnChangeProxy(onChange, target) {
> return new Proxy(target, {
> get(target, property) {
> const item = target[property];
> if (isMutableObject(item))
> return createOnChangeProxy(onChange, item);
> return item;
> },
> set(target, property, newValue) {
> ;
> target[property] = newValue;
> onChange();
> return true;
> },
> });
> }
> function isMutableObject(maybe) {
> if (maybe === null)
> return false;
> if (maybe instanceof Date)
> return false;
> // treat Uint8Arrays as immutable, even though they
> technically aren't, because we use them a lot and we treat them as
> immutable
> if (maybe instanceof Uint8Array)
> return false;
> // TODO: filter out any other special cases we can find, where
> something identifies as an `object` but is effectively immutable
> return typeof maybe === 'object';
> }
> let changeCount = 0;
> const o = createOnChangeProxy(() => changeCount++, {});
> o.foo = 1;
> o.bar = 2;
> o.baz = {};
> o.baz.lorem = true;
> o.baz.yeee = {};
> o.baz.yeee.wooo = 12;
> console.log(changeCount === 6);
> }
> test();
> ```
More information about the es-discuss
mailing list