Array.isArray(new Proxy([], {})) should be false (Bug 1096753)

# Axel Rauschmayer (9 years ago)

The subject is a SpiderMonkey bug.

Is that really desirable? Doesn’t it invalidate the Proxy’s role as an interceptor?

# Tom Van Cutsem (9 years ago)

I agree with your sentiment. I have previously advocated that Array.isArray should be transparent for proxies. My harmony-reflect shim explicitly differs from the spec on this point because people using the shim spontaneously reported this as the expected behaviour and thought it was a bug that Array.isArray didn't work transparently on proxies.

As far as I can remember, the argument against making Array.isArray transparent is that it's ad hoc and doesn't generalize to other types / type tests. My opinion is that array testing is fundamental to core JS and is worth the exception.

# David Bruant (9 years ago)

Le 12/11/2014 17:23, Tom Van Cutsem a écrit :

I agree with your sentiment. I have previously advocated that Array.isArray should be transparent for proxies. My harmony-reflect shim explicitly differs from the spec on this point because people using the shim spontaneously reported this as the expected behaviour and thought it was a bug that Array.isArray didn't work transparently on proxies.

For reference tvcutsem/harmony-reflect#13

As far as I can remember, the argument against making Array.isArray transparent is that it's ad hoc and doesn't generalize to other types / type tests. My opinion is that array testing is fundamental to core JS and is worth the exception.

Agreed. Author usability should trump language purity.

# Rick Waldron (9 years ago)

On Wed, Nov 12, 2014 at 11:23 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

My opinion is that array testing is fundamental to core JS and is worth the exception.

I agree and I want to know if you think this is worth revisiting once more? Next meeting's agenda?

# Tom Van Cutsem (9 years ago)

2014-11-12 19:53 GMT+01:00 Rick Waldron <waldron.rick at gmail.com>:

I agree and I want to know if you think this is worth revisiting once more? Next meeting's agenda?

I don't think we've ever discussed this issue before during a TC39 meeting (but I didn't attend quite a few, so I may be wrong). Adding it to the agenda for next meeting seems good to me.

, Tom

# Kevin Smith (9 years ago)

In general, it appears that SM unwraps proxies so that internal slot access is transparently forwarded. I don't see any such unwrapping in the ES spec though. I assume that the spec is correct?

# Rick Waldron (9 years ago)

On Wed, Nov 12, 2014 at 2:12 PM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

2014-11-12 19:53 GMT+01:00 Rick Waldron <waldron.rick at gmail.com>:

I agree and I want to know if you think this is worth revisiting once more? Next meeting's agenda?

I don't think we've ever discussed this issue before during a TC39 meeting (but I didn't attend quite a few, so I may be wrong). Adding it to the agenda for next meeting seems good to me.

Done tc39/agendas/blob/master/2014/11.md

# Jeremy Martin (9 years ago)

My opinion is that array testing is fundamental to core JS and is worth the exception.

Knowing that Array.isArray() tests fail for proxies, I'd be afraid to ever create a Proxy for an array that I don't control the complete lifecycle of. That seems to critically inhibit the usefulness of Proxies, especially when transparency seems to have been an intended characteristic of them going all the way back to the strawman days.

# Andrea Giammarchi (9 years ago)

If Array.isArray should fail for non "pure" Arrays, can we have a Proxy.isProxy that never fails with proxies ?

At least this would give a better idea on what's eventually going on:

if (Array.isArray(obj) && !Proxy.isProxy(obj)) {
  // ... ok, pure Array, no magic wrappers
}
# Tom Van Cutsem (9 years ago)

2014-11-12 20:37 GMT+01:00 Kevin Smith <zenparsing at gmail.com>:

In general, it appears that SM unwraps proxies so that internal slot access is transparently forwarded. I don't see any such unwrapping in the ES spec though. I assume that the spec is correct?

This auto-unwrapping for internal slots used to be part of an early iteration of direct proxies < harmony:direct_proxies#wrapping_irregular_objects>

but was not carried over into the ES6 spec. IIRC, mainly because just forwarding any internal slot access endangers the encapsulation properties of membrane-like use cases.

# Tom Van Cutsem (9 years ago)

2014-11-12 23:49 GMT+01:00 Andrea Giammarchi <andrea.giammarchi at gmail.com>:

If Array.isArray should fail for non "pure" Arrays, can we have a Proxy.isProxy that never fails with proxies ?

We ruled out Proxy.isProxy very early on in the design. It's antithetical to the desire of keeping proxies transparent. In general, we want to discourage type checks like you just wrote.

If you're getting handed an object you don't trust and need very strong guarantees on its behavior, you'll need to make a copy. This is true regardless of proxies. In your example, even if the array is genuine, there may be some pointer alias to the array that can change the array at a later time.

, Tom

# Andreas Rossberg (9 years ago)

On 12 November 2014 17:23, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

My opinion is that array testing is fundamental to core JS and is worth the exception.

This change would only make sense if we also were to special-case all other places in the spec that currently say "if O is an exotic Array object, ..." to magically handle proxies. Otherwise, all you get is a predicate that gives a misleading result.

However, I don't think we want to special-case all those places. That would put us on a very slippery slope.

# Tom Van Cutsem (9 years ago)

2014-11-12 20:53 GMT+01:00 Rick Waldron <waldron.rick at gmail.com>:

Done tc39/agendas/blob/master/2014/11.md

Thanks. A related point: my harmony-reflect shim also patches Array.prototype.concat such that it recognizes proxies-for-arrays and will splice their elements (rather than adding the proxy as a single element to the target array).

In ES6, this may not be necessary since a proxy could implement @@isConcatSpreadable (see < people.mozilla.org/~jorendorff/es6-draft.html#sec-array.prototype.concat>

step 9.b)

However, looking at IsConcatSpreadable < people.mozilla.org/~jorendorff/es6-draft.html#sec-isconcatspreadable>

step 5, you'll notice it explicitly tests for Arrays. If we decide that Array.isArray should return true for proxies-for-arrays, then I think we should relax this step 5 to also return true for proxies-for-arrays, on the grounds of consistency and principle-of-least-surprise.

# Tom Van Cutsem (9 years ago)

2014-11-13 8:18 GMT+01:00 Andreas Rossberg <rossberg at google.com>:

On 12 November 2014 17:23, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

My opinion is that array testing is fundamental to core JS and is worth the exception.

This change would only make sense if we also were to special-case all other places in the spec that currently say "if O is an exotic Array object, ..." to magically handle proxies. Otherwise, all you get is a predicate that gives a misleading result.

Yes, good point. See my just-recent post on Array.prototype.concat.

However, I don't think we want to special-case all those places. That would put us on a very slippery slope.

A cursory look at the spec reveals the following built-ins that test for "exotic Arrays":

  • Array.isArray
  • Array.prototype.concat
  • Array.prototype.filter
  • Array.prototype.map
  • Array.prototype.slice
  • Array.prototype.splice
  • JSON.parse
  • JSON.stringify
  • Object.prototype.toString

filter, map, slice and splice didn't used to test for arrays in ES5. They seem to test for arrays such that the new array they create derives from the same realm. This can probably be made to work with proxies-for-arrays, but I'm not sure it is necessary or desirable.

For JSON.stringify, the question is whether a proxy-for-array should serialize as "[]" vs "{}". Obviously, if Array.isArray(proxyForArray), I would expect it to serialize as "[]".

For toString, proxies could still implement @@toStringTag to be able to intercept that call. At this point, I believe Object.prototype.toString.call(proxyForArray) would return "[object Object]".

# Andrea Giammarchi (9 years ago)

well, Proxy can be a diabolic beast

Object.setPrototypeOf(
  Object.prototype,
  new Proxy(Object.prototype, evilPlan)
)

having no way to understand if an object is a Proxy looks like a footgun to me in the long term, for libraries, and "code alchemists"

You indeed wrote that different Array methods need to know if there's a Proxy in there ... if dev cannot know the same via code they are unable again to subclass properly or replicate native behaviors behind magic internal checks.

If there is a way and I'm missing it, then it's OK

# Andreas Rossberg (9 years ago)

On 13 November 2014 12:25, Andrea Giammarchi <andrea.giammarchi at gmail.com> wrote:

well, Proxy can be a diabolic beast

Object.setPrototypeOf(
  Object.prototype,
  new Proxy(Object.prototype, evilPlan)
)

having no way to understand if an object is a Proxy looks like a footgun to me in the long term, for libraries, and "code alchemists"

Well, the actual diabolic beast and universal foot gun in this example is setPrototypeOf. ;)

# Andrea Giammarchi (9 years ago)

Same would be reassigning any prototype but it was just to make the diabolic point... also yeah, sPO is powerful, that's why we love it!

( it's also way less diabolic than any "proto" string floating around ;) )

# David Bruant (9 years ago)

The best defense is Object.freeze(Object.prototype); No application worth considering needs to arbitrarily modify Object.prototype at an arbitrary point in time (or someone should bring a use case for discussion). It usually shouldn't and even if it does, it should do it at startup and freeze it afterwards.

Le 13/11/2014 12:25, Andrea Giammarchi a écrit :

well, Proxy can be a diabolic beast

Object.setPrototypeOf(
  Object.prototype,
  new Proxy(Object.prototype, evilPlan)
)

having no way to understand if an object is a Proxy looks like a footgun to me in the long term, for libraries, and "code alchemists"

You're giving guns to people and try to evaluate how to defend from them. Consider not letting guns around the rooms ;-)

# Boris Zbarsky (9 years ago)

On 11/13/14, 6:44 AM, Andreas Rossberg wrote:

Well, the actual diabolic beast and universal foot gun in this example is setPrototypeOf. ;)

Note that there is at least some discussion within Mozilla about trying to make the prototype of Object.prototype immutable (such that Object.getPrototypeOf(Object.prototype) is guaranteed to always return the same thing, modulo someone overriding Object.getPrototypeOf), along with a few other things along those lines. See bugzilla.mozilla.org/show_bug.cgi?id=1052139.

Whether this is web-compatible, we'll see.

# Allen Wirfs-Brock (9 years ago)

On Nov 12, 2014, at 12:12 PM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

2014-11-12 19:53 GMT+01:00 Rick Waldron <waldron.rick at gmail.com>: I agree and I want to know if you think this is worth revisiting once more? Next meeting's agenda?

I don't think we've ever discussed this issue before during a TC39 meeting (but I didn't attend quite a few, so I may be wrong). Adding it to the agenda for next meeting seems good to me.

Yes, this has been discussed at TC39 meetings although I’d have to go search notes for a record of it. Regardless, I’ll try to summarize the logic that lead to the current decision.

First Array.isArray was introduced in ES5 to provide a reliable way to detect Array instances that works regardless of what realm created the instance. In ES5, Array.isArray is specified as a test whether the [[Class]] internal property has the the value “Array”. In ES5 the only objects that have that [[Class]] values are objects created use the Array constructor or using an Array literal (or internally instantiated objects created “as if” by new Array). Any such object responds true for Array.isArray even if its [[Prototype]] is set (using proto) to something different than Array.prototype.

All ES5 objects for which Array.isArray answers true have exotic length behavior where adding an index property beyond the current length increases the value of length and deleting training index properties decrease the value of length.

ES6 eliminates the [[Class]] internal properties, but the equivalent is any object that is an “exotic array object’ . Any “exotic array object” is an exotic object whose [[DefineProperty]] internal method maintains the length property invariant described above. ES6 specifies that Array.isArray answers true only for exotic array objects. Given any ES5-level ES code, the ES6 spec. will produce exactly the same result of Array.isArray as was produced by ES5.

In deciding the ES6 behavior for Array.isArray we considered at least four significant issues:

  1. Backwards compatibility with ES5 level code that uses ES5.1. It must produce the same result for all such code. This includes code that does proto hacking.

  2. ES6 subclassing. Given let sa = new (class extends Array {}); what should be the value of Array.isArray(sa) ? This is something we explicitly talked about at TC39 meetings and agreed that the answer should be true (except when…). However, there are really two kinds of subclass to consider. Subclasses of Array that automatically inherit the creation of exotic array object instances and subclasses of Array that explicit over-ride in their constructor to produce some other kind of instance object (an ordinary object or perhaps some other kind of exotic object or a Proxy). Note this is roughly equivalent to ad hoc ES5 subclassing where you might either use proto to change the [[Prototype]] of an array instance to something other than Array.prototype or where you might create an ordinary object (not an Array instance) and set its [[Prototype]] to Array.prototype in order to inherit Arrray.prototype methods. In the TC39 discussions we concluded that the exotic array based isArray definition gives reasonable answers for both ES5 and Es6 subclassing.

  3. Proxies. Should Array.isArray treat proxy instances specially? To answer this question we need to think about what a ES programmer actually thinks Array.isArray means? The meaning that ES5 established is that Array.isArray returning true means that the object observably maintains the array length invariant. There is no general way to determine whether this invariant is true for an arbitrary proxy. Checking if the proxy’s target object is an exotic array object isn’t sufficient to say isArray is true because the defineProperty/get/set/delete traps might not maintain the invariant. Similar, checking that the object is an ordinary object isn’t suffiient for isArray to say false because appropriate the handler may impose the array length invariants upon an ordinary object.

4)Self-hosting Array. An ES6 goal was to allow self-hosting of built-ins using Proxy where necessary to implement self-hosted exotic objects. A implementation could self-host Array by using a Proxy that targeted an ordinary object and a handler that maintained the length invariants. However, if it did this, it would also have to provide an implementation of Array.isArray that was able to recognize those Proxy based self-hosted Array instances. It might by using some sort of private tagging mechanism of Array instances (for example via a WeakSet) that it’s Array.isArray would recognize.

The current ES6 definition still seems like the best definition, if you accept that Array.isArray means "does this object maintain the length invariant?" Tunneling through a Proxy and testing if its target is an exotic array object isn’t an adequate fix, because of the reasons mentioned above.

In the end, this is really about what JS programmers think Array.isArray means and why they would want to call it. One of the main motivating use cases during ES5 development was the JSON behavior of stringifying Array instances differently from non-Array instances. In retrospect, Array.isArray is improbably not the best way to address that use case. If we were doing it over in ES6 I probably would have defined an @@stringifyAsArray property on Array.prototype and made JSON.stringify sensitive to the presence of that property.

What are other reasonable use cases for Array,isArray?

# Allen Wirfs-Brock (9 years ago)

On Nov 12, 2014, at 11:58 AM, Jeremy Martin wrote:

My opinion is that array testing is fundamental to core JS and is worth the exception.

Knowing that Array.isArray() tests fail for proxies, I'd be afraid to ever create a Proxy for an array that I don't control the complete lifecycle of. That seems to critically inhibit the usefulness of Proxies, especially when transparency seems to have been an intended characteristic of them going all the way back to the strawman days.

Proxies are not transparent forwarders! In particular their default handling of the this value on method invokes will break any built-in method that needs to access "internal slots" of an object.

var p = new Proxy(new Array, {});

will give you an object that will fail on serval of the Array.prototype methods.

It is even worse for other built-ins such as Map or the typed array constructors.

# Brendan Eich (9 years ago)

Let's avoid all-or-nothing judgments about Proxies, they are definitely a practical, standardizable thing -- so a work in progress, subject to further improvement.

Proxies are meant to be transparent in general. Exceptions include built-ins with magic internal properties. We haven't added private data (ideally the basis for self-hosting such built-ins) yet, and we'd need extensions to Proxies should we do so. Etc.

That someone might make a proxy-for-array that violates an Array invariant is just part of the bargain: a bug or feature, depending on situation. We still may want Array.isArray on a proxy-for-array to return true, all things considered.

# Kevin Smith (9 years ago)

Proxies are meant to be transparent in general. Exceptions include built-ins with magic internal properties. We haven't added private data (ideally the basis for self-hosting such built-ins) yet, and we'd need extensions to Proxies should we do so. Etc.

We'd need to discuss it at any rate, but I don't see how private data differs from internal slots with respect to Proxies. Perhaps a discussion for another day...

# Tom Van Cutsem (9 years ago)

2014-11-13 20:31 GMT+01:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:

  1. Proxies. Should Array.isArray treat proxy instances specially? To answer this question we need to think about what a ES programmer actually thinks Array.isArray means? The meaning that ES5 established is that Array.isArray returning true means that the object observably maintains the array length invariant.

Is the length invariant really the dominant meaning JS developers attribute to Array.isArray? I think to most developers Array.isArray(obj) returning true means that it's safe to call the array utilities (map, forEach, ...) on obj, not so much that obj.length is special.

My intuition is that Array.isArray is often used to branch based on whether code received just one versus a collection of values. E.g. a function may take a single parameter that can be bound to either a single value or a collection of values, and treat a collection of values differently. In fact, that is essentially what Array.prototype.concat does: if the argument is an array, splice its values, otherwise, don't splice. This has nothing to do with length magic. The same goes for JSON.stringify (serialize as "[]" vs "{}").

Now, if we take the meaning of Array.isArray to be "supports the Array.prototype utility methods", a proxy-for-array may of course expose a totally different API, leading a client that expects to be able to use the Array.prototype methods to fail. But this foregoes the fact that for virtually all practical use cases of proxies, proxy authors will not do this. They want to be able to wrap objects, intercept some things, but mostly faithfully forward those operations to the wrapped target. It would be rare for a proxy to change the API of the thing it wraps. Indeed, the whole point of proxies is to be able to intercept operations without modifying client code.

, Tom

# Allen Wirfs-Brock (9 years ago)

On Nov 13, 2014, at 1:35 PM, Tom Van Cutsem wrote:

2014-11-13 20:31 GMT+01:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:

  1. Proxies. Should Array.isArray treat proxy instances specially? To answer this question we need to think about what a ES programmer actually thinks Array.isArray means? The meaning that ES5 established is that Array.isArray returning true means that the object observably maintains the array length invariant.

Is the length invariant really the dominant meaning JS developers attribute to Array.isArray? I think to most developers Array.isArray(obj) returning true means that it's safe to call the array utilities (map, forEach, ...) on obj, not so much that obj.length is special.

I agree that what JS programs think Array.isArray means is the key issue here. But if "safe to call array utilities" is what it means then the world is really confused. I guess it may come down to what "safe" means in that statement. More than any other built-in class the Array prototype methods are specified to be generic to any object.

All of the Array.prototype methods work just fine with a defined as:

var a={0:"zero", 1:"one", length: 2};

but Array.isArray(a) answers false.

Conversely:

var b=Object.freeze(["zero", "one"]);
b.sort();

is going to throw, even though Array.isArray(b) answers true.

My intuition is that Array.isArray is often used to branch based on whether code received just one versus a collection of values. E.g. a function may take a single parameter that can be bound to either a single value or a collection of values, and treat a collection of values differently. In fact, that is essentially what Array.prototype.concat does: if the argument is an array, splice its values, otherwise, don't splice. This has nothing to do with length magic. The same goes for JSON.stringify (serialize as "[]" vs "{}").

Well, Array.isArray was new in ES5 so it's legacy only goes back that far.

We might redefine Array.isArray to be based upon testing for @@isConcatSpreadable but that potentially would give different results for legacy uses that did proto hacking such as I mentioned in my previous mote.

Now, if we take the meaning of Array.isArray to be "supports the Array.prototype utility methods", a proxy-for-array may of course expose a totally different API, leading a client that expects to be able to use the Array.prototype methods to fail. But this foregoes the fact that for virtually all practical use cases of proxies, proxy authors will not do this. They want to be able to wrap objects, intercept some things, but mostly faithfully forward those operations to the wrapped target. It would be rare for a proxy to change the API of the thing it wraps. Indeed, the whole point of proxies is to be able to intercept operations without modifying client code.

Certainly if you use a proxy to define a virtual object, to self-host, spec-defined exotic objects or to implement DOM objects you just aren't transparently wrapping the target object...

I think at the root of this is that many JS programmer don't really understand what is (or isn't special about Array instances and that Array.isArray may have just added to the confusion.

# Brendan Eich (9 years ago)

Allen Wirfs-Brock wrote:

We might redefine Array.isArray to be based upon testing for @@isConcatSpreadable but that potentially would give different results for legacy uses that did proto hacking such as I mentioned in my previous mote.

How about we turn the @@ property into @@isArrayLike or @@isStronglyArrayLike if you insist.

Now, if we take the meaning of Array.isArray to be "supports the Array.prototype utility methods", a proxy-for-array may of course expose a totally different API, leading a client that expects to be able to use the Array.prototype methods to fail. But this foregoes the fact that for virtually all practical use cases of proxies, proxy authors will not do this. They want to be able to wrap objects, intercept some things, but mostly faithfully forward those operations to the wrapped target. It would be rare for a proxy to change the API of the thing it wraps. Indeed, the whole point of proxies is to be able to intercept operations without modifying client code.

Certainly if you use a proxy to define a virtual object, to self-host, spec-defined exotic objects or to implement DOM objects you just aren't transparently wrapping the target object...

I think at the root of this is that many JS programmer don't really understand what is (or isn't special about Array instances and that Array.isArray may have just added to the confusion.

That may be, but (part of) our job is to help them. So progressively refining the meaning via @@isArrayLike seems winning.

# Andrea Giammarchi (9 years ago)

As quick parenthesis, I do hope the potentially evil code I've written will be possible in the future, as universal solution to undefined is not defined error in development code.

evil is never necessarily evil, going with "prohibitionism" won't make anyone happy.

About Array.isArray though, I believe devs expect the Proxy to be transparent, but again, for development or library sake, I expect developers to be able to tell if there is some cellophane around their objects.

Array.isArray(proxied) whould be true if the proxied object isArray, but please consider give developers a way to understand if there is a proxy around.

It won't be a good looking thing, but as it's needed internally, it might be needed out there too.

# Domenic Denicola (9 years ago)

From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Tom Van Cutsem

Is the length invariant really the dominant meaning JS developers attribute to Array.isArray? I think to most developers Array.isArray(obj) returning true means that it's safe to call the array utilities (map, forEach, ...) on obj, not so much that obj.length is special.

This is really interesting. It does argue for some kind of redefinition of Array.isArray to return "is this an instance of some %ArrayPrototype% in some realm?" That is very close to "does the object have an @@isConcatSpreadable" property, the main difference being that you can "fake" the latter via myObj[Symbol.isConcatSpreadable] = true while still not inheriting from any %ArrayPrototype%.


To me the most interesting question is how to create objects that get JSON-stringified as [], not {}. Some sort of symbol-based mechanism makes sense for that, IMO...

# Michał Wadas (9 years ago)

Hai.

Why not Symbol.isArray?

To me the most interesting question is how to create objects that get

JSON-stringified as [], not {}. Some sort of symbol-based mechanism makes sense for that, IMO...

toJSON method.

JSON.stringify({toJSON: () => [2,3,4,5]})

# Kevin Smith (9 years ago)

This is really interesting. It does argue for some kind of redefinition of Array.isArray to return "is this an instance of some %ArrayPrototype% in some realm?"

Yeah, my guess would be that if (Array.isArray(obj) === true), then the user will infer that one or both of the following is true:

  • It behaves as if it were created with an array literal (including the length invariant and Array.prototype methods).
  • It should be "rendered" (via JSON or console output) as if it were created with an array literal.
# Allen Wirfs-Brock (9 years ago)

On Nov 13, 2014, at 5:02 PM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

We might redefine Array.isArray to be based upon testing for @@isConcatSpreadable but that potentially would give different results for legacy uses that did proto hacking such as I mentioned in my previous mote.

How about we turn the @@ property into @@isArrayLike or @@isStronglyArrayLike if you insist.

This could be done, but that property could pre-exist on Array prototype. If it did, it would change the behavior (probably including the JSON.stringify behavior) of existing code that puts Array.prototype on its [[Prototype]] chain.

The definition of Array.isArray(obj) would be roughly:

  1. Let isArray be ToBoolean(Get(obj, @@isArrayLike));
  2. If isArray is true, then return true;
  3. If obj is an exotic array object, then return true;
  4. return false
# Allen Wirfs-Brock (9 years ago)

On Nov 14, 2014, at 4:53 AM, Domenic Denicola wrote:

From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Tom Van Cutsem

Is the length invariant really the dominant meaning JS developers attribute to Array.isArray? I think to most developers Array.isArray(obj) returning true means that it's safe to call the array utilities (map, forEach, ...) on obj, not so much that obj.length is special.

This is really interesting. It does argue for some kind of redefinition of Array.isArray to return "is this an instance of some %ArrayPrototype% in some realm?" That is very close to "does the object have an @@isConcatSpreadable" property, the main difference being that you can "fake" the latter via myObj[Symbol.isConcatSpreadable] = true while still not inheriting from any %ArrayPrototype%.

What do you mean by "is an instance of some %ArrayPrototype%"? Do you mean that it has some %ArratPrototype% in it's prototype chain? This is actually fairly hard to determine. We look at trying to identify known prototypes independent of realm for an earlier issue and rejected the possibility because it would require additional tragging of such prototypes. Also, it wasn't clear who the concept could be extended to JS defined classes.

# Domenic Denicola (9 years ago)

From: Allen Wirfs-Brock [mailto:allen at wirfs-brock.com]

What do you mean by "is an instance of some %ArrayPrototype%"? Do you mean that it has some %ArratPrototype% in it's prototype chain?

Yeah, more or less. A "realm-independent instanceof." This makes sense also from the historical perspective that Array.isArray was meant to provide a cross-realm alternative to instanceof Array. (I believe that was the case; before my time.)

However as you pointed out in a related conversation this would not be backward-compatible, i.e. it would change the answer for code that does prototype munging.

This is actually fairly hard to determine. We look at trying to identify known prototypes independent of realm for an earlier issue and rejected the possibility because it would require additional tragging of such prototypes. Also, it wasn't clear who the concept could be extended to JS defined classes.

Right, good points. Probably not the way to go, indeed.

# Allen Wirfs-Brock (9 years ago)

On Nov 14, 2014, at 11:41 AM, Domenic Denicola wrote:

From: Allen Wirfs-Brock [mailto:allen at wirfs-brock.com]

What do you mean by "is an instance of some %ArrayPrototype%"? Do you mean that it has some %ArratPrototype% in it's prototype chain?

Yeah, more or less. A "realm-independent instanceof." This makes sense also from the historical perspective that Array.isArray was meant to provide a cross-realm alternative to instanceof Array. (I believe that was the case; before my time.)

No really, it was a way to expose a test of the [[Class]] internal property. That test wasn't dependent upon the [[Prototype]] chain.

# Tom Van Cutsem (9 years ago)

2014-11-14 21:00 GMT+01:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:

On Nov 14, 2014, at 11:41 AM, Domenic Denicola wrote:

Yeah, more or less. A "realm-independent instanceof." This makes sense also from the historical perspective that Array.isArray was meant to provide a cross-realm alternative to instanceof Array. (I believe that was the case; before my time.)

No really, it was a way to expose a test of the [[Class]] internal property. That test wasn't dependent upon the [[Prototype]] chain.

I think what Domenic was saying is that Array.isArray used such a test because instanceof Array didn't work reliably cross-realms. I too vaguely recollect that reliable cross-realm "instanceof Array" testing was a primary motivation for Array.isArray.

# Brendan Eich (9 years ago)

Tom Van Cutsem wrote:

No really, it was a way to expose a test of the [[Class]] internal
property.  That test wasn't dependent upon the [[Prototype]] chain.

I think what Domenic was saying is that Array.isArray used such a test because instanceof Array didn't work reliably cross-realms. I too vaguely recollect that reliable cross-realm "instanceof Array" testing was a primary motivation for Array.isArray.

Yes, that was Crock's stated motivation as I recall. Here's a 2003-era post from Doug:

groups.google.com/forum/#!msg/comp.lang.javascript/XTWYCOwC96I/70rNoQ3L

# Jeremy Martin (9 years ago)

I don't have the data to back this up, but I would argue that the developer community has essentially adopted Array.isArray() as a sane replacement for Object.prototype.toString.call(maybeArray) === '[object Array]'.

I realize this forum doesn't have the luxury of not considering the nuances between a value having the right [[Class] property vs. having Array.prototype in the chain vs. having an exotic length property, etc., but I would at least suggest that the consumers of Array.isArray() are largely just concerned with whether or not they can do everything with a particular value that they could do if it were an actual array, while maintaining equal code semantics.

Allen's previous comments:

Proxies are not transparent forwarders! In particular their default

handling of the this value on method invokes will break any built-in method that needs to access "internal slots" of an object.

...lead me to believe that this isn't the case for proxified Arrays, so I'd have to reverse my earlier position, as Array.isArray(proxifiedArray) evaluating to true just seems likely to break code.

# Tom Van Cutsem (9 years ago)

2014-11-13 22:35 GMT+01:00 Tom Van Cutsem <tomvc.be at gmail.com>:

My intuition is that Array.isArray is often used to branch based on whether code received just one versus a collection of values. E.g. a function may take a single parameter that can be bound to either a single value or a collection of values, and treat a collection of values differently. In fact, that is essentially what Array.prototype.concat does: if the argument is an array, splice its values, otherwise, don't splice. This has nothing to do with length magic. The same goes for JSON.stringify (serialize as "[]" vs "{}").

Amusingly, not only does JSON.stringify(value, replacer, space) branch on the type of its value argument (to serialize as "[]" vs "{}"), it apparently also type-tests its replacer argument, which can either be a function or an array. If it's an array, it serves as a whitelist of the properties that can appear in the output. While it's unlikely anyone would ever want to pass a proxy-for-array as a JSON whitelist, JSON.stringify is an example of the kind of type-testing I had in mind to simply distinguish whether one of its parameters is just one value vs. a collection of values. For such functions, there would be no harm in treating proxies-for-arrays as arrays.

# Tom Van Cutsem (9 years ago)

2014-11-14 21:52 GMT+01:00 Jeremy Martin <jmar777 at gmail.com>:

Allen's previous comments:

Proxies are not transparent forwarders! In particular their default

handling of the this value on method invokes will break any built-in method that needs to access "internal slots" of an object.

...lead me to believe that this isn't the case for proxified Arrays, so I'd have to reverse my earlier position, as Array.isArray(proxifiedArray) evaluating to true just seems likely to break code.

For the particular case of Arrays though, because Array methods were carefully specified to be generic (i.e. to also work on non-array objects), they will work just fine on proxies-for-arrays:

var p = new Proxy([1,2,3] , {} ); var a = p.map(function(x) { return x + 1; }); console.log(a); // [2,3,4]

So, of all the exotic objects in the ES and WebIDL specs, Arrays are probably one of the few exceptions where a Proxy wrapper is transparent by default.

# Brendan Eich (9 years ago)

Tom Van Cutsem wrote:

2014-11-14 21:52 GMT+01:00 Jeremy Martin <jmar777 at gmail.com <mailto:jmar777 at gmail.com>>:

Allen's previous comments:

    Proxies are not transparent forwarders!  In particular their
    default handling of the `this` value on method invokes will
    break any built-in method that needs to access "internal
    slots" of an object.


...lead me to believe that this isn't the case for proxified
Arrays, so I'd have to reverse my earlier position, as
`Array.isArray(proxifiedArray)` evaluating to true just seems
likely to break code.

For the particular case of Arrays though, because Array methods were carefully specified to be generic (i.e. to also work on non-array objects), they will work just fine on proxies-for-arrays:

var p = new Proxy([1,2,3] , {} ); var a = p.map(function(x) { return x + 1; }); console.log(a); // [2,3,4]

So, of all the exotic objects in the ES and WebIDL specs, Arrays are probably one of the few exceptions where a Proxy wrapper is transparent by default.

Right.

There is no all-or-nothing solution. Allen's words applie to exceptions to the rule. Arrays and well-written proxies for them fit in the rule. Jeremy, what do you say?

# Jeremy Martin (9 years ago)

(Mostly) transparent forwarding seems to be one of the more compelling and generally useful characteristics of Proxies. I have to lean heavily on the deeper knowledge of the group here, but if new Proxy([], {}) otherwise behaves like a bonafide array when you treat it like one, then it would be a pity to lose the ability to take advantage of that over Array.isArray() resolving to false. If transparent forwarding is an intended feature of proxied arrays, then I'd argue it's far too common of a test to discount . But again, I can't answer those "ifs". :)

# David Bruant (9 years ago)

Le 13/11/2014 17:29, Boris Zbarsky a écrit :

On 11/13/14, 6:44 AM, Andreas Rossberg wrote:

Well, the actual diabolic beast and universal foot gun in this example is setPrototypeOf. ;)

Note that there is at least some discussion within Mozilla about trying to make the prototype of Object.prototype immutable (such that Object.getPrototypeOf(Object.prototype) is guaranteed to always return the same thing, modulo someone overriding Object.getPrototypeOf), along with a few other things along those lines. See bugzilla.mozilla.org/show_bug.cgi?id=1052139.

This would result in objects which [[Prototype]] cannot be changed but which properties can be changed. This is not possible per ES6 semantics I believe unless the object is a proxy (which setPrototypeOf trap throws unconditionally and forwards the rest to the target). Is it a satisfactory explanation? Should new primitives be added?

Whether this is web-compatible, we'll see.

I guess my above questions can wait the answer to this part.

# Allen Wirfs-Brock (9 years ago)

On Nov 15, 2014, at 1:24 AM, David Bruant wrote:

Le 13/11/2014 17:29, Boris Zbarsky a écrit :

On 11/13/14, 6:44 AM, Andreas Rossberg wrote:

Well, the actual diabolic beast and universal foot gun in this example is setPrototypeOf. ;)

Note that there is at least some discussion within Mozilla about trying to make the prototype of Object.prototype immutable (such that Object.getPrototypeOf(Object.prototype) is guaranteed to always return the same thing, modulo someone overriding Object.getPrototypeOf), along with a few other things along those lines. See bugzilla.mozilla.org/show_bug.cgi?id=1052139. This would result in objects which [[Prototype]] cannot be changed but which properties can be changed. This is not possible per ES6 semantics I believe unless the object is a proxy (which setPrototypeOf trap throws unconditionally and forwards the rest to the target). Is it a satisfactory explanation? Should new primitives be added?

