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.

var keys = []; for(var i in badArray) { // Uhoh, this loop exposes non-array properties! keys.push(i); } keys;
var keys = []; for (var i = 0; i < badArray.length; i++) { // Good! This loop will only hit array indexes! keys.push(i); } keys;
var keys = []; for(var key in badObject) { // Uhoh, this loop exposes inherited properties! keys.push(key); } keys;
var keys = []; for(var key in badObject) { if (!badObject.hasOwnProperty(key)) continue; // Good, this loop will only iterate over the // object's own, enumerable properties. keys.push(key); } keys;

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); // Only retrieves non-inherited properties data.get('Harry').raw;
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.