Learning JavaScript the right way™, part 2

Click here for part 1

JavaScript only has two types of data structures, called Objects and Arrays. Neither is really what the name implies. Many other languages – Java comes to mind – come loaded with a whole list of trees, lists, maps, etc. JavaScript manages to cover all use cases with only two, at the cost of efficiency. They’re both always passed by reference.

Arrays

They should be used to store sets of identical elements. They can contain any type of elements and even mix types inside of the same array, but don’t mix types because it defeats the entire points of arrays. “Real arrays”, as in C, are contiguous blocks of memory. JavaScript “Arrays” aren’t really arrays, instead they’re a specializtion of the JavaScript Object type (more on that in part 3) and they work like lists. JavaScript provides a handy built-in literal notation using square brackets.

var arr1 = [1,2,3,4,5]; //Declares an array with 5 elements
console.log(arr1[0]); //Outputs 1
console.log(arr1.length); //Ouputs 5


The four common Array manipulation functions are push, pop, unshift, and shift. In order, they add and remove from the end and add and remove from the beginning. Adding and removing from the end is done in O(1), while the other two are O(n). Be careful whenever using shift and unshift: if the array is very long, the performance can become abysmal. Modifying a value in an array is O(1). Slice (O(n)) creates a copy of an interval (from, to) of the Array.

var arr1 = [0,1,2,3,4];
arr1.push(5); //arr1 is now [0,1,2,3,4,5]
arr1.pop(); //arr1 is now [0,1,2,3,4]
arr1.unshift(999); //arr1 is now [999,0,1,2,3,4]
arr1.shift(); //arr1 is now [0,1,2,3,4]

arr2 = arr1.slice(1, 3);
//arr2 contains [1,2] and arr1 hasn't changed.

Join returns a string from the array. That string is the value of each element, in order, with the supplied separator between each value. Join is often found near split, because they make a powerful pair. Split is the opposite of join: it operates on strings and returns an array.

var str1 = "abc";
var str2 = "ab-cd-ef";

var arr1 = str1.split(""); //arr1 is ["a", "b", "c"]
var arr2 = str2.split("-"); //arr2 is ["ab", "cd", "ef"]

arr1.join(""); //Returns "abc"
arr1.join(" "); //Returns "a b c"
arr2.join("-"); //Returns "ab-cd-ef"

Concat creates a new array from multiple arrays. The original arrays are not modified.

var arr1 = [1,2,3];
var arr2 = [4,5,6];
var arr3 = [7,8,9];
var arr4 = arr1.concat(arr2, arr3);
console.log(arr4); //[1,2,3,4,5,6,7,8,9]

Sort modifies the array itself. It can perform powerful and complex sorts by passing it a lambda.

var arr1 = [9,3,5,12,8,2];
var arr2 = arr1.slice(0); //Copy of arr1
arr1.sort(); //arr1 is now [2,3,5,8,9,12]

//Let's sort arr2 so that odd numbers are first, in increasing order,
//then come the even numbers, in decreasing order.
arr2.sort(function(a,b){
    if (a % 2 === 0){
        return b % 2 === 0 ? (b-a) : 1;
    }else{
        return b % 2 === 0 ? -1 : (a-b);
    }
});
//arr2 is now [3, 5, 9, 12, 8, 2]

I’m using the ternary operator (condition ? ifTrue : ifFalse) to avoid ugly nested ifs. The lambda compares 2 values; if it returns >0, then a is considered greater than b. If <0, then a is smaller. Returning 0 means they’re equal. Doing b-a or a-b is a much nicer way of checking which is greater than the other.

Arrays are well suited for data that needs to be manipulated in block. Every element of an array should be of the same type and comparable. For example, don’t mix prices with temperatures in the same array.

Since ES5, JavaScript Arrays also implement three powerful functional programming tools: map, reduce and filter. Together, they can replace 95% of the cases you’d loop over an Array. If you catch yourself writing a for loop over an Array, make sure that it can’t be done with map, reduce, filter or a combination of them. They are easier to read, easier to debug, eliminate off-by-one errors, are easier to reason about and easier to chain.

Map is used to apply an operation on each element of the array. Reduce generates an aggregate value from the array, such as the sum, average, etc. Filter gets rid of elements that don’t pass a certain test. These three functions work with lambdas and return a copy of the original array.

// MAP
var arr1 = [1,2,3,4,5];
var arr2 = arr1.map(function(a){
    return a+3;
});
//arr2 is [4,5,6,7,8]. arr1 is unchanged

// REDUCE
var arraySum = function(arr){
    return arr.reduce(function(a,b){
        return a+b;
    });
};
sum = arraySum(arr1); //15

// FILTER
var arr3 = arr2.filter(function(a){
    return a % 2 === 0;
});
//arr3 is [4,6,8]
sum = arraySum(arr3); //18

We’re finally starting to scratch the surface of why JS is a brilliant language.

Map

The lambda is passed one argument: the element of the list being modified. The value returned by the lambda is the new value of that element. In the next example, I implement a Caesar’s cypher. The key I chose is 5, meaning that each letter in my clear text is shifted 5 letters to the right. To keep the code simple, I assume that the message only contains letters.

var secret = "AttackAtDawn";
var encrypt = function(clear, key){
    if (key < 0){
        key += 26;
    }
    return clear.toLowerCase().split("").map(function(a){
        var position = a.charCodeAt(0) - 97; //a is 97 in ascii
        var newAsciiValue = ((position+key) % 26) + 97;
        return String.fromCharCode(newAsciiValue);
    }).join("");
};
var encrypted = encrypt(secret, 5); //"fyyfhpfyifbs"
var clear = encrypt(encrypted, -5); //"attackatdawn"

The lambda in map is passed “a”, then “t”, “t”, “a”, “c”, etc. It finds the letter’s ascii value and shifts the whole alphabet from 97-122 to 0-25. By using a modulo, I can make the alphabet loop around (z+5 = e).

Reduce

Unlike map and filter, reduce doesn’t return an Array: it returns an aggregate value for the array. Examples of aggregates include the sum, average, max/min value, concatenated string from all the values (join) and countless others. Reduce is also different because the lambda is passed two arguments. The lambda is first called on the first and second elements of the array and returns an aggregate. The aggregate value is then called with the third element, returning a new aggregate. That new aggregate is then called with the fourth element, and so on, until all the elements have been “eaten up” by the reduction. The final aggregate is then returned.

var arr = [1,2,3,4,5];
var sum = arr.reduce(function(a,b){
    return a+b;
}); //15
/*
This is how it happens, one step at a time:
a=1, b=2
a=3, b=3
a=6, b=4
a=10, b=5
return 15;
*/

Filter

Very simple, the lambda is passed one element at a time. If it returns true, the element is kept, otherwise it won’t be present in the returned array.

var vowels = ["a", "e", "i", "o", "u", "y"];
var str = "Javascript is pretty awesome!";

var arr = str.toLowerCase().split("").filter(function(a){
    return vowels.indexOf(a) >= 0;
});
console.log(arr); //["a", "a", "i", "i", "e", "y", "a", "e", "o", "e"]

indexOf returns the first position of the passed element. If it not found in the array, it returns -1. In this example, every element of the array returns -1 (and thus false after >= 0) and gets discarded, except the vowels. Keep in mind that filter doesn’t modify the original array.

Arrays are ubiquitous in JavaScript and the language provides powerful tools to manipulate them in readable and maintainable ways without having to resort to clunky low level loops, so use those tools!

One thought on “Learning JavaScript the right way™, part 2

  1. Pingback: Learning JavaScript the right way™, part 1 | Simon Grondin

Leave a Reply