Actually the ES6 semantics does allow for this. The global object would have t be implemented as a new kind of implementation specific exotic object whose [[SetPrototypeOf]] internal method always returns false.

A Proxy would be required to self-host such an object using ES code, but at the implementation level a Proxy would not be required.

# Boris Zbarsky (9 years ago)

On 11/15/14, 11:47 AM, Allen Wirfs-Brock wrote:

Actually the ES6 semantics does allow for this. The global object would have t be implemented as a new kind of implementation specific exotic object whose [[SetPrototypeOf]] internal method always returns false.

That works for the global object (which is an exotic object anyway in browsers, because it has Web IDL branding of various sorts), but not for Object.prototype....

# Brendan Eich (9 years ago)

Boris Zbarsky wrote:

On 11/15/14, 11:47 AM, Allen Wirfs-Brock wrote:

  • hide quoted text -- show quoted text -

Actually the ES6 semantics does allow for this. The global object would have t be implemented as a new kind of implementation specific exotic object whose [[SetPrototypeOf]] internal method always returns false.

That works for the global object (which is an exotic object anyway in browsers, because it has Web IDL branding of various sorts), but not for Object.prototype....

Perhaps a one-off exception?

Are you confident this change is web-compatible?

# Boris Zbarsky (9 years ago)

On 11/16/14, 2:12 AM, Brendan Eich wrote:

Are you confident this change is web-compatible?

No, I said that up-thread already. So there may be nothing to worry about here spec-wise for now.

# Frankie Bagnardi (9 years ago)

Consider when Array.isArray would be used. In my experience, checks to see if something is an array are used for:

  • deciding how to iterate it (for(;;) vs for..in, for example)
  • deciding if the output should be an array or plain object (e.g. lodash)
  • early errors, e.g. runtime typecheck errors

In all of these situations, I don't care if it is an actual per-spec exact array, I care if I can use it like an array. That's how Array.isArray will likely be used, and if it runs into problems, then expect github to be full of 'don't use Array.isArray' patches on many libraries, much like the existing instanceof situation.

# Tom Van Cutsem (9 years ago)

2014-11-17 3:34 GMT+01:00 Frankie Bagnardi <f.bagnardi at gmail.com>:

Consider when Array.isArray would be used. In my experience, checks to see if something is an array are used for:

  • deciding how to iterate it (for(;;) vs for..in, for example)

This is a good one. Here, again, a typical proxy-for-array would work as intended, as the for(;;) loop just queries .length and properties named "0", "1", ... via standard [[Get]] access.

Can someone come up with a convincing example where a proxy-for-array would not work as intended? ("convincing" here means it doesn't involve a proxy that blatantly violates the Array contract on purpose)

# Allen Wirfs-Brock (9 years ago)

On Nov 16, 2014, at 11:08 PM, Tom Van Cutsem wrote:

2014-11-17 3:34 GMT+01:00 Frankie Bagnardi <f.bagnardi at gmail.com>: Consider when Array.isArray would be used. In my experience, checks to see if something is an array are used for:

  • deciding how to iterate it (for(;;) vs for..in, for example)

This is a good one. Here, again, a typical proxy-for-array would work as intended, as the for(;;) loop just queries .length and properties named "0", "1", ... via standard [[Get]] access.

Can someone come up with a convincing example where a proxy-for-array would not work as intended? ("convincing" here means it doesn't involve a proxy that blatantly violates the Array contract on purpose)

Probably not, because the Array.prototype methods are all carefully coded to not have any internal slot or this identify dependencies.

But probing through the proxy as has been proposed is a terrible violation of the MOP API boundary and isn't generalizable to other built-ins that are dependent upon internal state. While a null handler proxy on a direct instance of Array would work under that design, a comparable Map.isMap or Promise.isPromise method would not. Rather than a one-off hack I think we should use a new pattern that is generalizable:

Here is a proposal:

Array.isArray is redefined equivalently to:

Array.isArray = function (obj) {
   let constructor = obj.constructor;
   If (typeof constructor != 'function') return false;
   return !!constructor[Symbol.isArray];
}

and also

Array[Symbol.isArray]] = true;  //note, this is a constructor property, not an Array.prototype property.

we also change Array.prototype.concat to do the above Array.isArray test instead of using @@comcatSpreadable and change JSON.stringify to use this test where it current checks for an exotic array object.

Then we have the following:

assert(Array.isArray( [ ] )); //just like ES5 assert( ! Array.isArray( Object.create(Array.prototype))); //just like ES5 assert (new class extends Array{}); //ES6 Array subclass instances answer true unless the subclass over-rides the static @@isArray property assert(new class extends Array { constructor() {return {}}; }); // even if the subclass instance isn't an exotic array object (this is an improvement over the current ES6 spec. which says that //isArray answers false in this case assert(new Proxy( [ ], { }); //because it's all done with property access, no instance inspection required.

assert( ! new Int32Array(10)); //not array-like, just like ES5

However, I think we should experiment with giving %TypedArrau% is true valued @@isArray property. I'm guessing that this change won't actually break anything.

the above proposal completely decouples Array.isArray from exotic array object checking or any other direct instance inspection. Everything works at the property access level and so is (by default) transparent to proxies and completely controllable at the ES cod level.

# Brendan Eich (9 years ago)

Allen Wirfs-Brock wrote:

Array[Symbol.isArray]] = true;  //note, this is a constructor 
property, not an Array.prototype property.

Minomer, then -- how about Symbol.isArrayClass?

we also change Array.prototype.concat to do the above Array.isArray test instead of using @@comcatSpreadable and change JSON.stringify to use this test where it current checks for an exotic array object.

Then we have the following:

assert(Array.isArray( [ ] )); //just like ES5 assert( ! Array.isArray( Object.create(Array.prototype))); //just like ES5

How so? True:

js> Array.isArray(Array.prototype)

true js> Array.isArray(Object.create(Array.prototype))

false

but constructor isn't shadowed:

js> Array.prototype.constructor

function Array() { [native code] } js> Object.create(Array.prototype).constructor

function Array() { [native code] }

assert (new class extends Array{}); //ES6 Array subclass instances answer true unless the subclass over-rides the static @@isArray property assert(new class extends Array { constructor() {return {}}; }); // even if the subclass instance isn't an exotic array object (this is an improvement over the current ES6 spec. which says that //isArray answers false in this case assert(new Proxy( [ ], { }); //because it's all done with property access, no instance inspection required.

(These want Array.isArray inside assert, of course.)

assert( ! new Int32Array(10)); //not array-like, just like ES5

However, I think we should experiment with giving %TypedArrau% is true valued @@isArray property. I'm guessing that this change won't actually break anything.

Probably ok.

the above proposal completely decouples Array.isArray from exotic array object checking or any other direct instance inspection. Everything works at the property access level and so is (by default) transparent to proxies and completely controllable at the ES cod level.

Righteous goal!

# Allen Wirfs-Brock (9 years ago)

On Nov 17, 2014, at 11:06 AM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

Array[Symbol.isArray]] = true;  //note, this is a constructor property, not an Array.prototype property.

Minomer, then -- how about Symbol.isArrayClass?

perhaps, I have a negative reaction to including the work "class" but can probably live with it.

we also change Array.prototype.concat to do the above Array.isArray test instead of using @@comcatSpreadable and change JSON.stringify to use this test where it current checks for an exotic array object.

Then we have the following:

assert(Array.isArray( [ ] )); //just like ES5 assert( ! Array.isArray( Object.create(Array.prototype))); //just like ES5

How so? True:

js> Array.isArray(Array.prototype) true js> Array.isArray(Object.create(Array.prototype)) false

but constructor isn't shadowed:

js> Array.prototype.constructor function Array() { [native code] } js> Object.create(Array.prototype).constructor function Array() { [native code] }

Shit, you're right. This is the hard case for legacy compat using this technique.

I think I see a way to make this give the legacy answer, but I suspect I can't make it give the same answer for Array.isArray(new Proxy(Object.create(Array.prototype), { })); May limiting the WTF=ness to that case isn't so bad.

assert (new class extends Array{}); //ES6 Array subclass instances answer true unless the subclass over-rides the static @@isArray property assert(new class extends Array { constructor() {return {}}; }); // even if the subclass instance isn't an exotic array object (this is an improvement over the current ES6 spec. which says that //isArray answers false in this case assert(new Proxy( [ ], { }); //because it's all done with property access, no instance inspection required.

(These want Array.isArray inside assert, of course.)

of course

# Allen Wirfs-Brock (9 years ago)

On Nov 17, 2014, at 2:30 PM, Allen Wirfs-Brock wrote:

On Nov 17, 2014, at 11:06 AM, Brendan Eich wrote:

Allen Wirfs-Brock wrote:

Array[Symbol.isArray]] = true;  //note, this is a constructor property, not an Array.prototype property.

Minomer, then -- how about Symbol.isArrayClass?

perhaps, I have a negative reaction to including the work "class" but can probably live with it.

we also change Array.prototype.concat to do the above Array.isArray test instead of using @@comcatSpreadable and change JSON.stringify to use this test where it current checks for an exotic array object.

Then we have the following:

assert(Array.isArray( [ ] )); //just like ES5 assert( ! Array.isArray( Object.create(Array.prototype))); //just like ES5

How so? True:

js> Array.isArray(Array.prototype) true js> Array.isArray(Object.create(Array.prototype)) false

but constructor isn't shadowed:

js> Array.prototype.constructor function Array() { [native code] } js> Object.create(Array.prototype).constructor function Array() { [native code] }

Shit, you're right. This is the hard case for legacy compat using this technique.

I think I see a way to make this give the legacy answer, but I suspect I can't make it give the same answer for Array.isArray(new Proxy(Object.create(Array.prototype), { })); May limiting the WTF=ness to that case isn't so bad.

OK, this should deal with the Object.create(Array.prototype) case

Array.isArray = function isArray(obj) { let constructor = obj.constructor; If (typeof constructor != 'function') return false; let construictorIs be Object.getOwnPropertyDescript(constructor,"isArray"); if (constructorIs) { //the constructor has an isArray property, so we'll assume it is a built-in Array constructor if (isOrdinaryObject(obj)) return false; //The Object.create(Array.prototype) legacy compat. case. if (isProxy(obj)) return isArray(getProxyTarget(obj)); //Array.isArray(new Proxy(Object.create(Array.prototype), {})) //I'm not sure that the above line is really a compat. issue. I'd prefer to leave it out. } return !!constructor[Symbol.isArray]; }

It takes a little hackery to deal with the legacy Object.create(Array.prototype) cases but for ES6 subclasses and non-subclasses such as typed arrays it doesn't perform any exotic instance sniffing.

# Brendan Eich (9 years ago)

Allen Wirfs-Brock wrote:

    if (isProxy(obj)) return isArray(getProxyTarget(obj)); //Array.isArray(new Proxy(Object.create(Array.prototype), {}))
    //I'm not sure that the above line is really a compat. issue. I'd prefer to leave it out.

Please do -- it contradicts the righteous goal.

# Tom Van Cutsem (9 years ago)

2014-11-17 18:30 GMT+01:00 Allen Wirfs-Brock <allen at wirfs-brock.com>:

But probing through the proxy as has been proposed is a terrible violation of the MOP API boundary and isn't generalizable to other built-ins that are dependent upon internal state. While a null handler proxy on a direct instance of Array would work under that design, a comparable Map.isMap or Promise.isPromise method would not. Rather than a one-off hack I think we should use a new pattern that is generalizable:

If we can find consensus on this much more general pattern of type-testing by using a symbol on the constructor, I'm all for it. +1! I also think we don't want to special-case Proxies in the proposed type-testing algorithm.

That said, if we cannot come to a consensus on this more generic form of type-testing, I would still defend the position that Array.isArray merits an exception (compared to Map.isMap/Promise.isPromise etc.) precisely because Arrays were carefully defined not to depend on any special instance state. In this scenario, repurposing the @@isConcatSpreadable symbol seems like the most obvious thing to do (it currently feels terribly ad-hoc, making it something more generic such as @@isArrayLike makes more sense to me)