"Subclassing" basic types in DOM - best method?

# Tab Atkins Jr. (13 years ago)

For several things in the DOM and related APIs, we want objects that are more-or-less the same as some basic ES stuff, like Arrays or Maps, and which are appropriate to treat as those objects in a generic manner.

For example, the URLQuery interface in the URL Standard url.spec.whatwg.org/#interface-urlquery, which represents the

query portion (key/value pairs) of a URL, is basically a Map. (It's missing a few pieces right now, like has() and the iterator-producing functions, but Anne plans to add them.)

Ideally, an author could take a library with generic Map-manipulating functions, pass a URLQuery object in, and have a reasonable chance of having that just succeed. Hopefully, this should be robust to common type-checking operations, like doing a structural test, or an instanceof test.

Naively, this is doable just by subclassing Map (in the standard proto way), and overriding the methods, to delegate to the Map methods and then do additional things. AWB posted some code to WHATWG detailing how you would do this in ES5 and ES6; it's predictably simple.

However, URLQuery has some invariants it needs to maintain: it's a String->String map, not Any->Any. As such, it probably doesn't want

to actually initialize itself as a Map and forward to the Map methods; instead, it should probably maintain a hidden internal Map and control the access to that itself, so it can ensure that no non-Strings leak into the map.

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable? Are there better ways, perhaps on the horizon? Any other thoughts?

(Further examples: NodeList being an Array, URLFragments being an Array, a few DOM->CSS bridges being Maps...)

# Brendan Eich (13 years ago)

Tab Atkins Jr. wrote:

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable?

I think it's either irrelevant (no one tests 'aUrlQuery instanceof Map') or else a potential problem (cross-frame instanceof).

Are there better ways, perhaps on the horizon? Any other thoughts?

Testing whether something quacks like a duck has not been on the agenda since ES4's like, which had other issues (as an annotation, arguably not so much as an operator). Just testing for properties, even testing their values against certain "types", is perhaps not enough to deduce Map-ness or (in the case of urlQuery) string->string Map-ness.

We can let people develop protocols, functional predicate APIs, for this. Or we could try to preload some as built-ins. I fear any committee shooting once, hitting some target somewhere, and pushing that into the spec.

Where I would like to go, if you force me to pick a promising path:

disnetdev.com/contracts.coffee/#duck, disnetdev.com/blog/2011/09/05/Duck-Typing-Invariants-In-contracts.coffee

# Alex Russell (13 years ago)

On Nov 19, 2012, at 11:16 PM, "Tab Atkins Jr." <jackalmage at gmail.com> wrote:

For several things in the DOM and related APIs, we want objects that are more-or-less the same as some basic ES stuff, like Arrays or Maps, and which are appropriate to treat as those objects in a generic manner.

For example, the URLQuery interface in the URL Standard url.spec.whatwg.org/#interface-urlquery, which represents the query portion (key/value pairs) of a URL, is basically a Map. (It's missing a few pieces right now, like has() and the iterator-producing functions, but Anne plans to add them.)

Ideally, an author could take a library with generic Map-manipulating functions, pass a URLQuery object in, and have a reasonable chance of having that just succeed. Hopefully, this should be robust to common type-checking operations, like doing a structural test, or an instanceof test.

Naively, this is doable just by subclassing Map (in the standard proto way), and overriding the methods, to delegate to the Map methods and then do additional things. AWB posted some code to WHATWG detailing how you would do this in ES5 and ES6; it's predictably simple.

However, URLQuery has some invariants it needs to maintain: it's a String->String map, not Any->Any. As such, it probably doesn't want to actually initialize itself as a Map and forward to the Map methods; instead, it should probably maintain a hidden internal Map and control the access to that itself, so it can ensure that no non-Strings leak into the map.

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable? Are there better ways, perhaps on the horizon? Any other thoughts?

(Further examples: NodeList being an Array, URLFragments being an Array, a few DOM->CSS bridges being Maps...)

You can preserve all of those invariants + instanceof checking with proxies, or a subclass that simply over-writes the set methods to blow up on on-strike key/value setting. Either way, it doesn't seem particularly difficult and doesn't, to my mind, reduce the utility of having actual JS-native types as the baseline from which we design/subclass over in DOM.

-- Alex Russell slightlyoff at google.com slightlyoff at chromium.org alex at dojotoolkit.org BE03 E88D EABB 2116 CC49 8259 CF78 E242 59C3 9723

# Alex Russell (13 years ago)

Actually, looking at this IDL more closely, I see unneeded invariants causing most of the problem. If URLQuery subclasses Map (assuming we make this possible, which we should), it only needs to promise to hand back strings, not take them. The behavior can simply be defined as toString()-ing the contents when calling "getAll()". There's no reason to re-defined anything about Map here or prevent the normal Map methods from taking any/any as key/value pairs. That URLQuery might, in normal usage, behave this way is a decision for users of the API.

# Domenic Denicola (13 years ago)

For URLQuery in particular, since it's a String->String map, why not just use a plain-old-JavaScript-object with appropriate interceptions via a proxy? This provides a much more idiomatic API:

new URLQuery(object) stays the same urlQuery.get(name) -> urlQuery[name][0]

urlQuery.getAll(name) -> urlQuery[name]

urlQuery.set(name, values) -> urlQuery[name] = values; use proxy here

urlQuery.add(name, values) -> urlQuery[name].push(...values); use proxy here

urlQuery.has(name) -> Object.prototype.hasOwnProperty.call(urlQuery, name)

urlQuery.delete(name) -> delete urlQuery[name]

urlQuery.delete(name, value) -> if (urlQuery[name]) { urlQuery[name] = urlQuery[name].filter(x => x !== value); }

"use proxy here" means use it to do argument validation and manipulation of the associated URL object, with percent-encoding.

The above assumes you want urlQuery[name] to always be an array; alternatively it could be either a string or array of strings, with appropriate magic behavior for either.

# Alex Russell (13 years ago)

I think the basic issue here is that DOM is over-specifying the constraints (I assume because WebIDL makes that most natural?), not the available JS hacks to implement their weirdo type constraints. Lets not feed the misdesign trolls = )

