
In 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 caused a heated discussion about whether it is permissible to extend native objects. I was very surprised that almost everyone who spoke out opposed it.
This prompted me to translate the SugarJS manifesto on this issue. Apparently, the author of this library very often had to hear such attacks. Therefore, he very carefully and rather impartially commented on each of them.
')
This material deals with the pitfalls of JavaScript, known and not so, as well as proposed methods of protection. 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 word 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 over, and is fundamentally different in nature. However, this choice is not without consequences. Below are the potential problems caused by the change of native objects, and Sugar’s position about each of them is outlined:
- Modification of environment objects
- Functions as enumerated properties
- Property Overriding
- Conflicts in the global namespace
- Assumptions about the lack of properties
- Compliance specification
1. Modification of environment objects
Problem:
The term “environment objects” (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 comply with the specification, environment objects can vary at the discretion of browser developers, and their implementations in different browsers may differ.
Without going into details, if you modify environment objects, your code may be prone to errors, slow down and be vulnerable to future environmental changes.
Sugar Position:
Sugar works only with native JavaScript objects. The objects of the environment are not interesting for him (or, more precisely, unknown). This path was chosen not only to avoid problems with host objects, but also to make the library accessible to a large variety of JavaScript environments, including those running outside the browser.
From the translator: here
is the Sugar module in the Node repository.
2. Functions as enumerable properties
Problem:
In browsers that do not follow modern specifications, defining a new property makes it enumerable. When traversing the properties of an object, the new property will be affected along with the properties containing the data.
DetailsBy default, when a new property is defined on an object, it becomes enumerable. Thus, we store data in the objects and cycle through them:
var o = {}; o.name = "Harry"; for(var key in o) { console.log(key); }
If we assign a function as a new property (or, to put it in OOP, add a method to an object), this function will also be enumerated:
Object.prototype.getName = function() { return this.name; }; for(var key in {}) { console.log(key); }
As a result, bypassing the properties of an object with a cycle will lead to an unexpected result, but we do not want that at all. Fortunately, with the help of 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 missing in Internet Explorer 8 and below.
So, with the enumeration of the properties of ordinary objects sorted out, but what about the arrays? Usually, to bypass the values ​​of arrays use the usual
for
loop with counters.
Array.prototype.name = 'Harry'; var arr = ['a','b','c']; for(var i = 0; i < arr.length; i++) { console.log(arr[i]); }
As you can see, problems with the enumeration of properties can be avoided by simply spinning the counter. If you bypass the object with
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]); }
For this reason, when referring to the properties of objects by property names (and to the values ​​of arrays by index numbers) in loops of the form
for..in
, you should use the
hasOwnProperty
method. This will exclude properties that do not belong to the object directly, but are inherited through a chain of prototypes:
Array.prototype.name = 'Harry'; var arr = ['a','b','c']; for(var key in arr) { if(arr.hasOwnProperty(key)) { console.log(arr[key]); } }
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 translator:
The author does not mention that there is another array traversal method:
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 performance as the native
forEach
). The polyfill code uses the first (safe) way to traverse the array. At the same time, it is known that
forEach
noticeably slower than the simplest
for
loop with a counter, apparently, due to additional checks.
forEach
is available 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 finally disappears, you should always keep this problem in mind. Its root lies in traversing the properties of the loop, and we should consider separately the two main types of objects that can be traversed by the loop: ordinary objects and arrays.
Because of this problem (and also because of the problem of overriding properties), Sugar does not modify the Object.prototype, as is done in the examples above. This means that using
for..in
loops on regular JavaScript objects will never lead to unknown properties in the loop, because there are none.
With arrays, the situation is somewhat more complicated. The standard way of traversing arrays is a simple
for
loop, which during 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 also possible to bypass the array with the
for..in
loop, but this is not considered good practice. If you decide to use this approach, always use the
hasOwnProperty
method to check if the properties belong directly to the object (see the last example in the open box above).
It turns out that traversing an array with a
for..in
and the lack of checking
hasOwnProperty
is a bad practice inside a bad practice. If this code is executed in an outdated browser (IE8 and below), all the properties of objects, including Sugar methods, will come 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 need to do is check whether you bypass the properties of the objects in the loops. 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 below is important to you, then you cannot use the Suray Array package.
Build your Sugar assembly by removing this package.
3. Redefinition of 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 also hash tables, dictionaries, associative arrays) are ordinary objects, and "methods" are simply functions assigned to properties of objects instead of data. Good or bad, but any method declared for an object (directly or further along the chain of prototypes) is also a property, and it is accessed in the same way as for data.
The problem becomes apparent. For example, if the method
count
defined for all objects, and then some object is written to a property with the same name, the method will be unavailable.
Object.prototype.count = function() {}; var o = { count: 18 }; o.count
The
count
property, which is directly defined for an object, seems to obscure the eponymous method, which lies further along the prototype chain (in the original “casts a shadow” - is “shadowing”). As a result, it is impossible to call a method for this object.
Sugar Position:
Together with the problem of enumerated properties, this is the main reason why Sugar does not modify the
Object.prototype
. Even if you know in advance what methods of objects you will use and decide to avoid using properties of the same name, your code will still be vulnerable, and debugging redefined properties is not a pleasant task.
Instead, Sugar prefers to present all methods for simple objects as static methods of the
Object
class. As long as JavaScript does not make a difference between properties and methods, this approach will not change.
From the translator:
If desired, you can transfer the Sugar methods for working with ordinary objects to the properties of a specific object. This is done with
Object.extended()
:
var foo = {foo: 'foo'}, bar = {bar: 'bar'}; foo = Object.extended(foo); foo.merge(bar); console.log(foo);
4. Conflicts in the global namespace
Problem:
If you exist in the global namespace, your main source of stress is worrying about what 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 question of awareness. If you are the only developer in the project, modifying prototypes carries minimal danger, because you know what you modify and how. If you work in a team, 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 only work inconsistently, but nothing criminal. If they define two methods that perform different tasks but have the same name, they will break the project.
The value of Sugar lies in, among other things, that it provides a single, canonical API, the only task of which is to add small helper methods to the prototypes. Ideally, this task should be trusted only to one library (be it Sugar or some other). To bring new players to the field of the global namespace, with whom you are not very familiar and whose tasks are less obvious, is to increase the 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 Sugar for the same reason. Modifying global objects must be a conscious decision of the end user. If the author of the library nevertheless decides to use Sugar, he must inform his users about it in the most visible place.
From the 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 not do anything that could not be rewritten in pure JavaScript. Abuse of this rule by the authors of the libraries may 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 far as conflicts in the global namespace are dangerous, the assumptions about what is (or 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 is passed an object, it must extract a string from a specific property that the object has. Therefore, you check whether the argument has such a property:
function getName(o) { if(o.first) { return firstName; } else { return lastName; } }
This deceptively simple code makes an implicit assumption - that the
first
property will never be defined anywhere in the object's prototype chain (even if it is a string). Of course, no one will give you guarantees for this, because
Object.prototype
and
String.prototype
are 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:
Fix the code from the last example is a snap. You are already familiar with the solution:
function getName(o) { if(o.hasOwnProperty('first')) { return firstName; } else { return lastName; } }
Now the function will check only the properties declared directly for the transferred object, and not for all the properties in its prototype chain. One could go further and prohibit the transfer of various types of data, but even such a simple check is enough to avoid problems associated with changes in the global namespace.
In his lifetime, Sugar provoked this problem in the code of large libraries two times:
jquery # 1140 and
mongoose # 482 . The culprits in both cases were the unsuccessfully named methods of Sugar. We willingly renamed them, and this solved the problem. In addition, one of the libraries (jQuery) worked the problem together with us to eliminate the flaw on its side.
Sugar is trying to work very carefully in the global scope, but there is not enough 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 the specification
Problem:
The ECMAScript specification is a standard that defines the behavior of native methods. The developers of JavaScript runtimes strive to ensure that their implementation of native methods is both accurate and up to date. Therefore, two things are important: that our methods always act in accordance with the specification and that possible specification changes in the future do not cause regressions.
Sugar Position:
From the very beginning, we developed Sugar, aiming not only to conform to the specification, but also to evolve with it.
In the ES5 package, Sugar offers polyfills of the methods described in the current specification. Of course, if there is a native implementation of the methods in the execution environment, the native ones are used. Sugar has an extensive set of tests, thanks to which you can be sure that all polyfills exactly meet the specifications. In addition, if you wish, you can opt out of the ES5 package and use any other ES5 polyfill.
To conform to the specification means to adapt to the changes in it. Sugar has the responsibility to always be at the forefront of standards. The sooner you start to comply with the new draft draft specification, the more painless will be the transition to it in the future. Starting with version 1.4, Sugar equals ECMAScript 6 standard (and looks at 7, which is found at the earliest stages of development). As the specification changes, Sugar will continue to adapt, 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 the situation: the browsers of visitors to your site are updated, and the site breaks down 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 missing in ECMAScript are redefined unconditionally.
Although it may seem the opposite, but this solution is aimed at improving the support of sites. Even if, as a result of updating the specification, native methods change and conflict with Sugar, Sugar will override them. Consequently, the methods will continue to work as before - until your hands reach the site to update. But as already mentioned, we strive to go far ahead of the specification, minimizing this need.
Tl / dr
Let's go through all the problems and related risks:
- Problem: Modifying Environment Objects
Risk: absent. - Problem: Functions as enumerable properties
Risk: minimal. Bypassing normal objects there is no risk, as well as by traversing arrays in a safe way. When traversing arrays in an unsafe way there will be problems in IE8 and below. - Problem: Overriding Properties
Risk: absent. - 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, the 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; as a last resort, report the use of Sugar to your users as loudly as possible. - Problem: Assumptions about the lack of properties
Risk: minimal. The problem arose twice in the history of Sugar, both cases were quickly resolved. - Problem: Compliance Specification
Risk: very little. Sugar tries to be as careful with modifying native objects as possible. But is that enough? The answer to this question depends on the user's beliefs and the structure of the project, and also changes with time (for the better).
These conclusions we made ourselves based on the experience of using Sugar in the real world and user feedback. If they cause you to doubt, each item we discussed in detail in the article - in the hope that this will help you draw your own conclusions about whether Sugar is suitable for your project.
From translator
SugarJS Performance
Being more comfortable to use, Sugar noticeably loses Lo-Dash in performance. However, the issue of performance, in my opinion, is important only in the processing of any large amounts of data. If you are working with the front-end, 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 where, after traversing the properties of an object / array, it is not necessary to return a new object with all properties, LazyJS wins Lo-Dash in performance. In compound operations, the gap becomes significant. For example, the operation
map -> filter
LazyJS is five times faster than Lo-Dash, and fifteen times than SugarJS. If you need not just walk through the property values, but to collect a new object / array, then LazyJS loses the advantage.
StreetStrider suggests that lazy computation on chains is
planned in LoDash version 3.
Here you can compare the performance of ten similar libraries on a variety of typical operations directly in your browser.
The convenience we lost
You may be wondering how elegantly the problem of extending native objects is solved in Ruby. They proposed a refinement mechanism (refinements), which in the version of Ruby 2.1 came out of experimental status.
Suppose the developer Vasya, who writes the Vasya library, does not hesitate to make decoys-patches: from his library he (re) defines the methods of standard objects using the
refine
design.
refine String do def petrovich "Petrovich says: " + self end end
Developer Peter sculpts one of the parts of a large project that many programmers work on. When Petya connects the Vasya library, the Vasiny monkey patches do not apply to the entire project and do not interfere with the rest of the coders. At library connection redefinition of native objects does not occur at all.
In order to use new methods, Petya, in his code, indicates the monkey patches from which libraries he needs:
using Vasya using HollowbodySixString1957
As a result, the redefinition of global objects made from these libraries is applied only for 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 it was obvious to me alone. Stand out my answer from the comments.
This is probably more obvious to those who started serious programming with Ruby. As they say, you quickly get used to good: a rich and very functional standard library, a natural way of calling methods for a dynamic language, and the ability to build methods into chains.
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();
... it becomes somehow, you know, depressingly.