Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SugarJS array methods treat DOM elements of same nodeName as same object #177

Closed
dlee opened this issue Jul 30, 2012 · 54 comments
Closed

SugarJS array methods treat DOM elements of same nodeName as same object #177

dlee opened this issue Jul 30, 2012 · 54 comments

Comments

@dlee
Copy link

dlee commented Jul 30, 2012

div1 = document.getElementsByTagName('div')[0];
div2 = document.getElementsByTagName('div')[1];

[div1].subtract([div2]);
/* should return [div1] */
/* returns [] */

[div1].union([div2]);
/* should return [div1, div2] */
/* returns [div1] */

[div1].intersect([div2]);
/* should return [] */
/* returns [div1] */
@dlee
Copy link
Author

dlee commented Jul 30, 2012

I just got bit by this.
This is a huge issue, since manipulating arrays of DOM elements represents a large portion of client-side code.

Another example:

[div1].some(div2);
/* should return false */
/* returns true */

@andrewplummer
Copy link
Owner

Having a preliminary look at this now, but it's going to be tricky. Very tricky.

For one, DOM Elements don't have any of their own properties, making them effectively behave like empty objects.
Allowing matching to check through inherited properties is also disastrous because they will step through every property recursively and this is absolutely massive. The sheer number of properties by itself isn't so bad, but each dom elements have references to their parents and children... any attempt to traverse these will effectively "walk the tree" and massively lock up the browser.

Let me think about it a bit because at the moment I don't see any approach that provides a clear resolution...

@dlee
Copy link
Author

dlee commented Jul 30, 2012

What do you think about providing equality-by-reference versions of methods, or options to methods that make them use equality-by-reference? Of course, this would be a major change to SugarJS.

Possibly something like this:

[div1, div2, div1].uniqueByReference();
/* returns [div1, div2] */
[div1, div2, div1].unique();
/* returns [div1] */

@andrewplummer
Copy link
Owner

Like I said in the other thread, I think that's it's potentially a source of massive confusion and it will never be as explicit or understandable as using the === operator.

That said, I see the dilemma. In the first place I wonder if there is ever a situation in which you would want to be stepping through an element's properties (inherited too) to perform a comparison. I can't see how it would ever be desirable. Of course you could bail after a property not matching but even in that case simply by having a match it would be horribly unperformant. And it gets worse. Although it would be possible in theory to bail after a property proves the match to be false for normal array methods like findAll, the methods unique, union, and intersect are doing something completely different, which is serializing the object and using a hash for comparison. This was a performance optimization and provided a massive speed boost. This benefit would (in theory) have to be lost and you'd end up with On² operations on elements... dear god.

Another thing to note here is that Sugar's internal multiMatch is returning true up front if a value is equal by reference, so as it stands elements will be properly and immediately matched (it also makes me hesitant to explain Sugar's array matching methods as "equal-by-value" vs. "equal-by-reference", because it's not... or more to the point those aren't mutually exclusive). The problem comes in this phrasing of the question "what properties qualify an element as unique?" Clearly it should be more than the tag name of the element (or should it? perhaps this should at least not be taken for granted), but not all properties.

For example the innerHTML property could be one, or even the only one. Defining it this way would make searching easy:

arr.some(function(el) {
  return el.innerHTML === html;
});

Likewise, unique solves this property by allowing a callback to define the property that makes the object unique:

arr.unique(function(el) {
  return el.innerHTML;
});

The problem is I suppose that presumably it would be a combination of fields, not just a single one. Now Sugar could define these fields and always check against them. I would be very against that for a number of reasons but in the end it would be preferable to a solution that needs to check all inherited properties of an element. And by "preferable" I mostly mean "possible". In the first place who is to say which, if not all, of these properties make it unique? It most certainly seems like something that a user should be defining, not Sugar.

@dlee
Copy link
Author

dlee commented Jul 31, 2012

Unfortunately, the best way I can think of uniquely hashing a DOM element is to use its documentURI + Xpath + outerHTML. This would be quite slow. :(

I don't think it'd be too bad to have the equality-by-reference On^2 counterparts for unique, union, intersect, and subtract. This would let the users decide what uniqueness/performance characteristics they want and make informed tradeoffs.

@andrewplummer
Copy link
Owner

Agreed it would be slow. I also contend that it could not be agreed upon either.

unique has a function to allow returning a uniquing property on a case-by-case basis, so this gets us somewhere. However the others do not... perhaps they could be refactored to handle a callback of some kind that could do the same... that could be one approach.

That's the only possibility I have in my mind right now... other things that have crossed my mind are somehow having a counter that counts up for each reference to an element that cannot be hashed. Of course that doesn't really help as it still has to match by reference and if it simply counts up it will never match, which will get us nowhere. I would love to have something tangible like a number that points to its memory block so a true representation of its reference could be used, but this is just a fantasy as far as I can tell (and I've looked into it before).

@andrewplummer
Copy link
Owner

I had an idea here... IF we could somehow be able to identify DOM elements (this may be easier said than done with internal [[class]] issues in IE), there is a mechanism by which we could match them ONLY by reference, even for unique, union, and etc.

There is a sense in which I don't like this solution as right now Sugar is completely agnostic to the browsing environment, but I do agree that it is an important point as having elements in arrays is quite common, and it would be far better than forcing a choice of "equal by reference (kindof)" versus "not equal by reference (kindof)".

Thoughts on this approach?

@dlee
Copy link
Author

dlee commented Aug 6, 2012

What do you mean by "identify DOM elements"? If you mean "a unique ID per DOM element", then we're out of luck.

@andrewplummer
Copy link
Owner

I mean "identify an object that is a DOM element" (not uniquely). Object.prototype.toString will revel "[Object DOMElement]" or something to that effect, but the problem is IE, which does not...

@dlee
Copy link
Author

dlee commented Aug 6, 2012

jQuery naively uses the presence of the nodeType property to determine if an object is a DOM element. If jQuery-compatible naive checks don't bother you, we can go with that.

@andrewplummer
Copy link
Owner

I would normally hate this of course (especially the naivety of the detection) but in this case what it gains us may be worth it...

@inossidabile
Copy link

I believe Sugar should not be used to solve this task. DOM is just a part of a browser JS environment. Sugar belongs to the lower level and should not bother about DOM elements. I trust DOM is still inoperable without a proper framework. You will need a tool to handle it no matter if Sugar can do some parts of it internally. So yes, this is a common task. And it already has the common solution. Why would you want to address it again?

At the moment Sugar has pretty straightforward borders of its "influence". They should not blurred.

@ghost
Copy link

ghost commented Aug 6, 2012

Maps and WeakMaps, part of ES6, uniquely identify any object including DOM element. Here's a shim that works with anything that implements es5: https://github.com/Benvie/ES6-Harmony-Collections-Shim.

@dlee
Copy link
Author

dlee commented Aug 6, 2012

@inossidabile You say this already has a "common solution". What's that common solution?

@andrewplummer
Copy link
Owner

@inossidabile I really do agree with this... and yet @dlee's point that manipulating elements in an array as a common use seems to have some bite to it. Even if we understand that DOM Elements require some special handling when manipulating in arrays, this will not be immediately understandable to everyone, and would require explanation that could be be avoided if they could just be handled.... imagine how nice that would be....

@andrewplummer
Copy link
Owner

@dlee Well, jQuery, for one.

@inossidabile
Copy link

@dlee http://api.jquery.com/not/ http://api.jquery.com/jQuery.unique/ etc.

@andrewplummer what really is the common case it's the manipulation of collection of DOM elements. Not arrays. See the difference. Frameworks like jQuery provide all the required methods to such a collections. They work pretty much like Sugar. You can not use basic JS arrays and expect them to know anything about DOM, this is totally incorrect. As soon as DOM objects were implemented to be === equal for different instances of the same nodeType... let it be. This is the expected thing. Maybe a little surprising but at least totally compatible to every other piece of code that can avoid usage of Sugar.

@andrewplummer
Copy link
Owner

what really is the common case it's the manipulation of collection of DOM elements.

Another very good point.

In fact it really is the most (and perhaps only) relevant point here... Frameworks exist for manipulating collections of elements, and those collections are not the same thing as arrays. Even the browser itself has the concept of DOM element collections, and those are distinct from arrays.

Note here to remind us of the context of this discussion: Arrays of elements ARE perfectly usable in Sugar provided you are passing a function to the method in question (ES5 filter, some as well as other Sugar only methods like findAll, etc.). However union, intersect, subtract do not expose those mapping functions. unique does, but a bit differently from the others.

@inossidabile
Copy link

True, the ability to explicitly specify the comparison function would solve this. And this is the only possible correct and clear solution. It also is something I could use for my custom entities in theory. However with the current API it gonna be a really difficult task to solve, Andrew :). It should be thought out twice before you start implementing it.

@andrewplummer
Copy link
Owner

@inossidabile Always my voice of reason :)

@dlee
Copy link
Author

dlee commented Aug 6, 2012

@inossidabile DOM objects are not === equal for different instances of the same nodeType, nor of the same nodeName.

I think all this boils down to one thing: SugarJS introduces useful methods with better (Ruby-esque) equality semantics, but this is broken for DOM elements. We shouldn't pretend that DOM elements have no part in Javascript.

I know SugarJS isn't meant for "DOM manipulation", but we shouldn't conflate "DOM manipulation" with "proper handling of javascript language objects that happen to also represent DOM nodes". In fact, we already have the possibility of implementing the correct behavior, at the cost of performance: treat a DOM object just like a regular object.

@andrewplummer
Copy link
Owner

@dlee Keep in mind that DOM Elements 1. are standard "objects" in the sense that they are of type "object" and inherit from Object, and 2. have no direct properties on them. Under that strict definition they are working in these methods exactly as intended. Whether or not that is clear to devs is one thing, whether it is an acceptable definition is another, but there is definitely at least one plausible definition by which they are not broken.

@dlee
Copy link
Author

dlee commented Aug 6, 2012

I'm surprised with your 2. From what I can tell, DOM elements have direct properties:

x = document.body.children[0];
for (i in x) { if (x.hasOwnProperty(i)) console.log(i) }

@andrewplummer
Copy link
Owner

Sorry I had only preformed a preliminary experiment on this before so here's more data:

  1. In Chrome/Webkit it does have direct properties.
  2. In Firefox it only has 1 direct property, the constructor (I also know I have seen elements show no direct properties as well).
  3. In IE9, elements have getAttribute, removeAttribute, and setAttribute as direct properties. Nothing else is.
  4. In <= IE8, elements don't even have a hasOwnProperty method to determine this... had to double check this because I haven't seen it before. Pretty nuts.

So, even if they did have direct properties, it's pretty clear that they completely vary from implementation-to-implementation.

This is one of the many reasons Sugar steers clear of host objects. There is far too much inconsistency here, and it's this inconsistency that can be interpreted as being "broken". If (and I mean IF) Sugar does decide to handle DOM Elements differently, it will only be to match them explicitly by reference, and will continue to stay well away from this mess of different direct properties, etc.

@andrewplummer
Copy link
Owner

Another data point, IE9 returns an empty array when using Object.keys on a DOM Element. This despite the fact that "getAttribute" in el is true.

@dlee
Copy link
Author

dlee commented Aug 6, 2012

That's absolutely terrible. :(

With pity and respect for the maintainer who has to work with browser inconsistencies, I resign pushing for this. SugarJS should just take a stand whether or not it wants to specially support DOM elements to work around their quirks. Either way, it should specifically document behavior of DOM elements with SugarJS so that developers have a sense of what to expect.

Thanks for all the attention to this issue.

@andrewplummer
Copy link
Owner

Well, wait... let's keep this discussion going a bit more because I think there may be something here. Remember that all the insane inconsistencies above are only tangentially related... Of course I have no interest in somehow juggling those "direct" properties but let's look at the more obvious solution:

what if for any objects with the property nodeType defined (either direct or inherited), instead of performing any complex matching or stringification, the internal matching method would only return a direct === operation -- in other words true only if the values match by reference, false otherwise. I have an idea that I think could make this work not only for all array matching methods, but also unique, subtract, etc.

NOW before you go all nuts on me: I'm not saying it isn't hacky. I'm not saying it's pretty. In fact I'm not advocating doing this at all... I simply want to think through what the consequences would be here, both benefits and problems.

The immediate benefit is that all array methods would immediately and intuitively work with DOM elements in pretty much they only way they can work -- by comparing a reference. Of course you can still use callback functions for those methods that accept them if you want to, for example, find an element with the exact innerHTML.

The immediate problem with this is that any rogue object defining nodeType on itself is immediately susceptible to unexpected behavior. That's bad. I get it. But remember, nodeType is a hack for the sake of <= IE8 in the first place. Modern ES5-compatible environments should properly be defining element [[class]] in the first place. So far confirmed this on Chrome, FF, Safari, Opera, and IE9 and going to assume the rest for now. Some other solution might be available to us too.

The next obvious question is should elements be treated differently? Certainly it is unexpected, and unexpected behavior deserves a great justification. However it seems to me in this case that even if you like the flexibility of the "fuzzy matching" methods, when:

[el1, el2].findAll(el1);

returns both elements, no matter how you slice it, this is already very unexpected, and I think Sugar will have to do a LOT more work to make this clear (and it won't be a very satisfying answer either).

Finally, as I touched on above, even if DOM elements defined all their properties exactly the same across all browsers and we had perfect consistency, they would still be either horribly slow to perform fuzzy matching against (at best) or impossible (at worst) due to the fact that they represent a tree of elements, each pointing further up the tree and often (no doubt) in cyclic structures back at each other.

Considering this, I think it would be fair to say matching DOM elements by reference is not only the most expected method, it is for all intents the only plausible way.

Now, are there other objects out there that behave in a similar way? Of course. XML structures would exhibit very similar if not identical behavior. So the question is "what's so special about DOM elements?" But of course we all know the answer to this... DOM elements are very special as the main building blocks of all web pages. Of course I want Sugar to remain agnostic to them in an ideal world but I don't know if it's worth it to ignore them when there's such a huge (potential... we can still argue over this) benefit to be gained. If Sugar is extremely explicit about how it is handling elements in a special way, would this change not be for the best?

Please, more thoughts on this. If there are benefits/problems I missed I want to know.

@dlee
Copy link
Author

dlee commented Aug 6, 2012

I think you hit the main benefits/problems on the dot.

As for rogue objects defining nodeType, I don't think it's a big problem, as long as it's documented.
Here is another suggestion: You might also be able to check a String(domElement), since that returns "[object HTML*Element]" or even check String(domElement.constructor.prototype), which returns an error for DOM objects.

You might even want to provide an overrideable Object.isDOMElement() or a Object.shouldCompareByReference().

As for treating DOM elements differently, I say yes, since it's a different data type, and it behaves like a different data type. After brief research, the official term seems to be IDL platform objects.

As for other objects that behave in a similar way: I think the criteria is whether a data type has a way to serialize into a String such conceptually unique objects are serialized in a unique way. Functions might fall into this category, as I don't know how you can uniquely serialize a function, including its lexical scope (filed #183).

@andrewplummer
Copy link
Owner

Yes I saw your other ticket and it's something I need to address and points to a deeper problem, but I wanted to get this worked out first. Leveraging String (Object.prototype.toString would work as well) is an interesting solution. Perhaps anything that is [object XXX] but not [object Object] could be compared by reference.

This of course is predicated on the fact there is nothing following the [object XXX] pattern that could be compared by (deep) value, which of course is something that would have to be researched thoroughly... but it would eliminate the need for the nodeType check...

@timoxley
Copy link

timoxley commented Aug 7, 2012

IMO special-casing sugar for dom elements should be out of the question, at least for core. Perhaps create these inside an optional component like "sugar-dom". implementation could involve hooking sugar up so it can use jquery or other framework available in current scope for these scenarios (most common use-case). Creating cross-browser normalization code specifically for sugar seems way out of core project scope.

@andrewplummer
Copy link
Owner

@timoxley @inossidabile I feel myself getting into one of those situations where I sound like I'm pushing you to change your minds about this but I promise this isn't the case... I just want to make absolutely sure we're talking about the same thing.

Cross-browser normalization code is not and will never be on the table for Sugar. We got off on a tangent above talking about cross-browser incompatibilities, but the point of that was to illustrate why it won't be handled in that way. Rather, I want to consider applying "equality-by-reference" (and being very explicit about how and why) in one of the 3 following cases:

  1. Object is "type" [[object HTML***Element]].
  2. Object is "type" [[object ***]] but not "type" [[object Object]].
  3. Object defines a nodeType property (worst case scenario).

Having a look at cross-browser issues right now it seems that to maintain IE compatibility 1 and 2 might not be feasible... at least not alone. But I still want to consider them as they might be part of a strategy that would provide a robust check. Also, looking toward the future there will come a day when <= IE7 support is not required (it's coming up soon).

Again, if there is no other choice but to deal with cross-browser issues, then this solution will be immediately off the table...

@timoxley
Copy link

timoxley commented Aug 7, 2012

tough call. I will re-read this tome of a thread and ponder.

@andrewplummer
Copy link
Owner

That's all I ask :)

@andrewplummer
Copy link
Owner

Wanted to mention something else that's been rolling around in my head. After poking a bit and experimenting, it seems that calling toString on a DOM element produces [object HTML***Element] in standards browsers and [object] in IE (very non-standard obviously). Now of course a true DOM check would have to be more robust than this, but what if Sugar could leverage this quirk to perform a match-by-value only on objects that do not begin with [object OR are [object Object]. Anything else could be matched by reference. Of course this isn't a perfect solution -- for one if classes defined a toString method that beings with [object they would suddenly be evaluated differently inside an array... but at least the issue could be addressed without introducing any browser-environment-specific code to Sugar.

@dlee
Copy link
Author

dlee commented Aug 13, 2012

I guess it comes down to the chance a non-DOM object would have a nodeType property vs the chance a non-DOM object would override toString() to output "[object".

@inossidabile
Copy link

Now of course a true DOM check would have to be more robust than this, but what if Sugar could leverage this quirk to perform a match-by-value only on objects that do not begin with [object OR are [object Object]. Anything else could be matched by reference.

@andrewplummer Okay, you should stop here. This is heavy damage to backward compatibility. And even introducing the browser-specific quirks is a preferred solution compared to this one.

@dlee you should not be talking about "chances" within minor version ticks and I trust this is not something that deserves major one.

Come on, guys, do you really want to grab all that troubles for the DOM-specific thing that will not be used as often as you might imagine? I'm not even sure my projects using Sugar will remain green with these modifications... I'm really scared with they way you follow this issue.

@andrewplummer
Copy link
Owner

@inossidabile Yes, I think it's worth investigating if it can involve a solution that does not tread on the toes of other applications and backward-compatibility. I understand your "fear" but you're speaking generally and I don't want to get into theory but instead back assertions up with specific examples of how a change will do "heavy damage". I just don't see it.

However, that said, note here that I after having researched I am less confident with the above solution that I referenced (not "proposed" dont worry... i'm not "proposing" anything yet). The reason is that among other quirks that may arise HTMLAnchorElements have a toString method defined on them that is the href of the link. So far it appears to be special in this behavior, but one kink is enough, so in the end I think it would have to come down to being a check for a [[class]] of HTML***Element... that doesn't work in IE, so I think we're back to a naive nodeType check like suggested initially.

I'm hoping that some better solution will present itself but it certainly doesn't seem to be.

@inossidabile
Copy link

Do you need a real-life example of an entity that could potentially have the nodeType attribute being not a DOM node? Do you really think this is so impossible? ;)

@andrewplummer
Copy link
Owner

No I meant the other one. nodeType I get, which is why I'm not excited at all about the idea.

@dlee
Copy link
Author

dlee commented Aug 13, 2012

I think the best way is to add versions of the methods that use === for equality checking, or have the methods accept a flag parameter that makes the method use === for equality checking. This will resolve other issues (including the array of functions bug) and give developers the ability to choose what they want.

If you go this route, we should add a quip in the documentation for the original methods stating that they won't work as expected for DOM elements.

@andrewplummer
Copy link
Owner

So that basically leaves us back where we started, where array finding methods are working like expected, subtract, union, intersect are not, and unique can be tooled to work, but is a bit awkward.

@dlee
Copy link
Author

dlee commented Aug 14, 2012

You can just document that subtract, union, intersect, and unique (stringify versions) are only meant for arrays with elements of basic Javascript data types.

For arrays containing DOM elements, functions, and other "exotic" data types, developers should use the versions of methods that have === equality semantics.

@ghost
Copy link

ghost commented Aug 14, 2012

For performant functions that need to operate on unique object identity during array iteration you can use temporary "tag" properties during the operation and remove them before returning control to other code (as a fallback for when Map/WeakMap/Set aren't available). An example of using this technique is shown here for Array unique: http://bbenvie.com/articles/2012-06-10/Array-prototype-unique-in-O-n-time-complexity

@andrewplummer
Copy link
Owner

@Benvie Thanks for the input ... I had a read over both those.

@andrewplummer
Copy link
Owner

Note that Issue #183 is tangentially related to this issue... Functions, which need to be equal by reference only were not being properly handled, so this needed to change. It is now, and this effectively lays the groundwork for something to be done here too, but that doesn't mean I've come to any harder conclusions about this yet.

I do want to say this, however... The idea of "fuzzy matching" (in the case of array matching methods) or "equal by value" (in the case of subtract, intersect, union... they're essentially the same mechanism) is, in the end, something that should be opted into. In other words elements in an array that can be properly matched by value may do so, but only if it can be properly determined that such a match has significance. If not, they should be matched by reference. In a world of pure Javascript objects, this ONLY means functions, as any other native object can be meaningfully matched by value. However, host objects throw a whole new dynamic into the mix as they also cannot be meaningfully matched by value (for a lot of reasons, not just one, some of which are above, but I won't go into further here).

So there is a real sense in which this mechanism is in fact partially broken as it is now, and even though it took host objects to bring this to light, I'm trying hard not to conflate fixing the underlying issue with making special exceptions for the sake of host objects. Now I realize I've been implying just that (making special exceptions above), but I most definitely do NOT want to pursue that, and also I've since been starting to see the problem more clearly.

So, what elements can be matched by value? Well, right now, it's essentially opting out functions, so what we have is:

Things that match by reference
  • Functions
Things that match by value
  • Everything else

What I'm starting to realize is that this approach is flawed. Host objects illustrate this, but they are not necessarily the only objects that have no meaning in comparing by value. Other objects can follow this pattern as well. I think a better definition would be:

Things that match by value
  • Javascript primitives and their object counterparts (string, number, boolean)
  • null and undefined
  • Dates, which have an intrinsic comprable value
  • Regexes (this one is actually arguable I think, but assume it for now)
  • Arrays by way of comparing each element
  • Objects by way of comparing each property
Things that match by reference
  • Everything else

Note the exclusion of functions here. Note also that we are now essentially defining things that are safe to compare by value rather than the other way around and assuming that everything is. Already this seems more appropriate to me. So the big question is what about the last one... what is an "object"? Anything that is typeof === "object"? Of course that could be one definition but I don't think it's the correct one. So what if we are more strict in our definition and say that an "object" here is anything of "class" [object Object]? Immediately we can see that this will be able to handle object literals, which is a big part of our goal, as such values like JSON objects can be compared by value (through iteration). It will also mean that instances including those with nested inheritance chains will also match their public properties (and by "public" here I mean any properties on the instances that are not hidden in a closure). So essentially any object that is defined and under our control, including both object literals and instances of classes will be covered by this. But also notice what is not... Any DOM elements reporting [object HTML***Element] will not fall into this category, and so instead will default to matching by reference... and this is exactly the behavior we want. Note finally that even if a user-defined object comes along and overloads the toString method, it will not matter here as we are doing an internal [[class]] check (this is distinctly different from the other solution above).

To me, this is how I am viewing the problem now. What I want to get across is not that this is the "only right way" but instead that even if we leave this issue alone, there is already an assumption being made in the logic that I think is fundamentally flawed. It is the assumption that "almost everything can be matched by value except for a couple things which are matched by reference". This assumption was made because working only with native JS objects it seems this way, but host objects pretty clearly reveal that this is not in fact the case. Instead, it's more like this: "certain Javascript data types have values that can be meaningfully compared, but there is no guarantee that this will always be the case".

Seen on this level, it has nothing to do with making exceptions for host objects that may have unintended behavior, but instead directly addressing the flaw that is currently causing unintended behavior.

@dlee
Copy link
Author

dlee commented Aug 14, 2012

Bravo. That's exactly how I view it.

@inossidabile
Copy link

@andrewplummer makes sense. How are you going to distract host objects from a simple objects?

@dlee
Copy link
Author

dlee commented Aug 15, 2012

Here's another thing to consider: host objects nested under JS objects and arrays.

x = document.body.children[0]
y = document.body.children[1]

[{e: x}, {e: y}].unique()
# what should this return?

[[x], [y]].subtract [[x]]
# what should this return?

@andrewplummer
Copy link
Owner

Well that's the thing... by opting in [[object Object]], host objects are implicitly excluded (not explicitly).

........................................................sort of. So, here is where I want to draw a line because I want what I said in the last comment to stand on it's own. It represents an ideal situation and is how things SHOULD be defined, and is what we get from ES5 standards.

Ok, so here's a line:


Everything below this line is NOT to be considered part of the above comment :)

SO... of course, as always, the problem is IE. In <= IE8, DOM elements are also of class [object Object]. So, fuck... I know, fuck. Do we do some sort of workaround for IE? nodeType? toString check? this (and not the above) is where Pandora's box could be opened with special case handling. Again I'm very much against making special checks here.

However here is where I think there is a chance we can get really, really lucky. Because now, if we follow the above logic, instead of performing negative checks, we are performing positive checks about "what constitutes an object". So, class [object Object] is one gate. This will always be true for any user defined objects, literals, instances, or otherwise. Then the other one that I propose is 'hasOwnProperty' in obj. This will essentially perform a check to see if the object inherits from Object. This could be a handful of other methods such as toString, valueOf, etc. that exist on Object.prototype, but hasOwnProperty seems to me to be the most explicit.

Now here's where it gets interesting. By using the in keyword, we are checking if hasOwnProperty is defined on an object all the way up the inheritance chain. If you were to perform delete obj['hasOwnProperty'] it would not affect this check, as hasOwnProperty is still defined on Object.prototype. You can mask the property like this obj['hasOwnProperty'] = null, but that still would not affect this check as the property is still defined. So there are essentially only 2 conditions in which this check would return false... 1 is if the property were explicitly deleted on Object.prototype itself (i.e. delete Object.prototype.hasOwnProperty), or if the object does not inherit from Object. And guess what objects don't inherit from Object? That's right... DOMElements in <= IE8.

So it appears (fingers crossed) that this can be done. Elements in IE do not inherit from Object, and in ES5 environments they do, but they are not of class [object Object]. This will effectively exclude all elements from being matched by value, which will instead tell them to "match by reference". And in the end we only need to do a couple checks that are essentially saying "in order to allow an equality-by-value check, first make sure that this is a TRUE object". And unless I hit another snag, ANY user-defined object (meaning not host objects) will pass this check with ONE caveat, which is you probably should not delete members of Object.prototype :)

@andrewplummer
Copy link
Owner

@dlee You just had to go and point that out didn't you? :) That's a good point and it's actually an issue with functions now... but it's going to have it's own trickiness, so I think let's leave it out of this discussion for now.

@andrewplummer
Copy link
Owner

@dlee moved that discussion over to #186

@dlee
Copy link
Author

dlee commented Sep 5, 2012

BTW, this is what zepto does: https://github.com/madrobby/zepto/blob/master/src/zepto.js#L51

@dlee
Copy link
Author

dlee commented Sep 5, 2012

Actually, my guess is that the __proto__ check doesn't work in IE, as zepto is explicitly not supporting IE.

@andrewplummer
Copy link
Owner

Okay this fix is now released in 1.3.1.

I have a mammoth amount of unit tests now asserting on this and everything seems kosher.
To summarize the above I am NOT doing hacky, oddball things like checking nodeType or __proto__ (which you're right, would not work in IE). Instead I am have flipped the logic until now and am now white listing things that are safe to be matched by value. This includes all Javascript native types except functions, notably including objects and arrays, then the final hasOwnProperty check is the ace in the hole that effectively excludes host objects in IE (where they are already excluded in es5 browsers by not being [object Object]).

Note that Sugar is a bit special here in only needing to make a split between what it matches by value and what it matches by reference. Most other libs like jQuery, etc., have a harder requirement to match DOM Elements explicitly (and in <= IE8, too), which is much trickier (or basically impossible without nodeType, etc). The point is Sugar is not pulling off the impossible here... it simply lucked out as it has different requirements that don't parallel libs that are aware of the DOM.

Finally just a note, that Zepto implementation uses instanceof, which will fail across iframes. Heads up :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants