-
Notifications
You must be signed in to change notification settings - Fork 749
General coding tips
This is a collection of some random coding tips I've collected as I write Cinnamon JavaScript code.
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.
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!
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)
}
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.
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.
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.
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.
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).
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.
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.
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.