2012-02-23

Major and minor JavaScript pitfalls and ECMAScript 6

Update 2012-02-24: New content in sections
  • 2.1. Function-scoped variables
  • 2.2. Inadvertent sharing via a closure
  • 2.7. for...in is weird

JavaScript has many pitfalls. This post examines whether they make JavaScript “unfixable” as a language – as some argue. To do so, it separates the pitfalls into two categories: The ones that become harmless after learning about them and the ones that don’t. We’ll also look at how the upcoming ECMAScript 6 fixes most problems.

Warning: If you are new to JavaScript then don’t let this post be your introduction to it. Consult other material first.

Major JavaScript pitfalls

There are two pitfalls in JavaScript that frequently trip up even experienced programmers.

Dynamic this

Using a function inside a method is problematic, because you can't access the method’s this. This might be fixed in ECMAScript 6 via block lambdas (that don’t have their own this):
    var obj = {
        name: "Jane",
        friends: ["Tarzan", "Cheeta"],
        printFriends: function () {
            // forEach argument is block lambda => can omit parentheses
            this.friends.forEach {
                | friend |
                console.log(this.name+" knows "+friend);
            }
        }
    }

Subtyping is difficult

While implementing a single type via a constructor is something that can be learned, creating a sub-constructor is too complicated [1]. As a result, numerous inheritance libraries have sprung up that lead to greatly varying coding styles, making life unnecessarily hard for humans and IDEs. And libraries are limited with regard to what they can do about super-calls. ECMAScript 6 will probably bring us some kind of inheritance operator and super-calls [2]:
    var Employee = Person <| function (name, title) {
        super.constructor(name);
        this.title = title;
    }
    Employee.prototype .= {
        describe() {
            return super.describe() + " (" + this.title + ")";
        }
    };
Additionally, there might be class literals which would make things even simpler.

Minor JavaScript pitfalls

Apart from the big ones, there are several minor pitfalls that people frequently complain about. It is obviously not ideal that those exist, but you can learn and accept them. And once you have, they are unlikely to bite you in the future. The following list is not exhaustive, but covers the more ugly ones.

Function-scoped variables

JavaScript’s var statement is function-scoped [3], even in nested blocks, the variables declared by it exist in the complete (innermost enclosing) function.
    function createInterval(start, end) {
        // Variable tmp already exists here
        console.log(tmp); // undefined
        
        if (start > end) {
            var tmp = start;
            start = end;
            end = tmp;
        }
        return [start, end];
    }
You quickly learn to use a pattern called immediately-invoked function expression (IIFE). Not pretty, but it works.
    function createInterval(start, end) {
        // Variable tmp does not exist here,
        // accessing it would cause an error
        
        if (start > end) {
            (function () {  // IIFE: open
                var tmp = start;
                start = end;
                end = tmp;
            }());  // IIFE: close
        }
        return [start, end];
    }
ECMAScript 6 will have block-scoped variables via the let statement.
    function createInterval(start, end) {
        // Variable tmp does not exist here
        
        if (start > end) {
            let tmp = start;
            start = end;
            end = tmp;
        }
        return [start, end];
    }
But with ECMAScript 6’s destructuring assignment, you don’t even need a temporary variable to do the swapping:
    [start, end] = [end, start];

Inadvertent sharing via a closure

Functions you create in a given context stay connected to the variables in that context, even after leaving the context. For example, every function in the array result below will return 10, because that is the final value of i.
    var result = [];
    for(var i=0; i < 10; i++) {
        result.push(function () { return i })
    }
    console.log(result[5]()); // 10, not 5
To avoid sharing, you need to make a copy, via an IIFE with a parameter:
    var result = [];
    for(var i=0; i < 10; i++) {
        (function (i) {  // copied i
            result.push(function () { return i })
        }(i)); // original i
    }
    console.log(result[5]()); // 5
Inadvertent sharing is not a quirk, it’s how things should normally work. I find that I normally notice quite easily when it happens and do the requisite copying. But it can foil beginners. Which is why Dart creates a fresh copy of the iteration value for each loop iteration. ECMAScript 6’s for...of loop will probably do the same. If you use an iteration method where the iteration value is passed to a function, then sharing won’t be a problem, either:
    range(0, 10).map(function (i) {
        result.push(function () { return i })
    });
    console.log(result[5]()); // 5
    
    function range(start, end) {
        var arr = [];
        for(; start<end; start++) {
            arr.push(start);
        }
        return arr;
    }

Extracted methods can’t use this

You have to learn to use bind(). Example:
    function repeat(n, func) {
        for(var i = 0; i < n; i++) {
            func();
        }
    }
    var counter = {
        count: 0,
        inc: function () {
            this.count++;
        }
    }
    // The second argument can’t be just counter.inc
    repeat(2, counter.inc.bind(counter));

Creating global variables via this

Before ECMAScript 5, if you made a function call to a constructor (because you forgot new) or a method (see previous item), then this would lead to global variables being read or created:
    > function Point(x,y) { this.x = x; this.y = y }
    > Point(12, 7)
    undefined
    > x
    12
ECMAScript 5 strict mode [4] fixes that problem:
    > function Point(x,y) { "use strict"; this.x = x; this.y = y }
    > Point(12, 7)
    TypeError: Cannot set property 'x' of undefined

Automatic creation of globals

Before ECMAScript 5, writing to a variable that didn’t exist, yet, automatically created a global variable:
    > function f() { foobar = "hello" }
    > f()
    > foobar
    'hello'
With strict mode, you get an error:
    > function f() { "use strict"; foobar = "hello" }
    > f()
    ReferenceError: foobar is not defined

Comparison via == is weird

On the web, there are whole corpora of strange things you can do with ==. Simple solution: Don’t use it. Ever [5]. The strict equality operator === is very reliable.

for...in is weird

The for...in loop is a strange beast:
  • Objects: It iterates over all property names, including (enumerable) inherited ones.
        > var proto = { protoProp: true };
        > var obj = Object.create(proto);
        > obj.objProp = true;
        
        > for(var propName in obj) { console.log(propName) }
        objProp
        protoProp
    
    If, say, Object.prototype.toString wasn’t non-enumerable, it would also show up above, because by default, all objects inherit from Object.prototype.
  • Arrays: for...in seems to iterate over the array indices, but that is only because the length property is not enumerable. It actually iterates over all properties of an array.
        > var arr = [ "a", "b" ];
        > arr.hello = true;
        > for(var propName in arr) { console.log(propName) }
        0
        1
        hello
    
    For arrays, it would make much more sense to iterate (only) over the array elements and not their indices. ECMAScript 6’s for...of loop will do that.
Hence, for...in depends in a very fragile way on the enumerability [6] of properties. Solution: Don’t use it [7]. Use forEach for arrays.
    var arr = [ "apple", "pear", "orange" ];
    
    // You probably only want elem, but you have index, too
    arr.forEach(function(elem, index) {
        console.log(elem);
    });
To iterate over the property names of an object, you can use:
    Object.keys({ first: "John", last: "Doe" }).
    forEach(function (propName) {
        console.log(propName);
    });

Array-like objects

Some objects in JavaScript are array-like, they have a length property and indexed access, but none of the array methods. You therefore need to borrow array methods via call(). The special variable arguments is array-like:
    function printArguments() {
        console.log(Array.prototype.join.call(arguments, "; "));
    }
Not pleasant, but learnable. And you get errors quickly if you are doing something wrong. At least in ECMAScript 6, you won’t have to use arguments, any more:
    function printArguments(...args) {
        console.log(args.join("; "));
    }
As a side benefit, function arity can now be automatically checked by the language.

truthy and falsy values; having both undefined and null

Yes, not terribly elegant. Get over it. ECMAScript 6 might have operators that allow you to check for either undefined or null. Furthermore, one will be able to define default values for missing parameters, obviating one major use case for this kind of check.

References

  1. JavaScript inheritance by example
  2. JavaScript does not need classes
  3. JavaScript variable scoping and its pitfalls
  4. JavaScript’s strict mode: a summary
  5. When is it OK to use == in JavaScript?
  6. JavaScript properties: inheritance and enumerability
  7. Iterating over arrays and objects in JavaScript

No comments: