Supporting native object modification is a stance that is contentious, and Sugar is aware that many developers are critical of it. However, whatever else can be said about it, one thing it is not is simple. Many misconceptions exist around the issue and the details are important. This page lists major pitfalls that became issues for other libraries and the ways in which Sugar avoids them. In the end, the intent of making this feature opt-in is to allow the community to evaluate it on its own merit by providing an alternative, and no longer allow it to be a deal breaker to using the Sugar library as a whole.
When never to modify natives
Regardless of other details listed here, one situation in which modifying
natives should never be considered appropriate is when developing another
library, plugin, or other form of middleware. In short, the decision to have a
modified global state is one that the end user or team should be well aware of.
Failing to do this leads to issues that can be difficult to track down. Also,
issues with versioning can also lead to collisions and other bugs as well. If
there is any chance that your code may be consumed later by a third party, it is
strongly recommended not to use
extend. Fortunately, chainables
objects in a similar manner without having to extend.
- Host Objects
- Enumerable Properties
- Objects as Data
- Global Collisions
- Global Assumptions
- Aligning with the Spec
"host environment", as opposed to native components of the language itself.
XMLHttpRequest are all examples of host objects.
While native objects follow a strict specification, host objects are subject to
change by browser vendors at any time. To spare the
modifying host objects is error-prone, non-performant, and not future-proof.
Many of the issues encountered by Prototype.js were dealing with host objects.
or square bracket operators are enumerable, meaning they will appear in a
for..in loop. Modern browsers are capable of defining non-enumerable
Object.defineProperty, however this feature does not exist
in older browsers, most notably IE8 and below.
non-enumerable, as having them appear in loops would cause unexpected behavior.
When writing loops yourself, it is also important to use the right kind of loop.
Arrays should always be looped over using a
for loop (never
they can be seen as collections of numeric indexes, and so should not be looping
over other, non-numeric properties. Similarly, when looping over data in an
for..in, in 99% of cases a
hasOwnProperty check should be
included to prevent inherited properties from being looped over as well.
While this is fine for code that you control, unfortunately we can't count on other developers to also follow these good practices. What this effectively means is that in older browsers like IE8 (where properties can only be defined as enumerable), methods on native prototypes may be exposed if the above practices for proper looping aren't followed.
To look at the issues separately, by far the bigger threat is objects, as
for..in loops are their standard means of iteration. And although many
developers are aware of
hasOwnProperty, when compared to
for loops for
arrays, it is not uncommon to see code in the wild that does not know of or
has forgotten to use this check.
This is one of the main reasons that Sugar will not extend
when using the
extend method. Although this ability exists, it is hidden
behind a flag that carries the appropriate warnings and is generally not
recommended. See Object Methods for more.
Arrays are slightly more subtle. Due to the ubiquity of the
for loop for
Arrays and the utility that Array methods offer, Sugar does allow these objects
to be extended. To reiterate, this is only an issue in old browsers (that
do not have proper ES5 support). If these browsers are not being targeted,
then the issue does not exist. If older browser support is required and issues
are being encountered, v2.0 now allows this issue to be sidestepped by
excluding Arrays when using
Objects as Data
The difficulty in trying to define methods on objects should then quickly
become apparent. If a method like "count" is defined on
will appear to exist as a property for all objects. To check for the existence
of a "count" property in an object, it is then no longer sufficient to use the
.count syntax as this may return the method instead. Conversely,
the "count" method will become inaccessible if the same property is defined on
the object itself. This effect is called "property shadowing".
This is a major reason why Sugar does not modify
track of method names and foregoing standard operators to check for the
existence of a property is simply too heavy a price and undermines the utility
that Sugar is trying to provide.
Fortunately, as of v2.0, object chainables are now provided that fill this
gap quite nicely. While chainables are in general useful, especially if native
extension is not desired, Object chainables are especially useful as they are
tooled specifically at working with objects as data stores. First, they have
all object instance methods mapped to them, and so can be worked with in the
same manner as extending
Object.prototype would allow. They also provide some
useful methods like
has, which by default only operate on
non-inherited properties, and also provide special syntax like the ability to
deeply inspect object properties. Making good use of these object types should
hopefully alleviate some of the pain felt when working with objects as data
The most basic problem with existing in the global namespace is worrying about naming collisions. As mentioned above, the decision to extend natives is one that should be made by the end user, and not by middleware or libraries. Ensuring this avoids global collisions and makes sure that the global state is modified only a single time, by someone who is aware of the change.
In addition, it is Sugar's stance that only a single library be entrusted with
the ability to modify natives, whether this be Sugar or something else. Adding
others into the global namespace only increases the chance of collisions
becoming and issue. If you are working with other libraries that modify natives,
it is recommended to avoid using
extend with Sugar.
If creating colliding properties in the global namespace is one side of a coin, then making assumptions about the global namespace is the other. Take the following example:
Although this kind of code is common and seemingly innocuous, it actually can
be problematic. When it checks for the property
foo, it is actually checking
obj, but every object in the prototype chain of
obj as well. This
may be intended, but in most cases it is making an assumption that a property
of the same name will not exist anywhere in the prototype chain. If the property
foo were defined on
Object.prototype, it would give a false positive here
and likely cause issues. Note however, that the issue is not limited to the
global scope. If
obj is an instance of a user-defined class, it may have
properties in its prototype chain as well. For this code to fully match its
intent, it should ideally be using
hasOwnProperty instead of the dot operator
when performing this property check.
Although this is easy to say, using
hasOwnProperty to check every property is
clunky and awkward, which is why code like the above is far more common. To say
that the above example is "incorrect" would be a stretch. More simply,
a situation where we are forced to make this assumption, and unless that changes
this issue is likely to persist.
Sugar's choice not to modify
Object.prototype comes largely from this point.
Although this greatly mitigates the problem, it does not solve it completely.
Apart from the issue with user-defined instances as described above, if
is of a different type, for example a primitive such as a string, it may have
methods defined on it. Checking for a property on a primitive is rare, and
should be considered an anti-pattern, as properties cannot be set on primitives
and attempting to do so will even throw an error in strict mode. In the end,
performing property checks on arbitrary object types leads to unnecessary and
brittle code, and should be avoided.
As with enumerable properties this is easy enough to avoid in code you control,
but may become an issue when third party code makes the kind of assumptions
described above. In this case, Sugar's
extend method provides the ability to
control which methods get extended. If the offending method can be pinpointed,
it can be excluded. If not, it is recommended to instead use an opt-in strategy
and extend methods only as needed.
Aligning with the Spec
In addition to avoiding global collisions with other libraries, any library that deals with natives also has to contend with existing native methods as well. Sugar has made a continually increasing effort here to not only play well with the ECMAScript spec, but also provide robust polyfills that fix browser support when it is missing or broken. It also aligns many of its naming and argument conventions for other methods in a way that is intuitive for those familiar with browser native methods.
Part of being compliant means adapting to changes, which is a responsibility Sugar takes upon itself as well. In addition to keeping the library aligned with the spec as it changes, this also means ensuring that browser updates don't affect behavior. When extending, Sugar methods that are not already aligned with browser native ones will always take priority and overwrite any existing methods of the same name. At first this seems counterintuitive, however doing this ensures that changes in underlying browser behavior won't affect an app that is depending on Sugar, and guarantees that apps which cannot be updated will not break.
Of the six issues listed above, two have the potential to affect Sugar when
extending natives – enumerable properties and
global assumptions. If support for IE8 and below is not
as data stores means that bad assumptions about the global scope will always
have the potential to be affected when extending. This in turn means that
the ability to extend natives with 100% safety will likely never exist. However,
Sugar's decision to avoid
Object.prototype greatly mitigates the danger here.
Additionally, the ability to have fine-grained control over which methods are
extended means that issues can be worked around if they arise.
Ultimately, the question of whether all of this means that extending natives is "safe enough" does not depend on Sugar alone but also on the requirements of the application using it, and hopefully the issues listed here can help developers come to an informed decision that they are comfortable with.