Sugar | Javascript, sweetened.

Modifying natives...isn't it evil?

So, Sugar is a library that modifies Javascript native objects. But isn't that evil? Haven't we learned our lesson from Prototype?

There are many misconceptions that exist around the issue. Sugar avoids the major pitfalls that became issues for Prototype, and in this sense it is fundamentally different. However, the choice is not without consequences. Below are the potential issues with modifying natives along with Sugar's stance on them.


  1. Modifying host objects
  2. Functions as enumerable properties
  3. Property shadowing
  4. Global namespace collisions
  5. Global namespace assumptions
  6. Following the spec
  7. Conclusions


1. Modifying host objects

The issue:

The term "host objects" 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 the different browser vendors at any time.

To spare all the details, modifying host objects is error-prone, non-performant, and not future-proof. Most of Prototype's issues came from problems with host objects.

Sugar's stance:

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.

2. Functions as enumerable properties

The issue:

Properties defined in non-standards browsers are enumerable, and can be exposed in loops. Show Details

By default, a property defined on an object in Javascript is "enumerable". This means that it will appear when looping over the properties of that object:

var o = {};
o.name = "Harry";
for(var key in o) {
  console.log(key);
}

> name

If we define a function on a prototype in the same way, it will also be enumerable:

Object.prototype.getName = function() {
  return this.name;
};
for(var key in {}) {
  console.log(key);
}

> getName

This will cause unexpected results in loops, and is not what we want. Fortunately, with a slightly different syntax, we can define a function on an object that is not enumerable:

Object.defineProperty(Object.prototype, 'getName', {
  value: function() {
    return this.name;
  },
  enumerable: false
});
for(var key in {}) {
  console.log(key);
}

> (No output)

However, as always there is a catch. Here, the ability to define non-enumerable properties is not available in Internet Explorer 8 and below.

The problem with enumerability in standard objects should now be clear, but what about arrays? In general, arrays use a different syntax for looping:

Array.prototype.name = 'Harry';
var arr = ['a','b','c'];
for(var i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

> 'a'

> 'b'

> 'c'

By simply incrementing an index, problems with property enumerability are effectively avoided. However, if an array were to be looped over using the for..in pattern, it would expose the enumerable property:

Array.prototype.name = 'Harry';
var arr = ['a','b','c'];
for(var key in arr) {
  console.log(arr[key]);
}

> 'a'

> 'b'

> 'c'

> 'Harry'

For this reason literal values like objects and arrays should always use hasOwnProperty when using a for..in loop. This will exclude values that are not direct properties of that object (and not in the object's prototype chain):

Array.prototype.name = 'Harry';
var arr = ['a','b','c'];
for(var key in arr) {
  if(arr.hasOwnProperty(key)) {
    console.log(arr[key]);
  }
}

> 'a'

> 'b'

> 'c'

This is one of the more common examples of good practice in Javascript. Always use this check when iterating over literal values like plain objects and arrays.

Hide Details

Sugar's stance:

Sugar methods are non-enumerable whenever possible (in modern browsers). However, until IE8 goes away forever, this still needs to be considered an issue. At the core of the problem with enumerable properties is looping, so we need to look separately at the two basic objects that can be iterated over, objects and arrays.

Sugar does not modify Object.prototype as shown in the examples above for this reason, as well as that of property shadowing. This effectively means that using a for..in loop on a plain Javascript object will never result in unknown properties appearing, as there are none.

Arrays are slightly more complicated. The standard method of looping over an array is a simple for loop that increments the index (one of the first things any JS dev learns) and avoids the issue. Looping over an array using a for..in loop is possible, however not considered good practice. If used, however, for..in loops should always use hasOwnProperty to check that the properties are direct members (last example above).

So, looping over an array using for..in and also not performing a hasOwnProperty check can be considered a bad practice within a bad practice. However, were this kind of code to be encountered in a non-modern browser (IE8 and below), it would expose any enumerable properties, including Sugar methods, so it is important to note that the issue does exist. If simply including Sugar in your page is causing issues, proper usage of loops in your code is the first thing you should check for. It should also be noted here that this issue is not exclusive to Sugar but is true of any library that is providing polyfills for array methods.

Finally, if code with improper looping is causing issues, is not under your own control, and support for IE8 and below is a requirement, you may not be able to use the Array package. However, a custom build of Sugar can still be created that does not include this package.

3. Property shadowing

The issue:

In Javacript, almost everything is an object, which means it can have properties defined by key/value pairs. "Hashes" (aka "hash tables", "data maps", "key/value stores", "enumerative arrays", etc.) are simply plain objects, and "methods" are simply functions that exist on objects as properties, just like everything else. This means that, for better or worse, any method defined on an object (or in its prototype chain) is also considered a property, and is accessed in the same way.

The problem with this should be quickly apparent. If a method "count" is defined for all objects, it will become inacessible if the same property is defined directly on the object.

Object.prototype.count = function() {};
var o = { count: 18 };
o.count

> 18

The property "count" which is directly defined on the object is now "shadowing" the underlying method of the same name, which can now no longer be accessed.

Sugar's stance:

This problem, along with that of enumerable properties, is the main reason that Sugar does not modify Object.prototype. Although it's of course possible to know in advance what methods you will use on objects and avoid defining those as properties, keeping track of the names is error-prone and debugging shadowed properties can be tricky.

Sugar instead chooses to define all Object related methods as class methods on Object itself. So long as properties and methods are treated as the same in Javascript, this stance will be maintained.

4. Global namespace collisions

The issue:

The most basic problem with existing in the global namespace is worrying about naming collisions. There is always a chance of clobbering other methods or having yours clobbered by others.

Sugar's stance:

It is first important to pinpoint the exact nature of this problem. At the heart of this issue is awareness. If you are a single developer working alone, the danger of modifying prototypes is minimal, as you are aware of what has been modified and how. However if you are working in a team, this may not be the case. If developer A and B each add two different methods that do the same thing, at the least they will be working at cross-purposes. If they both add the same method that does two different things, then they are in danger of colliding.

Part of Sugar's value comes simply by providing a single, canonical API whose explicit purpose is adding small utility methods to prototypes. For this reason, ideally only one library should be entrusted to do this (whether this is Sugar or another). Adding more, less understood, and less explicit players into the global namespace can increase the chance for problems. This may be fine, but it should be considered proportionally to potential problems with awareness.

For the same reason, middleware (plugins, etc.) should avoid using Sugar, as modifying the global namespace is better left as a conscious decision for the end user. If a middleware does choose to use it, it should be very explicit about this choice.

5. Global namespace assumptions

The issue:

Just as collisions in the global namespace can lead to issues, making assumptions about the global namespace likewise leads to brittle code. Imagine that you have a function that accepts two kinds of input, strings or objects. When objects are passed, you are expecting a certain property to be defined on them, so you perform a simple check for that property:

function getName(o) {
  if(o.first) {
    return firstName;
  } else {
    return lastName;
  }
}

This deceptively simple code is implicitly making a big assumption -- that the property first will never exist anywhere on a string (including the prototype chain). Of course there are no guarantees that this is the case, as the global String.prototype object can be modified by anyone. Even if you don't agree with native augmentation, code like the above that makes such assumptions about the global namespace is brittle and can lead to issues, just as modifying the global namespace itself can.

Sugar's stance:

Fixing the above code turns out to be quite simple. Although type-checking may work as well, there is an even easier method:

function getName(o) {
  if(o.hasOwnProperty('first')) {
    return firstName;
  } else {
    return lastName;
  }
}

This modification simply ensures that only direct properties (not every property in the prototype chain) will be checked. It could be even further cleaned up to not accept multiple types of input, but this simple check will ensure that global namespace changes will not break this method.

Although rare, issues like the above have happened twice in Sugar's history. Both cases also happened to involve poorly named methods on Sugar's side which were promptly removed, effectively fixing the issue. Additionally, one library (jQuery) worked together to help resolve the original issue on their side. Sugar makes every effort to operate safely in the global namespace, but there is a certain level of cooperation required. At the core of the issue is also the nature of Javascript itself, which makes no syntactic distinction between properties and methods.

6. Following the spec

The issue:

The ECMAScript specification is a standard defining how native methods should work. It is important that they behave according to this spec, and that implementations remain both consistent and up to date. Also it is important that changes to spec do not cause further issues or regressions.

Sugar's stance:

Since its early days, Sugar has maintained an attitude of not only staying aligned with spec, but also trying to work alongside it rather than circumvent it.

Sugar's ES5 package provides polyfill (aka "shim") methods defined in the current spec, while falling back to native implementations when they exist. There is a large unit test suite that makes sure the polyfill behavior matches the spec exactly. Additionally, this package can even be removed in favor of a different one if needed.

Part of being compliant with standards also means adapting to changes in spec. It is Sugar's reponsibility to stay on top of the standards and align itself early to new spec to smooth the transition later. As of version 1.4, Sugar is aligned to the upcoming 6th edition specification, with the 7th in very early stages and still a number of years off. As the spec changes Sugar will continue to adapt, avoiding conflicts and trying to strike a balance between utility and faithfulness to native implementations.

Of course adaptation is good for those who can upgrade, but what about new spec breaking sites using older versions of Sugar? This is the worst scenario: a browser update that you have no control over breaks your site. Sugar recently came to the difficult decision to overwrite methods that are not explicitly part of the spec. Although counterintuitive, this decision is important for site maintainability. It means that even if the spec changed overnight to collide with existing Sugar methods, these methods would be overwritten and continue to work as before, until a time when the site can be updated. As described above however, staying well ahead of spec is the first priority to minimize this necessity.

7. Conclusions

First, let's review the issues and their associated risks:

Sugar is confident in being about as safe as modifying natives can get. "But is that safe enough?" — the answer to this will vary from person to person, project to project, and change as time goes on (it's getting safer).

The conclusions Sugar has drawn here are its own after dealing with real-world usage and feedback. However, the above issues are described in detail in the hope that they help you reach your own conclusions about whether Sugar is right for your project.