Extending natives

Sugar is a Javascript utility library that deals with native objects. One of
its core features is the ability to extend natives directly, sometimes referred
to as "monkey-patching". In previous versions this behavior was the default,
with no way to be opted out of. v2.0 changes this to make native augmentation
opt-in, and adds a high degree of control over this process.

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
are now provided instead as a middle ground to allow working with Javascript
objects in a similar manner without having to extend.

Issues:

Host Objects

The term "host object" refers to Javascript objects that are provided by the
"host environment", as opposed to native components of the language itself.
Event, HTMLElement, and 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
gory details,
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.

In contrast, Sugar deals with native Javascript objects only. It is not
interested in (or even aware of) host objects. This choice is not only to avoid
the problems with host objects, but also to make itself accessible to a variety
of Javascript environments, including those outside the browser.

Enumerable Properties

By default, properties defined on Javascript objects using the standard dot
or square bracket operators are enumerable, meaning they will appear in a
for..in loop. Modern browsers are capable of defining non-enumerable
properties using Object.defineProperty, however this feature does not exist
in older browsers, most notably IE8 and below.

When defining methods on Javascript natives, it is important that they are
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 for..in), as
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
object using 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 Object.prototype
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 extend.

Sugar.extend({
except: Array
});

Objects as Data

In Javascript, nearly everything is an "object", which means that it can have
properties defined on it using the dot or square bracket operators. There is no
concept in Javascript of a "hash" (a.k.a. "dictionary", "data map",
"key/value store", "associative array", etc.), as these roles are typically
fulfilled simply by using plain objects. For better or for worse, and notably
different from many other languages, methods are defined on objects in the same
way, and can be accessed, overwritten, or deleted just like any other property.
Also of note is that accessing a property (with the same dot or square bracket
syntax) checks not only properties on the object, but also inherited properties
in the prototype chain as well.

The difficulty in trying to define methods on objects should then quickly
become apparent. If a method like "count" is defined on Object.prototype, it
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
standard .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 Object.prototype. Keeping
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 get and 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
in Javascript. For more see chainables.

var data = new Sugar.Object(usersByName);
// Can get deep properties with the . syntax
data.get('Harry.profile.hobbies').raw;

var data = new Sugar.Object(usersByName);
// Raw data can still be accessed with .raw
data.raw['Harry'];

Global Collisions

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.

Global Assumptions

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:

function getFoo(obj) {
if (obj.foo) {
return obj.foo;
}
}

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
not only 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,
Javascript's own use of objects as data stores has lead to
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 obj
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.

Conclusions

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
required, then this drops to one. Unfortunately, Javascript's use of objects
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.