Extending native JavaScript objects - is it evil? SugarJS manifest

Original author: Andrew Plummer
SugarJS logoIn the comments to the post about Underscore / Lo-Dash, I mentioned that among the libraries that extend the standard JavaScript library, I prefer SugarJS, which, unlike most analogues, works through the extension of native objects.

This sparked a heated discussion about whether native objects could be extended. I was very surprised that almost all those who spoke spoke out against.

This prompted me to translate the SugarJS manifest on this issue. Apparently, the author of this library had to hear such attacks very often. Therefore, he very carefully and fairly openly commented on each of them.

This parses JavaScript pitfalls that are notorious and not very well-known, and offer protection methods. Therefore, I think that the article will be interesting and useful to any JS developer, regardless of his attitude to the problem of expanding native objects.

I give the floor to Andrew Plummer.



So, Sugar is a library that modifies native JavaScript objects. Wait, isn't it evil? “You ask,” have you not learned a lesson from the bitter experience of Prototype?

There are many misconceptions about this. Sugar avoids the pitfalls that Prototype stumbled upon, and is fundamentally different in nature. However, this choice is not without consequences. The potential problems caused by the change of native objects are discussed below, and Sugar's position regarding each of them is described:
  1. Modification of environment objects
  2. Functions as Enumerated Properties
  3. Property Override
  4. Conflicts in the global namespace
  5. Assumptions about the lack of properties
  6. Compliance

1. Modification of environmental objects


Problem:


The term "host objects" means JavaScript objects provided by the environment in which the code is executed. Examples of host objects: Event, HTMLElement, XMLHttpRequest. Unlike native JavaScript objects that strictly follow specifications, environment objects can change at the discretion of browser developers, and their implementation in different browsers may vary.

Without going into details, if you modify environment objects, your code may be error prone, it may slow down and be vulnerable to future changes in the environment.

Sugar Position:


Sugar only works with native JavaScript objects. Environmental objects are uninteresting to him (or, more precisely, unknown). This path is chosen not only to avoid problems with host objects, but also to make the library accessible to a large number of JavaScript environments, including those working outside the browser.

From the translator: here is the Sugar module in the Node repository.

2. Functions as Enumerated Properties


Problem:


In browsers that do not follow modern specifications, defining a new property makes it enumerable. When the loop traverses the properties of an object, the new property will be affected along with properties containing data.

Details
By default, when an object defines a new property, it becomes enumerable. Thus, we store data in objects and go through them in a loop:

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

If we assign a function as a new property (or, to use the OOP language, add a method to an object), this function will also become enumerated:

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

As a result, going around the properties of an object with a loop will lead to unforeseen results, but we do not want this at all. Fortunately, with a slightly different syntax, we can define non-enumerable methods:

Object.defineProperty(Object.prototype, 'getName', {
  value: function() {
    return this.name;
  },
  enumerable: false
});
for(var key in {}) {
  console.log(key);
}
// => (пусто)

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

So, with enumeration of properties of ordinary objects figured out, but what about arrays? Usually, a regular loop forwith a counter is used to bypass the values ​​of arrays .

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

As you can see, problems with property enumeration can be avoided by simply turning the counter. If you bypass the object using for..in, the enumerated properties will fall into the loop:

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, when accessing the properties of objects by property names (and by the values ​​of arrays by index numbers) for..in, the method should be used in view loops hasOwnProperty. This will exclude properties that do not directly belong to the object, but are inherited through the 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 most common examples of good JavaScript practices. Always use it when referring to object properties by property names.

From the interpreter:

The author does not mention that there is another workaround arrays: Array.prototype.forEach. As a result of a quick search, I found a polyfill from the Mozilla Developer Network, which, according to them, algorithmically reproduces the specification (and, as you can see from the following link, it has the same forEachperformance as the native one ). The polyfill code uses the first (safe) way to traverse the array. At the same time, it is known that it is forEachnoticeably slower than a simple cycle forwith a counter, apparently, due to additional checks.

forEachAvailable in all modern mobile and desktop browsers. Not available in IE8 and below.

Sugar Position:


Sugar makes its methods non-enumerable whenever possible, that is, in all modern browsers. However, until IE8 disappears completely, you should always keep this issue in mind. Its root lies in looping around properties, and we must separately consider two main types of objects that can be looped around: ordinary objects and arrays.

