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)?
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.