7

Suppose I have a function Foo, and I want objects constructed from it to have a bar property:

function Foo() {}
Foo.prototype.bar = 'baz'
console.log('new Foo().bar: ' + new Foo().bar)

new Foo().bar: baz

Now suppose I bind Foo in some way. Bound functions can still be used in constructor calls, and the bound this is ignored:

const Bound = Foo.bind(42)
console.log('new Bound().bar: ' + new Bound().bar)

new Bound().bar: baz

Proxies are supposed to be general and transparent. However...

const PFoo = new Proxy(Foo, { })
console.log('new PFoo().bar: ' + new PFoo().bar)

const PBound = new Proxy(Bound, { })
console.log('new PBound().bar: ' + new PBound().bar)

new PFoo().bar: baz
new PBound().bar: undefined

I would expect the second proxy to behave exactly as Bound, since I am using an empty handler. In other words, I would expect the last output to be baz.

Why is it not the case?

(complete snippet follows)

function Foo() {}
Foo.prototype.bar = 'baz'
console.log('new Foo().bar: ' + new Foo().bar)

const Bound = Foo.bind(42)
console.log('new Bound().bar: ' + new Bound().bar)

const PFoo = new Proxy(Foo, { })
console.log('new PFoo().bar: ' + new PFoo().bar)

const PBound = new Proxy(Bound, { })
console.log('new PBound().bar: ' + new PBound().bar)
Forty3
  • 2,199
  • 1
  • 14
  • 19
effeffe
  • 2,821
  • 3
  • 21
  • 44
  • (OT: is it possible to interleave text and code in executable snippets?) – effeffe Dec 19 '17 at 18:02
  • I don't think so, but you can use comments. (Or you can post multiple snippets) – Sébastien Dec 19 '17 at 18:10
  • 1
    Bound functions don't have their own `.prototype` object but they appear to search for it from their origin when `new` is used, while proxied functions only provide it if the original has it directly. So there must be some difference in how the `.prototype` is requested. The spec will have the answer if anyone cares to check. –  Dec 19 '17 at 18:26
  • @rockstar I was aware of the missing `prototype` from bound function, but never heard/thought of the proxied function only providing it if the proxied function has it directly. Will check, thanks for the input. – effeffe Dec 19 '17 at 18:31
  • 1
    Putting `console.log("new.target:", new.target.name);` in the `Foo` constructor yields an interesting result. –  Dec 19 '17 at 18:49
  • 1
    @rockstar Focusing on `new.target` was correct, thanks. I think I got it, I will write an answer with some details from the spec. Not sure this is actually a feature though... – effeffe Dec 20 '17 at 15:04

1 Answers1

3

tl;dr

When using new F, new.target is set to F unless F is a bound function, in that case new.target becomes the original function. This does not happen with proxies.

Long answer

Ok, I think I got it. This comment was a good starting point. Key ingredients:

  • new.target
  • [[Prototype]] internal slot
  • constructor functions prototype property

Note: in constructor calls, the new object's prototype is set to new.target.prototype. This is step 5 from this specification.

Starting point: when doing new F(), new.target is initially set to F (follow links). However, this can change during the process of construction...

new Foo()

Nothing weird here, new.target is Foo and the newly created object prototype is Foo.prototype.

new Bound()

This is interesting. At the beginning, new.target is Bound. However, step 5 of bound functions [[Construct]] internal method does the following: if new.target is set to the bound function, then it is changed to the target function, which is Foo. Thus, Foo.prototype is used again.

new PFoo()

new.target is always PFoo, but it is a proxy for Foo, so when PFoo.prototype is requested Foo.prototype is given, again.

new PBound()

new.target is set to PBound. This time, when the [[Construct]] internal method of the bound function is called, new.target is not equal to the bound function, so it is not changed, and we end up using PBound.prototype, which is forwarding to Bound.prototype. Indeed...

function Foo() { }
Foo.prototype.iAm = 'Foo'
const Bound = Foo.bind(42)
Bound.prototype = {iAm: 'Bound'}
const Proxied = new Proxy(Bound, { })
console.log(new Proxied().iAm)

Personal opinion: I understand that, when constructing a bound function, new.target is changed so that everything works as expected. Similarly, I would expect the construction of proxy objects to set new.target to the proxied function before going on. This is a naive thought, and maybe there are corner cases I am not considering.

Community
  • 1
  • 1
effeffe
  • 2,821
  • 3
  • 21
  • 44
  • 1
    Nice. I do wonder why they made that decision, or if it was an oversight. The fact that they do let you call `new` on a Proxy instance sure would seem to suggest that they intend for the wrapped function to be used as expected. Maybe the esdiscuss list has some info on this. –  Dec 20 '17 at 16:09
  • 2
    @rockstar I couldn't find anything... guess I will create an issue in the working group's GitHub repo, it looks like a good place for it. – effeffe Dec 20 '17 at 16:15
  • 1
    @rockstar done https://github.com/tc39/ecma262/issues/1052 (sorry for notification, thought you may be interested in it) – effeffe Dec 20 '17 at 16:40
  • Awesome. I was just about to leave a comment asking for a link. :-) –  Dec 20 '17 at 16:41