# Erik Arvidsson (13 years ago)

On Tue, Nov 20, 2012 at 9:25 AM, Domenic Denicola < domenic at domenicdenicola.com> wrote:

For URLQuery in particular, since it's a String->String map, why not just use a plain-old-JavaScript-object with appropriate interceptions via a proxy? This provides a much more idiomatic API:

new URLQuery(object) stays the same urlQuery.get(name) -> urlQuery[name][0] urlQuery.getAll(name) -> urlQuery[name] urlQuery.set(name, values) -> urlQuery[name] = values; use proxy here urlQuery.add(name, values) -> urlQuery[name].push(...values); use proxy here urlQuery.has(name) -> Object.prototype.hasOwnProperty.call(urlQuery, name)

That is not very nice.

urlQuery.delete(name) -> delete urlQuery[name] urlQuery.delete(name, value) -> if (urlQuery[name]) { urlQuery[name] = urlQuery[name].filter(x => x !== value); }

I think the main issue with using an API like this is that you will end up with conflicts between the storage space and the API space. In your proposal there are no API methods but as soon as you add one you are limiting the keys you can use. It seems very likely that you don't want this to throw when people try to concat this with a string, so you add a toString method. Now, what happens when you do urlQuery['toString'] or worse urlQuery['proto'] = 'huh'?

This kind of problem is hurting people all the time when they use localstorage

# Tab Atkins Jr. (13 years ago)

On Mon, Nov 19, 2012 at 9:46 PM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable?

I think it's either irrelevant (no one tests 'aUrlQuery instanceof Map') or else a potential problem (cross-frame instanceof).

People do perform those checks, though. For example, in a method that accepts either an array or other things, a quick "foo instanceof Array" check is a clear, easy way to check what you've got. It suffers from cross-frame issues, but shrug. Similar checks will proliferate as people write methods that take generic Maps.

(On the other hand, they might instead just feature-test for the existence of items(), and then use it as an iterable, in which case this would be a non-issue. Depends on the use-case, I suppose.)

Where I would like to go, if you force me to pick a promising path:

disnetdev.com/contracts.coffee/#duck, disnetdev.com/blog/2011/09/05/Duck-Typing-Invariants-In-contracts.coffee

That's my preferred direction, too, but I suspect that's far off. It doesn't even have a strawman yet, does it?

# Tab Atkins Jr. (13 years ago)

On Tue, Nov 20, 2012 at 3:28 AM, Alex Russell <alex at dojotoolkit.org> wrote:

Actually, looking at this IDL more closely, I see unneeded invariants causing most of the problem. If URLQuery subclasses Map (assuming we make this possible, which we should),

Already possible. AWB posted code to WHATWG showing how. The Map constructor is set up to respond correctly to "Map.call(this);", blessing the object with the appropriate Map data structure.

it only needs to promise to hand back strings, not take them. The behavior can simply be defined as toString()-ing the contents when calling "getAll()". There's no reason to re-defined anything about Map here or prevent the normal Map methods from taking any/any as key/value pairs. That URLQuery might, in normal usage, behave this way is a decision for users of the API.

Nope, that's not good enough. For example, you have to do input cleanup (replacing lone surrogates with U+FFFD, escaping &, etc.) which affects whether two keys are "the same". This affects set()'s replacing behavior, and add()'s coalescing behavior. Heck, even without extra cleanup, just the fact that it requires string keys means you need to eagerly preserve the invariant - foo.set([1,2], 'bar') and foo.set('1,2', 'bar') should both set the same key. So, the stringifying must be eager. Additionally, you must override the plain method anyway, since (if the URLQuery is attached to a URL), it synchronously adjusts the underlying URL as well.

However, if you do a normal subclass, then people can do silly things like "Map.prototype.set(query, key, val);", which would skip URLQuery's own set() method, avoiding the invariant preservation and the URL manipulation.

We could avoid these problems by only "subclassing" in the sense that URLQuery.prototype = new Map(); (or Object.create(Map.prototype)), but not invoking the Map constructor and actually storing data in a closure-hidden internal Map, so instanceof would work but using Map's own methods wouldn't.

Or, we could ignore the whole thing, and just make sure that we ducktype as a Map. Anne is fine with either, as long as a resolution is forthcoming in a reasonable amount of time (more or less, only dependent on things that are already Harmony, such that we can just apply them to the spec now, even if the actual impl will be magic before the feature is implemented properly).

# Tab Atkins Jr. (13 years ago)

On Tue, Nov 20, 2012 at 9:38 AM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:

On Tue, Nov 20, 2012 at 9:25 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

For URLQuery in particular, since it's a String->String map, why not just use a plain-old-JavaScript-object with appropriate interceptions via a proxy? This provides a much more idiomatic API:

new URLQuery(object) stays the same urlQuery.get(name) -> urlQuery[name][0] urlQuery.getAll(name) -> urlQuery[name] urlQuery.set(name, values) -> urlQuery[name] = values; use proxy here urlQuery.add(name, values) -> urlQuery[name].push(...values); use proxy here urlQuery.has(name) -> Object.prototype.hasOwnProperty.call(urlQuery, name)

That is not very nice.

urlQuery.delete(name) -> delete urlQuery[name] urlQuery.delete(name, value) -> if (urlQuery[name]) { urlQuery[name] = urlQuery[name].filter(x => x !== value); }

I think the main issue with using an API like this is that you will end up with conflicts between the storage space and the API space. In your proposal there are no API methods but as soon as you add one you are limiting the keys you can use. It seems very likely that you don't want this to throw when people try to concat this with a string, so you add a toString method. Now, what happens when you do urlQuery['toString'] or worse urlQuery['proto'] = 'huh'?

This kind of problem is hurting people all the time when they use localstorage

Yes, a real object (or a Proxy exposing an object-like API) is likely hostile here. It's okay in very limited instances, but it prevents you from extending the API beyond CRUD, requiring ugly workarounds like what you have for .has() and the 2-arg .delete().

Plus, as URLQuery is actually a MultiMap, not a plain Map (but is friendly to being treated as a plain Map), we already know we want to exceed that basic API, with things like the 2-arg delete().

On that note, are there any active proposals for MultiMaps in tc39? It would be great to align URLQuery's API with tc39's plans, if they have any.

# Rick Waldron (13 years ago)

On Tue, Nov 20, 2012 at 2:45 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:

On Mon, Nov 19, 2012 at 9:46 PM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable?

I think it's either irrelevant (no one tests 'aUrlQuery instanceof Map') or else a potential problem (cross-frame instanceof).

People do perform those checks, though. For example, in a method that accepts either an array or other things, a quick "foo instanceof Array" check is a clear, easy way to check what you've got.

Be careful there, it's incredibly rare to see code that does that—which is why Array.isArray was created and is generally shimmed with some version of ({}).toString.call(arg).slice(8, -1) === "Array"... instanceof is generally viewed (whether correctly or not) as "broken".

It suffers from cross-frame issues, but shrug. Similar checks will proliferate as people write methods that take generic Maps.

For the same reason as previously stated, I predict that popular libraries will simply add "isMap" to their offerings and the implementation will look similar to the pattern I showed earlier.

(On the other hand, they might instead just feature-test for the existence of items(), and then use it as an iterable, in which case this would be a non-issue. Depends on the use-case, I suppose.)

Probably not since that will result in a lot of false positives when:

let o = { items: function() {} };

would pass that test.

# Tab Atkins Jr. (13 years ago)

On Tue, Nov 20, 2012 at 1:31 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 2:45 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

On Mon, Nov 19, 2012 at 9:46 PM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable?

I think it's either irrelevant (no one tests 'aUrlQuery instanceof Map') or else a potential problem (cross-frame instanceof).

People do perform those checks, though. For example, in a method that accepts either an array or other things, a quick "foo instanceof Array" check is a clear, easy way to check what you've got.

Be careful there, it's incredibly rare to see code that does that—which is why Array.isArray was created and is generally shimmed with some version of ({}).toString.call(arg).slice(8, -1) === "Array"... instanceof is generally viewed (whether correctly or not) as "broken".

Of course, that pattern is broken too - it lets you detect actual Arrays, but not things that subclass Array. A Map.ismap that works the same way would completely fail to detect a URLQuery as a Map.

(On the other hand, they might instead just feature-test for the existence of items(), and then use it as an iterable, in which case this would be a non-issue. Depends on the use-case, I suppose.)

Probably not since that will result in a lot of false positives when:

let o = { items: function() {} };

would pass that test.

I'm not sure how else you're supposed to detect iterables.

# Peter van der Zee (13 years ago)

On Tue, Nov 20, 2012 at 10:31 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 2:45 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

On Mon, Nov 19, 2012 at 9:46 PM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable?

I think it's either irrelevant (no one tests 'aUrlQuery instanceof Map') or else a potential problem (cross-frame instanceof).

People do perform those checks, though. For example, in a method that accepts either an array or other things, a quick "foo instanceof Array" check is a clear, easy way to check what you've got.

Be careful there, it's incredibly rare to see code that does that—which is

I don't agree. I see often see instanceof, both with Array and with other objects. This danger you and everybody speaks of only applies to cross-frame scripts. And while this danger is real (and I don't mean to make it sound like it isn't), I think you should first consider the amount of people actually doing cross frame scripting because it's not something most people touch frequently, if at all. Any studies to get such numbers?

# Rick Waldron (13 years ago)

On Tue, Nov 20, 2012 at 4:54 PM, Peter van der Zee <ecma at qfox.nl> wrote:

On Tue, Nov 20, 2012 at 10:31 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 2:45 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

On Mon, Nov 19, 2012 at 9:46 PM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable?

I think it's either irrelevant (no one tests 'aUrlQuery instanceof Map')

or else a potential problem (cross-frame instanceof).

People do perform those checks, though. For example, in a method that accepts either an array or other things, a quick "foo instanceof Array" check is a clear, easy way to check what you've got.

Be careful there, it's incredibly rare to see code that does that—which is

I don't agree. I see often see instanceof, both with Array and with other objects. This danger you and everybody speaks of only applies to cross-frame scripts.

Please don't lump me in, I said: 'instanceof is generally viewed (whether correctly or not) as "broken".' I said nothing about cross-frame-anything.

And while this danger is real (and I don't mean to make it sound like it isn't), I think you should first consider the amount of people actually doing cross frame scripting because it's not something most people touch frequently, if at all. Any studies to get such numbers?

I'm more concerned with this: [] instanceof Object; // true

# Rick Waldron (13 years ago)

On Tue, Nov 20, 2012 at 4:52 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:

On Tue, Nov 20, 2012 at 1:31 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 2:45 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

On Mon, Nov 19, 2012 at 9:46 PM, Brendan Eich <brendan at mozilla.com> wrote:

Tab Atkins Jr. wrote:

If we did this, the only reason to continue subclassing Map is to get instanceof checks to work. Is this acceptable?

I think it's either irrelevant (no one tests 'aUrlQuery instanceof Map')

or else a potential problem (cross-frame instanceof).

People do perform those checks, though. For example, in a method that accepts either an array or other things, a quick "foo instanceof Array" check is a clear, easy way to check what you've got.

Be careful there, it's incredibly rare to see code that does that—which is why Array.isArray was created and is generally shimmed with some version of ({}).toString.call(arg).slice(8, -1) === "Array"... instanceof is generally viewed (whether correctly or not) as "broken".

Of course, that pattern is broken too - it lets you detect actual Arrays, but not things that subclass Array.

That's not possible in JavaScript today, so it's not "broken". There is no way to correctly subclass Array, so this has never been an issue that JavaScript programs would encounter. Future hostile? Maybe.

# Rick Waldron (13 years ago)

On Tue, Nov 20, 2012 at 3:08 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:

On Tue, Nov 20, 2012 at 3:28 AM, Alex Russell <alex at dojotoolkit.org> wrote:

Actually, looking at this IDL more closely, I see unneeded invariants causing most of the problem. If URLQuery subclasses Map (assuming we make this possible, which we should),

Already possible. AWB posted code to WHATWG showing how. The Map constructor is set up to respond correctly to "Map.call(this);", blessing the object with the appropriate Map data structure.

it only needs to promise to hand back strings, not take them. The behavior can simply be defined as toString()-ing the contents when calling "getAll()". There's no reason to re-defined anything about Map here or prevent the normal Map methods from taking any/any as key/value pairs. That URLQuery might, in normal usage, behave this way is a decision for users of the API.

Nope, that's not good enough. For example, you have to do input cleanup (replacing lone surrogates with U+FFFD, escaping &, etc.) which affects whether two keys are "the same". This affects set()'s replacing behavior, and add()'s coalescing behavior. Heck, even without extra cleanup, just the fact that it requires string keys means you need to eagerly preserve the invariant - foo.set([1,2], 'bar') and foo.set('1,2', 'bar') should both set the same key. So, the stringifying must be eager. Additionally, you must override the plain method anyway, since (if the URLQuery is attached to a URL), it synchronously adjusts the underlying URL as well.

However, if you do a normal subclass, then people can do silly things like "Map.prototype.set(query, key, val);", which would skip URLQuery's own set() method, avoiding the invariant preservation and the URL manipulation.

We could avoid these problems by only "subclassing" in the sense that URLQuery.prototype = new Map(); (or Object.create(Map.prototype)), but not invoking the Map constructor and actually storing data in a closure-hidden internal Map, so instanceof would work but using Map's own methods wouldn't.

class URLQuery extends Map { constructor(url) { // parse the url into a "map", for now // just pretend this is the result of parsing // the url: let parsed = [ ["a", "alpha"], ["b", "beta"] ]; super(parsed); } getAll() { // ... implementation details... } }

let query = new URLQuery("example.com");

(I don't actually know if that's the correct way to use URLQuery, I was just making something up for the sake of illustration)

# Tab Atkins Jr. (13 years ago)

On Tue, Nov 20, 2012 at 2:02 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 4:52 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

Of course, that pattern is broken too - it lets you detect actual Arrays, but not things that subclass Array.

That's not possible in JavaScript today, so it's not "broken". There is no way to correctly subclass Array, so this has never been an issue that JavaScript programs would encounter. Future hostile? Maybe.

function foo() {...} foo.prototype = [];

You get an object which has a self-updating .length property, gets all the Array methods like .map(), etc. If that's not "subclassing", I don't know what is. ^_^

# Tab Atkins Jr. (13 years ago)

On Tue, Nov 20, 2012 at 2:06 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 3:08 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

Nope, that's not good enough. For example, you have to do input cleanup (replacing lone surrogates with U+FFFD, escaping &, etc.) which affects whether two keys are "the same". This affects set()'s replacing behavior, and add()'s coalescing behavior. Heck, even without extra cleanup, just the fact that it requires string keys means you need to eagerly preserve the invariant - foo.set([1,2], 'bar') and foo.set('1,2', 'bar') should both set the same key. So, the stringifying must be eager. Additionally, you must override the plain method anyway, since (if the URLQuery is attached to a URL), it synchronously adjusts the underlying URL as well.

However, if you do a normal subclass, then people can do silly things like "Map.prototype.set(query, key, val);", which would skip URLQuery's own set() method, avoiding the invariant preservation and the URL manipulation.

We could avoid these problems by only "subclassing" in the sense that URLQuery.prototype = new Map(); (or Object.create(Map.prototype)), but not invoking the Map constructor and actually storing data in a closure-hidden internal Map, so instanceof would work but using Map's own methods wouldn't.

class URLQuery extends Map { constructor(url) { // parse the url into a "map", for now // just pretend this is the result of parsing // the url: let parsed = [ ["a", "alpha"], ["b", "beta"] ]; super(parsed); } getAll() { // ... implementation details... } }

let query = new URLQuery("example.com");

I'm not sure why you're suggesting this. Deferring to Map's built-ins for get(), etc. breaks the basic functionality of the object, as I explained in the quoted section immediately above your response. ^_^

# Rick Waldron (13 years ago)

On Tuesday, November 20, 2012 at 5:23 PM, Tab Atkins Jr. wrote:

On Tue, Nov 20, 2012 at 2:02 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 4:52 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

Of course, that pattern is broken too - it lets you detect actual Arrays, but not things that subclass Array.

That's not possible in JavaScript today, so it's not "broken". There is no way to correctly subclass Array, so this has never been an issue that JavaScript programs would encounter. Future hostile? Maybe.

function foo() {...} foo.prototype = [];

var a = new foo(); a.push(1);

a.length; // 1 a.length = 0;

console.log(a);

{"0":1,"length":0}

Again, it's not possible to subclass Array.

# Rick Waldron (13 years ago)

On Tuesday, November 20, 2012 at 5:25 PM, Tab Atkins Jr. wrote:

On Tue, Nov 20, 2012 at 2:06 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tue, Nov 20, 2012 at 3:08 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

Nope, that's not good enough. For example, you have to do input cleanup (replacing lone surrogates with U+FFFD, escaping &, etc.) which affects whether two keys are "the same". This affects set()'s replacing behavior, and add()'s coalescing behavior. Heck, even without extra cleanup, just the fact that it requires string keys means you need to eagerly preserve the invariant - foo.set([1,2], 'bar') and foo.set('1,2', 'bar') should both set the same key. So, the stringifying must be eager. Additionally, you must override the plain method anyway, since (if the URLQuery is attached to a URL), it synchronously adjusts the underlying URL as well.

However, if you do a normal subclass, then people can do silly things like "Map.prototype.set(query, key, val);", which would skip URLQuery's own set() method, avoiding the invariant preservation and the URL manipulation.

We could avoid these problems by only "subclassing" in the sense that URLQuery.prototype = new Map(); (or Object.create(Map.prototype)), but not invoking the Map constructor and actually storing data in a closure-hidden internal Map, so instanceof would work but using Map's own methods wouldn't.

class URLQuery extends Map { constructor(url) { // parse the url into a "map", for now // just pretend this is the result of parsing // the url: let parsed = [ ["a", "alpha"], ["b", "beta"] ]; super(parsed); } getAll() { // ... implementation details... } }

let query = new URLQuery("example.com");

I'm not sure why you're suggesting this. Deferring to Map's built-ins for get(), etc. breaks the basic functionality of the object, as I explained in the quoted section immediately above your response. ^_^

I was illustrating the ES6 way to subclass built-ins that result in correctly "inherited" semantics (eg. The array length behaviour gotcha).

# Tab Atkins Jr. (13 years ago)

On Tue, Nov 20, 2012 at 2:52 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tuesday, November 20, 2012 at 5:23 PM, Tab Atkins Jr. wrote:

function foo() {...} foo.prototype = [];

var a = new foo(); a.push(1);

a.length; // 1 a.length = 0;

console.log(a);

{"0":1,"length":0}

Again, it's not possible to subclass Array.

Ah, true. Then again, I've only ever seen that use of .length here on this list. I'm sure it exists, but it doesn't in my head. ^_^

# Tab Atkins Jr. (13 years ago)

Could you use a client that quotes properly? Your responses keep flattening the quotes to a single level, making it impossible to tell where each ends.

On Tue, Nov 20, 2012 at 2:56 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

On Tuesday, November 20, 2012 at 5:25 PM, Tab Atkins Jr. wrote:

I'm not sure why you're suggesting this. Deferring to Map's built-ins for get(), etc. breaks the basic functionality of the object, as I explained in the quoted section immediately above your response. ^_^

I was illustrating the ES6 way to subclass built-ins that result in correctly "inherited" semantics (eg. The array length behaviour gotcha).

Sure. As I said in the beginning of the thread, though, AWB already provided examples of that in the WHATWG thread, and this code has been alluded by me and others throughout the thread.

Since you supplied it without further commentary, and immediately following a quote from me where I explained why that doesn't work for our purposes, I was confused by your intent.

# Brendan Eich (13 years ago)

Tab Atkins Jr. wrote:

let o = { items: function() {} };

would pass that test.

I'm not sure how else you're supposed to detect iterables.

It's not 'items', btw. The iterator-getter/maker is named 'iterator' in SpiderMonkey currently, and last I heard would be @iterator (well-known public symbol) in ES6.

As with most duck typing, you're right and I think Rick is picking on a non-issue: there's no point worrying about the halting problem. Just call the quack function and let the chips fall where they may ;-).

# Alex Russell (13 years ago)

On Nov 20, 2012, at 8:08 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

On Tue, Nov 20, 2012 at 3:28 AM, Alex Russell <alex at dojotoolkit.org> wrote:

Actually, looking at this IDL more closely, I see unneeded invariants causing most of the problem. If URLQuery subclasses Map (assuming we make this possible, which we should),

Already possible. AWB posted code to WHATWG showing how. The Map constructor is set up to respond correctly to "Map.call(this);", blessing the object with the appropriate Map data structure.

it only needs to promise to hand back strings, not take them. The behavior can simply be defined as toString()-ing the contents when calling "getAll()". There's no reason to re-defined anything about Map here or prevent the normal Map methods from taking any/any as key/value pairs. That URLQuery might, in normal usage, behave this way is a decision for users of the API.

Nope, that's not good enough. For example, you have to do input cleanup (replacing lone surrogates with U+FFFD, escaping &, etc.) which affects whether two keys are "the same". This affects set()'s replacing behavior, and add()'s coalescing behavior. Heck, even without extra cleanup, just the fact that it requires string keys means you need to eagerly preserve the invariant - foo.set([1,2], 'bar') and foo.set('1,2', 'bar') should both set the same key. So, the stringifying must be eager.

Well that's clearly untrue. It's possible to have any number of other behaviors than "string keys" and put your cleanups in other places (getAll(), etc.). I get that you might not want to do that, but it's a choice, and one we don't have to accept at face value.

Additionally, you must override the plain method anyway, since (if the URLQuery is attached to a URL), it synchronously adjusts the underlying URL as well.

I see.

However, if you do a normal subclass, then people can do silly things like "Map.prototype.set(query, key, val);", which would skip URLQuery's own set() method, avoiding the invariant preservation and the URL manipulation.

So what? Unless there's a reason to disallow that, it seems fine by me. If people have a handle to the query object, one assumes they can already do all manner of silly things.

We could avoid these problems by only "subclassing" in the sense that URLQuery.prototype = new Map(); (or Object.create(Map.prototype)), but not invoking the Map constructor and actually storing data in a closure-hidden internal Map, so instanceof would work but using Map's own methods wouldn't.

Or, we could ignore the whole thing, and just make sure that we ducktype as a Map.

That's some pretty weak sauce. Lining up the types and designing APIs in the native way is the charge, and we have to do our best by it, lest we just continue to add magic to the platform.

Anne is fine with either, as long as a resolution is forthcoming in a reasonable amount of time (more or less, only dependent on things that are already Harmony, such that we can just apply them to the spec now, even if the actual impl will be magic before the feature is implemented properly).

~TJ

-- Alex Russell slightlyoff at google.com slightlyoff at chromium.org alex at dojotoolkit.org BE03 E88D EABB 2116 CC49 8259 CF78 E242 59C3 9723

# Tab Atkins Jr. (13 years ago)

On Wed, Nov 21, 2012 at 3:12 AM, Alex Russell <alex at dojotoolkit.org> wrote:

On Nov 20, 2012, at 8:08 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:

Nope, that's not good enough. For example, you have to do input cleanup (replacing lone surrogates with U+FFFD, escaping &, etc.) which affects whether two keys are "the same". This affects set()'s replacing behavior, and add()'s coalescing behavior. Heck, even without extra cleanup, just the fact that it requires string keys means you need to eagerly preserve the invariant - foo.set([1,2], 'bar') and foo.set('1,2', 'bar') should both set the same key. So, the stringifying must be eager.

Well that's clearly untrue. It's possible to have any number of other behaviors than "string keys" and put your cleanups in other places (getAll(), etc.). I get that you might not want to do that, but it's a choice, and one we don't have to accept at face value.

Dude, my example showed why you have to do that, unless you track additional metadata and eat an O(n) cost on get() (and adding that additional metadata would also require you to override the set() method, so you're not gaining anything).

What's the output of this code?

query.set("1,2", "foo"); query.set([1,2], "bar"); query.get([1]+",2");

It had better be "bar", because that's how string maps work. To ensure this, though, you'll need to keep track of the order things are set in. (Do Maps automatically iterate in key-creation order? That doesn't help - you can add a third "query.set('1,2', 'baz')" line in the example, and you should then get "baz" out.) Then, you have to do a walk through the entire map, string-converting every key, to figure out which version of the key was set last, so you can return it.

(On further thought, it's worse than that. Because you can associate multiple values to each key, you have to keep track of the creation time of each value, so you can then sort them by creation time after coalescing when doing a getAll() call.)

This is not a realistic scenario. No one would ever do this, or countenance browsers doing this under the hood. It's terrible, and the only way around it is to preprocess eagerly to ensure that your keys+values actually match the type contract that you're setting out.

However, if you do a normal subclass, then people can do silly things like "Map.prototype.set(query, key, val);", which would skip URLQuery's own set() method, avoiding the invariant preservation and the URL manipulation.

So what? Unless there's a reason to disallow that, it seems fine by me. If people have a handle to the query object, one assumes they can already do all manner of silly things.

The reason to disallow it is that it doesn't make sense. Allowing people to think they're setting a key in the URLQuery, but having it not actually work or add anything into the query (or worse, having it "work" but in completely unpredictable ways, as keys are munged together in unpredictable orders) is terrible API design. There's no benefit to it whatsoever.

For the purposes of this API, Map.prototype.set is a completely unrelated method that happens to share a name with something on URLQuery.prototype.

We could avoid these problems by only "subclassing" in the sense that URLQuery.prototype = new Map(); (or Object.create(Map.prototype)), but not invoking the Map constructor and actually storing data in a closure-hidden internal Map, so instanceof would work but using Map's own methods wouldn't.

Or, we could ignore the whole thing, and just make sure that we ducktype as a Map.

That's some pretty weak sauce. Lining up the types and designing APIs in the native way is the charge, and we have to do our best by it, lest we just continue to add magic to the platform.

Then help me figure out how to fix it. So far you've just insisted that we can do the simple thing, despite it having terrible side-effects.

(I'll say, though - ducktyping is a pretty native way to do things, too.)

# Rick Waldron (13 years ago)

On Wednesday, November 21, 2012 at 3:40 AM, Brendan Eich wrote:

Tab Atkins Jr. wrote:

let o = { items: function() {} };

would pass that test.

I'm not sure how else you're supposed to detect iterables.

It's not 'items', btw. The iterator-getter/maker is named 'iterator' in SpiderMonkey currently, and last I heard would be @iterator (well-known public symbol) in ES6.

As with most duck typing, you're right and I think Rick is picking on a non-issue:

I wish that were true, but I share that from user code I've encountered in the jQuery bug tracker history. Devs make objects that have a "length" property and don't understand why their object gets array treatment when passer to library code (less common these days and arguably a mistake in the duck-typing itself) or an object with a numeric nodeType property that gets mistaken for a DOM node.

...Most of this stuff is facepalm inducing :(