Due to this problem (and also due to the problem of overriding properties) Sugar does not modify Object.prototype, as is done in the examples above. This means that using loops for..inon regular JavaScript objects will never cause unknown properties to get into the loop because they are not there.

With arrays, the situation is somewhat more complicated. The standard way to traverse arrays is to simply loopforwhich, at each iteration, increments the counter by one and uses it as the name of the property. This method is safe, and the problem also does not occur. It is for..inalso possible to loop around an array with a loop , but this is not considered good practice. If you decide to use this approach, always use the method hasOwnPropertyto check if the properties belong directly to the object (see the last example in the drop-down above).

It turns out that loop traversal of the array for..inand the lack of verification hasOwnPropertyis bad practice inside bad practice. If such code is executed in an outdated browser (IE8 and lower), all the properties of objects, including Sugar methods, will crawl out, so it is important to note that the problem exists. If your project breaks down when Sugar is included in it, the first thing you should check is whether you are going to bypass the properties of objects in loops correctly. It is also worth noting that this problem is not a problem of Sugar alone, but is the case for all libraries that provide polyfills for array methods.

Conclusion. If you cannot rewrite the problematic code for traversing arrays, and support for IE8 and lower is important to you, then you cannot use the Array package of the Sugar library. Build your Sugar assembly by excluding this package.

3. Overriding properties


Problem:


In JavaScript, almost every entity is an object, which means it can have properties in the form of key-value pairs. In JavaScript, “hashes” (they are hash tables, dictionaries, associative arrays) are ordinary objects, and “methods” are just functions assigned to the properties of objects instead of data. For better or worse, any method declared for an object (directly or further along the prototype chain) is also a property, and it is accessed in the same way as for data.

The problem is becoming apparent. For example, if you define a method for all objects count, and then write data to a property with the same name for some object, the method will be unavailable.

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

The property countdirectly defined for the object obscures the method of the same name, which lies further down the prototype chain (in the original, it “casts a shadow” - is “shadowing”). As a result, it becomes impossible to call a method for this object.

Sugar Position:


Along with the problem of enumerated properties, this is the main reason why Sugar does not modify Object.prototype. Even if you know in advance what methods of objects you will use and decide to avoid using the properties of the same name, your code will still be vulnerable, and debugging overridden properties is not a pleasant task.

Instead, Sugar prefers to represent all methods for simple objects as static methods of a class Object. Until JavaScript makes a distinction between properties and methods, this approach will not change.

From the translator:

If you wish, you can transfer Sugar methods for working with ordinary objects to the properties of a specific object. This is done using Object.extended():

var
  foo = {foo: 'foo'},
  bar = {bar: 'bar'};
foo = Object.extended(foo);
foo.merge(bar);
console.log(foo);
// => {foo: 'foo', bar: 'bar'}

4. Conflicts in the global namespace


Problem:


If you exist in a global namespace, your main source of stress is the concern that you will be redefined. When writing code, there is always a risk of how to break other people's methods, and the fact that someone will break yours.

Sugar Position:


First of all, it is important to pinpoint the essence of this problem: it is a matter of awareness. If you are the only developer in the project, the modification of prototypes carries minimal danger, because you know what you are modifying and how. If you work in a team, then you may not be aware of everything.

For example, if the developers Vasya and Petya define two methods in the same prototype that do the same thing but have different names, then they just work inconsistently, but nothing criminal. If they define two methods that perform different tasks, but have the same name, then they will break the project.

Sugar’s value is, among other things, that it provides a single, canonical API whose sole purpose is to add small helper methods to prototypes. Ideally, this task should be trusted only to one library (whether it be Sugar or some other). To introduce new players to the field of the global namespace that you are new to and whose tasks are less obvious means increasing risk. This, of course, does not mean that you will immediately run into a problem. The degree of risk needs to be correlated with the degree of your awareness.

Libraries, plugins and other middlemware should not use Sugarfor the same reason. Modification of global objects must be a conscious decision by the end user. If the author of the library still decides to use Sugar, he should inform his users about this in a very visible place.

From a translator: I believe that any library should strive to have as few dependencies as possible, especially such optional ones as Sugar, Underscore, and similar libraries. They do nothing that cannot be rewritten in pure JavaScript. Abuse of this rule by the authors of libraries can lead to the fact that your project will have a mess of dependencies with duplicate and completely redundant functionality: Lazy.js, Underscore, Lo-Dash, wu.js, Sugar, Linq.js, JSLINQ, From .js, IxJS, Boiler.js, sloth.js, MooTools ... So the recommendation “do not use Sugar in middleware” is also valid for other libraries.

5. Assumptions about the lack of properties


Problem:


As dangerous are conflicts in the global namespace, assumptions about what is contained (or what is not) in the global namespace are just as harmful.

Imagine that you have a function that can take an argument of two types: a string and an object. If the function passed an object, it must extract the string from the specific property that the object has. Therefore, you check to see if the argument has this property:

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

This deceptively simple code makes the implicit assumption - that the property firstwill never be defined anywhere in the prototype chain of the object (even if it is a string). Of course, nobody will give you guarantees for this, because Object.prototypethey String.prototypeare global objects, and everyone can change them.

Even if you are opposed to changing native objects, you cannot afford to write code that, as in the example above, makes assumptions about the contents of the global namespace. Such code is vulnerable and can lead to problems.

Sugar Position:


Correcting the code from the last example is not difficult at all. You are already familiar with the solution:

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

Now the function will only check properties declared directly for the passed object, and not for all properties in its prototype chain. One could go further and prevent the transfer of various data types, but even such a simple check is enough to avoid problems associated with changes in the global namespace.

In its lifetime, Sugar provoked this problem in the code of large libraries twice: jquery # 1140 and mongoose # 482 . The culprits in both cases were the unsuccessfully named Sugar methods. We gladly renamed them, which is why we solved the problem. In addition, one of the libraries (jQuery) worked on the problem with us in order to eliminate a flaw on its side.

Sugar tries to work very accurately in the global scope, but there is no way to do without cooperation between the authors of the libraries. The root of the problem is the nature of JavaScript itself, which makes no distinction between properties and methods.

6. Compliance with specification


Problem:


The ECMAScript specification is a standard that defines the behavior of native methods. JavaScript runtime developers want their native methods to be accurate and up to date. Therefore, two things are important: that our methods always act in accordance with the specification and that possible changes in the specification in the future do not cause regressions.

Sugar Position:


From the very beginning, we developed Sugar, striving not only to comply with the specification, but also to evolve with it.

In the ES5 package, Sugar offers the polyfill methods described in the current specification. Of course, if there is a native implementation of methods in the execution environment, native methods are used. Sugar has an extensive set of tests, thanks to which you can be sure that all polyfill exactly match the specification. In addition, if you wish, you can refuse the ES5 package and use any other ES5 polyfill.

To comply with the specification means to adapt to changes in it. It is Sugar's responsibility to always be at the forefront of standards. The sooner you start meeting the new draft specification, the more painless it will be to switch to it in the future. Starting with version 1.4, Sugar is aligned with the ECMAScript 6 standard (and looks at 7, which is in the very early stages of development). As the specification changes, Sugar will continue to adjust, avoiding conflicts and striving to strike a balance between practicality and compliance with the native implementation.

Of course, adaptation is good for those users who are willing to regularly update dependencies. But how will the projects sitting on the old version of Sugar behave when the environment switches to the next version of the specification? Imagine a situation: the browsers of visitors to your site are updated, and the site breaks in them. Sugar recently made a difficult decision to redefine methods that are not explicitly described in the specification. Now no if (!Object.prototype.foo) Object.prototype.foo = function(){};, all methods that are absent in ECMAScript are redefined unconditionally.

Although it may seem the other way around, this decision is aimed at improving site support. Even if, as a result of updating the specification, the native methods change and conflict with Sugar, Sugar will override them. Consequently, the methods will continue to work as before - until you get your hands on updating the site. But as already mentioned, we strive to go far ahead of the specification, minimizing this need.

TL / DR


Let's walk through all the problems and the risks associated with them:
  1. Problem: Modification of environmental objects
    Risk: none.
  2. Problem: Functions as enumerated properties
    Risk: minimal. There is no risk when traversing ordinary objects, as well as traversing arrays in a safe way. When traversing arrays in an unsafe way, there will be problems in IE8 and below.
  3. Problem: Overriding properties
    Risk: none.
  4. Problem: Conflicts in the global namespace
    Risk: minimal, but grows inversely with your awareness of what is happening in the global namespace of your project. Ideally, a project should not contain more than one library like Sugar, and its use should be documented. Do not use Sugar if you are writing a library yourself; in a pinch, report your Sugar application to your users as loudly as possible.
  5. Problem: Assumptions regarding the lack of properties
    Risk: minimal. The problem arose twice in the Sugar story, both cases were quickly resolved.
  6. Problem: Compliance with specification
    Risk: very slight. Sugar tries to be as careful with modifying native objects as possible. But is that enough? The answer to this question depends on the beliefs of the user and the structure of the project, and also changes over time (for the better).


We made these conclusions ourselves based on our experience of using Sugar in the real world and user feedback. If they are in doubt, we have covered each point in the article in detail - in the hope that this will help you make your own conclusions about whether Sugar is suitable for your project.

From translator


Performance SugarJS


Being more convenient to use, Sugar noticeably loses Lo-Dash in performance. However, the performance issue, in my opinion, matters only in the processing of any large amounts of data. If you work with the frontend, then you will not find any difference in the speed of these libraries.

For those for whom performance is critical, I recommend LazyJS . In cases when, after traversing the properties of an object / array, it is not necessary to return a new object with all the properties, LazyJS outperforms Lo-Dash in performance. In compound operations, the gap becomes significant. For example, on operationsmap -> filterLazyJS is five times faster than Lo-Dash, and fifteen than SugarJS. If you need to not just go through the property values, but to assemble a new object / array, then LazyJS loses its advantage. StreetStrider tells you that lazy computing on chains is planned in LoDash version 3.

Here, in your browser you can directly compare the performance of ten similar libraries on a variety of typical operations.

The convenience we have lost


You may be wondering how elegantly the problem of expanding native objects was solved in Ruby. There, a refinements mechanism was proposed, which in Ruby 2.1 came out of experimental status.

Suppose the developer Vasya, who writes the Vasya library, is not shy about making decoy patches: he (re) defines methods of standard objects from his library using the construction refine.

refine String do
  def petrovich
    "Petrovich says: " + self
  end
end

The developer Petya sculpts one of the parts of a large project, which many programmers are working on. When Petya connects the Vasya library, Vasins seminal patches do not apply to the entire project and do not interfere with the rest of the coders. When connecting the library, redefinition of native objects does not occur at all.

To use the new methods, Petya in his code indicates which decoy patches from which libraries he needs:

using Vasya
using HollowbodySixString1957

As a result of the redefinition of global objects made from these libraries, they are applied only to the class, module, or source code file in which Petya asked for it.

UPD1: Why is all this necessary?


From the comments it became clear that this is obvious to me alone. I make my answer from the comments.

This is probably more obvious for those who started seriously programming with Ruby. As they say, you quickly get used to the good: a rich and very functional standard library, a natural way for a method to call methods in a dynamic language, the ability to chain methods.

When you see this code (abstract example):

var result = Math.floor( MyArray.last( arr ) );
if (debug) console.log( "result:", result );
return result;

... instead of this:

return arr.last().floor().debug();

... then it becomes somehow, you know, dreary.

Only registered users can participate in the survey. Please come in.

Is extension of native JS objects permissible?

  • 38% Unacceptable, with the exception of polyfills. 126
  • 16.3%Допустимо, но только при тщательном документировании, покрытии всего кода тестами и осведомительной работе с коллективом.54
  • 29.6%Допустимо, достаточно здравого смысла. Проблемы надо решать по мере их поступления, а не прятать голову в песок.98
  • 4.2%Ничего не думаю по этому поводу.14
  • 11.7%Я НЛО, похитьте меня кто-нибудь!39

Как изменилось ваше отношение к вопросу после прочтения статьи?

  • 36.7%Как был против, так и остался.116
  • 27.5%Как не был против, так и остался.87
  • 6%Оказывается, не так страшен черт.19
  • 7.2% I did not think that expanding native objects is so dangerous. 23
  • 8.2% I don’t think anything about this. 26
  • 14.2% pilot.jpg 45

Also popular now: