General coding tips

miDeb edited this page Jan 4, 2019 · 4 revisions

This is a collection of some random coding tips I've collected as I write Cinnamon JavaScript code.

Scoping

Function scoping

In Cinnamon code, we see a lot of Lang.bind(this, func). What does this actually do?

We first start with how normal function scoping works. This is usually quickly forgotten once we learn about Lang.bind, after which we just wrap everything with Lang.bind.

If you define a function, it can access variables outside it when defined. For example (assuming an imaginary print function)

let x = 42;
let func = function() {
    print(x);
}
func(); // Prints 42

This successfully prints 42. This is the normal way of using Javascript. There is no need for Lang.bind.

What Lang.bind does is it scopes this. If you are declaring an object prototype, and want to do this:

this.x = 42;
let func = function() {
    print(this.x);
}
func(); // Prints undefined

It fails miserably. this is not a variable. It is not passed to the function. Instead, you should do this:

this.x = 42;
let func = Lang.bind(this, function() {
    print(this.x);
})
func(); // Prints 42

You can also bind the function to something else, eg

this.x = {'a': 42};
let func = Lang.bind(this.x, function() {
    print(this.a);
}
func(); // Prints 42

You have been warned. Eventually you will abuse Lang.bind and use it everywhere, and forget how function scoping works. This only complicates your code.

Variable scoping

In Cinnamon most variables are declared using let, while "normal" javascript usually uses var. The difference is that the scope of let is the block it was defined in, while the scope of var is the function it was defined in. Refer to the following code:

function foo() {
    let bar0 = 0;
    var bar1 = 1;
    if (bar0 == 0) {
        print(bar0); // Prints 0
        print(bar1); // Prints 1
        let bar2 = 2;
        var bar3 = 3;
    }
    print(bar2); // Undefined
    print(bar3); // Prints 3
}

Most code in Cinnamon uses let, and it doesn't make a huge difference. But sometimes we want the behaviour of var. If you see var popping out somewhere in Cinnamon, don't randomly change it to let without checking why var is used!

Easy pitfalls

Removing elements from array

Suppose we have an array array = [1, 2, 3, 3, 2, 3]. We want to remove all occurences of 3.

The following code is wrong.

for (let i = 0; i < array.length; i ++){
    if (array[i] == 3)
        array.splice(i, 1);
}

Do you see why? In the first two runs, nothing happens. In the third run through the loop, i = 2 and array[i] = 3. So we remove an item from the array. Then new array is array = [1, 2, 3, 2, 3]. Then we go on with i = 3. array[3] = 2, nothing happens. Then i = 4, array[4] = 3 and it is removed. Then the for-loop stops. We are left with array = [1, 2, 3, 2]. Oops! A 3 is still there. Whenever we remove an item, the following items are shifted leftwards. This is bad.

What we want is a reverse loop:

for (let i = array.length - 1; i != 0; i ++) {
    if (array[i] == 3)
        array.splice(i, 1)
}

This is sometimes written as

let i = array.length;
while (i--) {
    if (array[i] == 3)
        array.splice(i, 1)
}

(Premature/micro)optimization

Optimizing your JavaScript code might sound absurd - it surely falls into the category of premature/micro-optimization. You really shouldn't bother trying to optimize existing code using funny techniques. Look for things that are very slow, such as generating pixmaps.

However, if you are writing new code, it doesn't hurt to do things the fast ways. In many cases, it is as clear and readable, if not more.

Array-length caching

This is a well-known trick. The usual way of writing a for loop is

for (let i = 0; i < array.length; i ++) {
    doSomething();
}

This is slow since we have to read the length of the array every time. It is much faster if we do

for (let i = 0, len = array.length; i < len; i ++) {
    doSomething();
}

Depending on the array size, the cached version can more than 5 times faster. We can also use a while loop (documented below), but the speed gain is not as significant.

Using built-in functions

forEach and map are awesome but easily forgotten (indexOf is also awesome but seldom forgotten). These built-in functions are many times faster than using home-made for loops on my machine. We are not counting in percents.

Note that the for-loop itself is not slow. In fact the for loop is quite quick. It is reading array[j] that is the bottleneck.

forEach

Using for loop:

for (let i = 0, len = array.length; i < len; i++) {
    doSomething(array[i]);
}

Using forEach:

array.forEach(function(item) {
    doSomething(item);
})

This is much faster, makes the intention clear (you do something for each item in array), and can make things shorter if the name of the array is long.

Map

Using a for loop:

for (let i = 0, len = array.length; i < len; i ++) {
    array[i] = doSomething(array[i]);
}

Using map:

array.map(function(item) {
    return doSomething(item);
})

Again, map has a very specified purpose, and using map makes your intention clear (as well as much faster).

Magic while loops

Here I define a "magic while loop" as using while loops where it isn't the obvious solution to a problem. Here I present some examples and how they compare to "normal" solutions. All tests are run on the git version of cjs, on my machine.

Replacing for loops

We can use a normal (cached) for loop:

for (let i = 0, len = array.length; i < len; i++) {
    doSomething(array[i]);
}

The magic while loop looks like this:

let i = array.length;
while (i--) {
    doSomething(array[i]);
}

This works since non-zero values of i are treated as true, while zero is treated as false. So the loop keeps running until i goes to 0. How do these three methods compare? Turns out while loop is faster by about 10%. This assumes that the doSomething is actually doNothing. In reality, the loop content is usually much slower than the loop itself. Note that this reverses the order of looping through the array, which may or may not be significant.

Searching for elements

You have an array of things, and you want to search for 0. The obvious solution is

array.indexOf(0)

This is, unsurprisingly, the fastest. But what if we want to see if foo.bar == 0 for each foo in array? There are 3 possible solutions. We can keep using array, but do a map beforehand:

let array2 = array.map(function(foo) {
    return foo.bar;
});
array2.indexOf(0)

We can use a (cached) for loop:

let pos = -1;
for (let i = 0, len = array.length; i < len; i++)
    if (array[i].bar == 0) {
        pos = i;
        break;
    }
}

Finally, a magic while loop:

let i = array.length;
while (i-- && array[i].bar != 0);

Note that both returns -1 if the item is not found. The execution times of the two loops are approximately the same. The loops are faster than indexOf for small arrays (<10 items), while slower for large arrays. The for loop is arguable more clear and the while loop is arguably more terse.

Of course, we also have to take into account the time used to read foo.bar. If a getter is used for the property (which is always the case if foo is a GObject), the reading time should be much more significant. In this case, using map and indexOf might be slower since we will have to read the value for all items in array, not up to the point where we find the thing we want.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.