55

I tried to extend Proxy, like so:

class ObservableObject extends Proxy {}

I used Babel to transpile it to ES5, and I got this error in the browser:

app.js:15 Uncaught TypeError: Object prototype may only be an Object or null: undefined

I looked at the line of code it pointed to. Here's that portion of the code with arrows pointing to the offending line of code:

var ObservableObject = exports.ObservableObject = function (_Proxy) {
    _inherits(ObservableObject, _Proxy); // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

    function ObservableObject() {
        _classCallCheck(this, ObservableObject);

        return _possibleConstructorReturn(this, Object.getPrototypeOf(ObservableObject).apply(this, arguments));
    }

    return ObservableObject;
}(Proxy);

Does anyone know why I might be getting this error? Is this a bug in Babel? What is supposed to happen when you try to extend Proxy?

user3840170
  • 26,597
  • 4
  • 30
  • 62
John L.
  • 1,903
  • 2
  • 13
  • 15
  • 1
    In Firefox, this throws an `undefined is not an object or null` and sometines a `ReferenceError: can't access lexical declaration [class name] before initialization`. – D. Pardal Jul 10 '19 at 17:05
  • Interestingly, in the description of the handlers get-trap on [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#parameters) the parameter `receiver` is described as "either the proxy or an object that inherits from the proxy." This sounds as if `Proxy` is supposed to be extendable. I thought I had seen a similar comment in the specs but can't find it anymore. Seems to contradict the observations... – Sebastian Nov 14 '22 at 15:43

7 Answers7

104

Well, I had forgotten about this question, but someone recently upvoted it. Even though you technically can't extend a proxy, there is a way to force a class to instantiate as a proxy and force all its subclasses to instantiate as proxies with the same property descriptor functions (I've only tested this in Chrome):

class ExtendableProxy {
    constructor() {
        return new Proxy(this, {
            set: (object, key, value, proxy) => {
                object[key] = value;
                console.log('PROXY SET');
                return true;
            }
        });
    }
}

class ChildProxyClass extends ExtendableProxy {}

let myProxy = new ChildProxyClass();

// Should set myProxy.a to 3 and print 'PROXY SET' to the console:
myProxy.a = 3;
John L.
  • 1,903
  • 2
  • 13
  • 15
  • I've tried this and it (currently) works. However I don't understand why, can you explain how/why this works? This seems like cleverly making use of a bug, but if that is the case I suspect this won't continue working indefinitely... Please expand on this! – Wim Barelds Jan 10 '17 at 17:58
  • @Wim Barelds I'm not an expert on ecma standards, but here's a guess: If you don't define a constructor for the child class, the constructor becomes `constructor() { super(); }`. `super()` is a call to the constructor of the parent class. If a constructor returns something, then `new ()` returns the thing that `()` returns. This behavior has been around for a while in the old-fashioned javascript constructors. So, when `super` is called, the object constructed becomes a Proxy. – John L. Jan 12 '17 at 05:49
  • 6
    @WimBarelds The ECMAScript 6/7 specification (I forget) states that, in a class hierarchy, the base class is responsible for allocating the object. So, in a hierarchy, the class that doesn't inherit (except implicitly from Object) is responsible for allocation. In this situation, ExtendableProxy is allocating new instances as instances of Proxy. So this works. And I don't think it's a bug. – Ethan Reesor Feb 16 '17 at 08:10
  • 1
    @WimBarelds This is completely intentional, in fact the answer here is one of the big reasons why, basically allowing arbitrary objects allows you do things like saving objects in caches, creating `Proxy` objects to intercept stuff, even things like subclass factories, etc. – Jamesernator May 25 '17 at 00:45
  • 4
    While in pure JS this works totally fine, TypeScript does not support it, and has some good argumentation for this decision: https://github.com/Microsoft/TypeScript/issues/11588#issuecomment-257437554 In TypeScript you should use functions, instead of es6 classes, in order to return new Proxy and to keep correct return type declaration. – Nurbol Alpysbayev Apr 05 '18 at 14:53
  • 4
    In 2014, Brendan Eich (the creator of JavaScript) confirmed explicitly that returning a value from a constructor NOT a bug: "We did discuss return expr; in constructors at the TC39 meeting, and my sense is that we want to keep that option. Classes as sugar (mostly), and all that." Some members of TC39 (who curate JavaScript) do, however, consider the technique an anit-pattern. As of 2019, @John L's ExtendableProxy example works and appears to be a valid approach to creating a Proxy subclass. (But if observing changes to a key/value store is the goal, extending Map might be less controversial.) – colin moock Aug 01 '19 at 03:57
  • 2
    Link to the Brendan Eich source: https://esdiscuss.org/topic/should-the-default-constructor-return-the-return-value-of-super – colin moock Aug 01 '19 at 03:58
  • Excellent ! We may have have it fully reusable with this form `class ExtendableProxy {constructor(parameters) {return new Proxy(this,parameters);}}` – yorg Jan 21 '22 at 21:01
  • [“What values can a constructor return to avoid returning `this`?”](https://stackoverflow.com/a/1978474/4510033) – Константин Ван Apr 19 '22 at 11:31
  • @JohnL. I like the approach, but honestly I don't understand why you are using `this` as proxy target in the `ExtendableProxy`. I would have expected `class ExtendableProxy{constructor(...args){return new Proxy(...args)}}`. This allows you to pass the same arguments to `new ExtendableProxy()` as you pass to `new Proxy()` and it would still be extendable. I've tried it and it works in principle. However, from within the trap functions I managed to only access private members of the Proxy subclass, but no public members (as those are trapped again)... – Sebastian Nov 17 '22 at 08:54
39

No, an ES2015 class cannot extend Proxy1.

Proxy objects have very atypical semantics and are considered "exotic objects" in ES2015, meaning that they do "not have the default behaviour for one or more of the essential internal methods that must be supported by all objects". They do not have any prototype, which is where you'd normally get most of the behaviour for a type you're extending. From section 26.2.2: "Properties of the Proxy Constructor" in the specification:

The Proxy constructor does not have a prototype property because proxy exotic objects do not have a [[Prototype]] internal slot that requires initialization.

This is not a limitation of Babel. If you attempt to extend Proxy in Chrome, where it and the class syntax are both natively supported, you'll still get a similar error:

Uncaught TypeError: Class extends value does not have valid prototype property undefined

1 "No" is the practical answer. However, Alexander O'Mara pointed out that if you assign a value to Proxy.prototype (gross!), it does become possible to extend, at least in some browsers. We experimented with this a little. Due to the behaviour of exotic Proxy instances this can't be used to accomplish much more than you could do with a function wrapping the constructor, and some behaviour does not appear to be consistent between browsers (I'm not sure what the specification expects if you do this). Please don't attempt anything like this in serious code.

Community
  • 1
  • 1
Jeremy
  • 1
  • 85
  • 340
  • 366
  • On a side note, patching in a `prototype` property will bypass this `TypeError` in native, non-babel implementations, but the way `Proxy` is apparently implemented overwrites any child classes leaving you with just a convoluted alias for `Proxy`. – Alexander O'Mara Jun 09 '16 at 00:51
  • @AlexanderO'Mara That is very interesting. I'm not familiar enough with the internal semantics to understand if this is clearly specified or an implementation detail. I set `.prototype = null` and tested this a bit. Your subclass constructor can take different arguments, and if it returns an Object that Object will correctly be produced instead of a Proxy instance, so the subclass constructor is being used properly. I think we may successfully be creating a subclass of Proxy, but there's no way for the subclass to actually modify the behaviour of its instances due to the exotic behviours. – Jeremy Jun 09 '16 at 03:09
  • I *think* this is related to the way a JavaScript constructor can return an object that is different from the implicit `this`. If a parent constructor does this, then that object replaces `this` when the subclass calls super and inheritance is basically thrown out. So I suspect that is what Proxy is basically doing under the hood. – Alexander O'Mara Jun 09 '16 at 03:14
  • I wasn't sure about the behaviour of subclassing other non-`this`-returning constructors, but a quick test verifies that it matches the behaviour I observed with a Proxy subclass. (edit: Ah, you commented as much while I was testing this.) So I think you *did* successfully create a Proxy subclass, it's just that that's probably not a useful thing to do because all you can accomplish is a modified constructor, and you could just use a wrapper function to accomplish the same thing. – Jeremy Jun 09 '16 at 03:15
  • In my testing, it did not seem successful because `obj instanceof ObservableObject` was false. It appears the `Proxy` constructor returns an object that break the inheritance chain. I'm not really sure how a subclassed Proxy would behave though. – Alexander O'Mara Jun 09 '16 at 03:17
  • `Proxy.prototype = null; class P extends Proxy { constructor() { super(window, {}); this.x = 'hello'; } }; new P; console.log(window.x);` is a neat example. – Jeremy Jun 09 '16 at 03:18
  • Here's a way to have a Proxy subclass instance for which `instanceof` works -- by using Proxy behaviour! :P `Proxy.prototype = null; class P extends Proxy { constructor() { super(window, { getPrototypeOf() { return P.prototype } }); } }; (new P) instanceof P === true;` – Jeremy Jun 09 '16 at 03:19
  • Funky, that last example behaves differently between V8 (Chrome) and Spidermonkey (Firefox). – Alexander O'Mara Jun 09 '16 at 03:21
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/114172/discussion-between-jeremy-banks-and-alexander-omara). – Jeremy Jun 09 '16 at 03:22
  • @JeremyBanks, Weirdly your code at https://stackoverflow.com/questions/37714787/can-i-extend-proxy-with-an-es2015-class#comment62905686_37714855 doesn't work in Chrome if I remove `Proxy.prototype = null; `? Why? – Pacerier Nov 23 '17 at 09:10
  • @JeremyBanks, It's not "gross". Subclassing proxy is the only way to catch property access for browsers that don't support noSuchMethod. http://stackoverflow.com/questions/2666602 – Pacerier Nov 23 '17 at 09:10
  • In Chrome somehow you manage to "subclass" a proxy yet the handler gets ignored. `alert` doesnt fire in my test code: `let first_A = Array; Proxy.prototype=null, class Array extends Proxy{ constructor(...a){ super(first_A(...a), {get:(first_t, k)=>alert(k)}); } }; new Array(10,20,30).length` – Pacerier Nov 23 '17 at 09:19
  • Interestingly, in the description of the handlers get-trap on [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#parameters) the parameter `receiver` is described as "either the proxy or an object that inherits from the proxy." This sounds as if `Proxy` is supposed to be extendable. I thought I had seen a similar comment in the specs but can't find it anymore. Changing the prototype of `Proxy` doesn't look like the intended way, however... – Sebastian Nov 14 '22 at 15:41
16

From @John L. self response:
Inside constructor we can use Proxy to wrap the newly created instance. No need to extend Proxy.

Example, provide an observed point from an existing Point class:

class Point {

    constructor(x, y) {
        this.x = x
        this.y = y
    }

    get length() {
        let { x, y } = this
        return Math.sqrt(x * x + y * y)
    }

}

class ObservedPoint extends Point {

    constructor(x, y) {

        super(x, y)

        return new Proxy(this, {
            set(object, key, value, proxy) {
                if (object[key] === value)
                    return
                console.log('Point is modified')
                object[key] = value
            }
        })
    }
}

test:

p = new ObservedPoint(3, 4)

console.log(p instanceof Point) // true
console.log(p instanceof ObservedPoint) // true

console.log(p.length) // 5

p.x = 10 // "Point is modified"

console.log(p.length) // 5

p.x = 10 // nothing (skip)
Joseph Merdrignac
  • 3,510
  • 2
  • 19
  • 16
  • This is good, works on web, but in nodejs does not. super does not give the right properties to the proxy and does not the job (just node) – sdykae Aug 14 '21 at 04:27
3
class c1 {
    constructor(){
        this.__proto__  = new Proxy({}, {
            set: function (t, k, v) {
                t[k] = v;
                console.log(t, k, v);
            }
        });
    }
}

d = new c1(); d.a = 123;

user3027221
  • 636
  • 1
  • 4
  • 4
0

Babel doesn't support Proxy, simply because it can't. So until browsers add support, it doesn't exist.

From the Babel docs: "Unsupported feature Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled"

omerts
  • 8,485
  • 2
  • 32
  • 39
  • You can get pretty close... https://github.com/GoogleChrome/proxy-polyfill – marksyzm Dec 21 '18 at 11:05
  • Only gotcha: "The polyfill supports just a limited number of proxy 'traps'. It also works by calling seal on the object passed to Proxy. This means that the properties you want to proxy must be known at creation time." – marksyzm Dec 21 '18 at 11:05
0
class A { }

class MyProxy {
  constructor(value, handler){
    this.__proto__.__proto__  = new Proxy(value, handler);
  }
}


let p = new MyProxy(new A(), {
  set: (target, prop, value) => {
    target[prop] = value;
    return true;
  },
  get: (target, prop) => {
    return target[prop];
  }
});

console.log("p instanceof MyProxy", p instanceof MyProxy); // true
console.log("p instanceof A", p instanceof A); // true

p is kind of MyProxy and it was extended by A of class simultaneously. A isn't original prototype , it was proxied, sort of.

  • in Firefox this only works for the first instance created. after that, I get the error `can't set prototype of this object` – seanlinsley Dec 13 '19 at 20:51
0

The code example below shows how to construct a Proxy hierarchy that mimics class and subclass. It accomplishes this by wrapping a standard object with multiple Proxy objects and judicious use of the handler get option.

Proxy's handler is a form of the Mix-in Pattern. We can simulate subclassing using the mix-in pattern.

The code contains two types of Proxy "classes": EmitterBase and EmitterNet. It uses those to collect statistics specific to a vanilla EventEmitter or to one of its subclasses Net. EmitterNet does not duplicate features of EmitterBase, but instead reuses Base by wrapping around it. Note, in the example, we wrap http.Server: a subclass Net and a subsubclass of EventEmitter.

The handler passed to Proxy implements all of the behavior for a subclassed Proxy. Multiple handler versions implement subclasses. For example, in EmitterBase we collect statistics on calls to on and emit (count the number of calls). EmitterBase also implements phantom members and methods to track and report on those counts. This is the equivalent of a base class with those methods and members. See the handler in wrapEmitterBase.

Next, we create a Proxy "subclass" using another handler (see wrapEmitterNet) which implements two new phantom members for counting Net specific calls (listen and close). It also implements a method stats() that overrides a method from the base class as well as calling the overridden method.

The Proxy standard gives us enough features to implement Proxy subclassing without resorting to class wrappers and messing with this.

import * as util from 'node:util';
import http from 'node:http';
import { EventEmitter } from 'node:events';

async function DemoProxyHierarchy()
{
  const greeter = wrapEmitterBase(new EventEmitter());

  greeter.on("hello", (person) => console.log((`Hello, ${person}!`)));
  greeter.emit("hello", "World");
  greeter.emit("hello", "Benjamin");

  console.log(`on   calls: ${greeter.countOn}`);
  console.log(`emit calls: ${greeter.countEmit}`);
  console.log(`statistics: ${JSON.stringify(greeter.stats())}`);

  const stats = new Promise((Resolve, reject) => {
    let steps = 0;
    const server = http.createServer((req, res) => { res.end() });
    const netWrapper = wrapEmitterNet(server) as any;
    const done = () => {
      if (++steps > 2) {
        console.log(`\non   calls: ${netWrapper.countOn}`);
        console.log(`emit calls: ${netWrapper.countEmit}`);
        netWrapper.close(() => Resolve(netWrapper.stats()));
      }
    };

    netWrapper.listen(8080, done);
    http.get('http://localhost:8080', done);
    http.get('http://localhost:8080', done);
  });

  return stats.then(s => console.log(`net  stats: ${JSON.stringify(s)}`));
}

function wrapEmitterBase(ee: EventEmitter)
{
  const stats = { on: 0, emit: 0 };
  const handler = {
    get: (target, key) => {
      switch (key) {
        case "countOn":   return stats.on;
        case "countEmit": return stats.emit;
        case "stats":     return () => ({ ...stats });
        case "on":        { stats.on++;   break; }
        case "emit":      { stats.emit++; break; }
      }

      return target[key];
    },
  }

  return new Proxy(ee, handler);
}

function wrapEmitterNet(ee: EventEmitter)
{
  const stats = { listen: 0, close: 0 };
  const handler = {
    get: (target, key) => {
      switch (key) {
        case "stats":  {
          return () => ({ ...target[key](), ...stats });
        }
        case "listen": { stats.listen++; break; }
        case "close":  { stats.close++;  break; }
      }

      return target[key];
    },
  };

  return new Proxy(wrapEmitterBase(ee), handler);
}

// IIFE
(()=> { await DemoProxyHierarchy() })();

/* Output:

Hello, World!
Hello, Benjamin!
on   calls: 1
emit calls: 2
statistics: {"on":1,"emit":2}

on   calls: 1
emit calls: 5
net  stats: {"on":2,"emit":6,"listen":1,"close":1}
*/
Andrew Philips
  • 1,950
  • 18
  • 23