Search code examples
javascriptnew-operator

Gain access to the original `this` passed to a constructor before the constructor returns a different instance object


I have to use a certain third-party library that works in the following way:

const Thing = function () {
  if (!new.target) return new Thing() // Note: This previously wasn't there

  const privateData = this

  privateData.somethingPrivate = 123

  return {
    someFunction () {
      return privateData.somethingPrivate
    }
  }
}

module.exports = Thing

As you can see, it wants to be called with new, but it then uses the auto-created this instance object as a container for private data and instead returns a fresh object (that doesn't even have the right prototype) with public functions.

So, we can do this...

const Thing = require('thing')

const thing = new Thing()

console.log(thing.someFunction()) // 123

...but we cannot do this:

thing.somethingPrivate = 456 // Does nothing

For reasons I can't explain here right now, I have to be able to access private data like somethingPrivate from the outside. Also, forking the library is not an option at the moment. I know this sounds like an XY problem but trust me, it's not. I know I'm trying to do something that I'm not "supposed" to do (and I know I have to be careful with library updates) but I need a creative solution here. (And no, this is not for nefarious purposes.)

Now, what would be needed to access somethingPrivate is gaining access to the original this that the constructor Thing is getting called with. This object is created by JS itself through the new operator. (Normally, this object is also returned from the new operator, unless the constructor returns another object, which is what happens here.)

Originally, the line if (!new.target) return new Thing() didn't exist in the library. I had a solution that looked like this (doing something similar to what the new operator would be doing, but keeping a reference to the original this):

const Thing = require('thing')

const thingPrivateData = Object.create(Thing) // Manually create instance, keep reference
const thing = Thing.call(privateData) // Call constructor on the instance
thing._private = thingPrivateData // Assign original "private" instance to a public property

Afterwards, I was able to access somethingPrivate like this:

thing._private.privateData = 456 // Works!

However, in a recent update, the library added the line if (!new.target) return new Thing() which rendered my solution useless, because my manual invokation of Thing would not set the new.target value and the library would then call itself again with new, so the this that I passed in manually would not be used and instead a new instance object would be created.

I researched into Reflect.construct and hoped I could do something like this:

const Thing = require('thing')

const thing = Reflect.construct(function () {
  const obj = Thing.call(this) // This doesn't work because yet again new.target is unset
  obj._private = this
  return obj
}, [], Thing)

But of course it doesn't work because even though new.target is set in my (anonymous) function, it is unset again when I call into Thing, because that call again is without new/Reflect.construct.

Am I out of luck or is there some sort of creative way to get access to that original instance passed to as this to the constructor, and/or to set new.target in an invocation while also passing a custom this (and not a prototype from which one is auto-created)?


Solution

  • I have a solution for my specific problem but it feels even dirtier than I want. I would still be interested in other answers!

    My current solution is as follows:

    let privateData
    const Hook = function () {} // Without a function we get "#<Object> is not a constructor"
    Hook.prototype = new Proxy(Thing.prototype, {
      set: function (target, prop, value) {
        privateData = target
        return Reflect.set(target, prop, value)
      }
    })
    
    const thing = Reflect.construct(Thing, [], Hook)
    thing._private = privateData
    

    Afterwards, thing._private.somethingPrivate works as it did with my previous solution.

    This works because apparently the proxy trap I set on the prototype is carried over to the object instance somehow (although I'm not 100% sure why it works this way), and when the constructor writes to the private data fields, it triggers my trap and I can save a reference to the internal object.

    Note that this only works because the constructor in this library is already writing to the this object. If it would not (and only methods would do that when called later) then I would need a more complicated solution where the proxy would actually trap both get and set and redirect all property reads/writes to another object instead. This way we would not have access to the actual this but we would have something almost equivalent - we would control what data the library reads from the object and we would see what it writes.