INFO-I400 Topics in Informatics

Graphics, Animation, and Multimedia for the Web

Lesson 8: JavaScript Arrays

Prerequisites:

Reading Assignment

Creating Arrays

Arrays are actually objects, that is, hash tables; so they work well for sparse arrays, and they grow dynamically if you insert new values that extend the size of the array. They are similar to Python’s lists and to Java’s ArrayLists.

Array Literal Expressions

You can write an array literal expression like this:

var ws = [1, 2, "three", "four"]; // an array with four elements

Constructed Arrays

Or you can construct an array with new Array(...):

var xs = new Array(10); // an array of 10 undefined elements
var ys = new Array("cat", "dog"); // an array with two elements
var zs = new Array(1, 2, "three", "four"); // like ws

Generally, if you know the elements of the array in advance, using an array literal is better than constructing an array (with new); it is more efficient.

Array Elements

We refer to array elements using the notation xs[i], where xs is the array and i is the subscript. Subscripts start at 0, as in Python and Java.

console.log(ys[0]); // prints "cat"
xs[5] = "puppies";  // stores "puppies" in xs[5]

Array Properties

The length property is the size, or number of elements, in the array.

ws.length // is 4
ws["length"] // is 4

It can be adjusted:

ws.length = 8; // increases size, new elements are undefined
ws.length = 3; // removes elements

Looping through Arrays

To iterate through all the elements of an array:

for (var i = 0; i < ws.length; i++)
    console.log(ws[i]);

Or, going backwards:

for (var i = ws.length - 1; i >= 0; i--)
    console.log(ws[i]);

Array Methods

Arrays are objects, so it shouldn’t be surprising that they have methods.

Non-Destructive Methods

Non-destructive methods produce a new object, instead of mutating an existing object.

concat

Concatenate (chain together) two arrays:

[1, 3, 5].concat([7, 8]) // value is [1, 3, 5, 7, 8]

Or using variables:

xs.concat(ys)

Remember, this is non-destructive, so both xs and ys are unchanged.

Concatenate three arrays, similarly:

xs.concat(ys).concat(zs)

Equivalently:

xs.concat(ys, zs)

This can go on with any number of array arguments.

join

join forms a string from the array elements; sep is the separator, the default is a comma:

xs.join(sep)

Examples—here, as you can see from the js> prompt, I am using SpiderMonkey as a stand-alone Java interpreter; but you could equally well type these examples into your web browser’s Java console:

js> xs = [10, 1, 2, 3];
"10,1,2,3"
js> xs.join()
"10,1,2,3"
js> xs.join(";") 
"10;1;2;3"
js> xs.join("!@#")
"10!@#1!@#2!@#3"

slice

slice works conceptually like slicing a list in Python, but the notation is different: it’s a method call.

// xs.slice(start[, end])
js> xs.length
7
js> xs.slice(0, 4)
["a", "b", "c", "d"]
js> xs.slice(4, 7)
["e", "f", "g"]
js> xs.slice(4)
["e", "f", "g"]
js> xs.slice(2, 5)
["c", "d", "e"]

toString

toString converts to a string (not usually needed explicitly).

js> xs
["a", "b", "c", "d", "e", "f", "g"]
js> xs.toString()
"a,b,c,d,e,f,g"

Destructive Methods

Destructive methods mutate the array.

Deque operations: push, pop, unshift, shift

An array can be operated on as a deque (a data structure that supports both stack and queue operations), with push and pop operating at the rear, unshift and shift at the front.

push adds an element to the rear of an array; it returns the new length.

pop removes and returns the rear element of an array.

js> var xs = [1, 2, 3];
js> xs
[1, 2, 3]
js> xs.push(10);
4
js> xs
[1, 2, 3, 10]
js> xs.pop()
10
js> xs
[1, 2, 3]

unshift adds an element to the front of an array, returning the new length.

shift removes and returns the front element of an array.

js> xs
[1, 2, 3]
js> xs.unshift(10);
4
js> xs
[10, 1, 2, 3]
js> xs.shift();
10
js> xs
[1, 2, 3]

sort

xs.sort(); // sorts alphabetically
xs.sort(function (x, y) { return x - y }); // sorts numerically

The optional compare function argument should return a negative value from compare(a, b) if a < b, 0 if a = b, and positive if a > b.

reverse

Destructively reverses the elements of the array

xs.reverse();

splice

Destructively adds and/or removes elements anywhere in an array

xs.splice(i, n [, e1, e2, ...]);
  // where i = position at which to add or remove elements
  // n = number of elements to remove
  // e1, e2, ... = elements to be inserted
  // Returns the elements removed

Examples:

js> xs
[10, 1, 2, 3]
js> xs.splice(1, 0, 'a', 'b', 'c');
[]
js> xs
[10, "a", "b", "c", 1, 2, 3]
js> xs.splice(1, 3);
["a", "b", "c"]
js> xs
[10, 1, 2, 3]
js> xs.splice(2, 2, 'cat', 'dog');
[2, 3]
js> xs
[10, 1, "cat", "dog"]

Functions with a Variable Number of Parameters

A function can be called with more or less actual parameters than it has formal parameters. All the actual parameters are available through a “property array”, arguments; arguments.length is the number of actual arguments, and the arguments are arguments[0]arguments[arguments.length - 1].

Example 08-01

Extend Example 07-01 using an array of ball objects, so that the cannon can fire repeatedly with multiple balls in the air.

Well, of course, we are going to create a lot of cannon balls, so it would be a good idea to define a function to make them:

function start_animation () {

    ...

    function makeBall () {
        return {'x': 0, 'y': 0, // position,
                'vx': init_speed * Math.cos(theta), 
                'vy': init_speed * Math.sin(theta), // velocity
                'ax': 0, 'ay': -9.8, // acceleration, meters/sec/sec
                'radius': 10, // pixels
                'color': 'black'};
    }

Instead of a single ball variable, we want an array of balls, initially empty:


    var balls = [];

    ...

Let’s fire the cannon every 3 seconds (fire_dt) and keep going for 20 seconds (stop_dt).

We need an interval timer (firing_iid) to fire the cannon, and a timeout (stop_tid) to make the cannon stop firing. But when the shooting stops, we don’t want the animation to suddenly end, with the balls in mid-air. Instead, we’ll need to wait for the last ball to return to terra firma; this will come in physics.

    var fire_dt = 3 * 1000; // fire every 3 seconds
    var stop_dt = 20 * 1000; // stop in 20 seconds

    // Interval and timeout timers, initialized later
    var physics_iid, render_iid;

    var firing_iid;
    var stop_tid;

The physics function has a few changes.

First, it uses a loop to iterate through the array, updating each ball.

Second, if a ball is out of range, instead of stopping the animation (as we did when there was only one ball), we just remove it from the array.

Third, when the last ball has gone out of range, it is time to stop the animation. Now when this happens, physics will have just removed the last ball from balls, and so balls will be empty, and that is how we know that the last ball has landed;1 physics will then clear the remaining interval timers, so that we don’t waste CPU cycles.

    // physics update
    function physics() {
        var dt = 0.001 * physics_dt; // milliseconds to seconds
        for (var i = 0; i < balls.length; i++) {
            var b = balls[i]; // short variable name
            // update velocity vy; vx does not change
            b.vy = b.vy + b.ay * dt;
            // update position
            b.x = b.x + b.vx * dt;
            b.y = b.y + b.vy * dt;

            // remove if out of range
            if (b.y < 0 || b.y > CH || b.x < 0 || b.x > CW) {
                console.log("Deleting ball at " + i + ": " + b);
                balls.splice(i, 1); // remove 1 element at balls[i]
            }
        }
        // stop when the last ball has come to earth
        if (balls.length == 0) {
            clearInterval(physics_iid);
            clearInterval(render_iid);
        }
    }

There are changes to rendering, since we now have several balls to render. First, we have render_balls instead of render_ball. Then, of course, there’s a loop to render each ball in the array balls.

    // animation update
    function render () {
        render_sky();
        render_balls();
    };

    ...

    function render_balls () {
        for (var i = 0; i < balls.length; i++) {
            var b = balls[i];
            ctx.beginPath();
            ctx.arc(xcc(b.x), ycc(b.y), b.radius, 0, 2 * Math.PI, false);
            ctx.closePath();
            ctx.fillStyle = b.color;
            ctx.fill();
        }
    }

    ...

The fire_cannon function creates a ball and pushes it onto the balls array (so that it becomes the last element).

    function fire_cannon () {
        balls.push(makeBall());
    }

The stop_cannon function clears the interval timer for the cannon, so that the cannon stops firing.

    function stop_cannon () {
        clearInterval(firing_iid);
        // physics will stop everything else
        // when balls is empty
    }

Finally, after defining all these inner functions, we have a few more steps needed to get things going. We start by firing the cannon once (fire_cannon()). Then we have two additional timers to set up: one to keep the cannon firing every few seconds (firing_iid), and one to stop the cannon (stop_tid).

    fire_cannon();
    render();

    physics_iid = setInterval(physics, physics_dt);
    render_iid = setInterval(render, render_dt);
    firing_iid = setInterval(fire_cannon, fire_dt);
    stop_tid = setTimeout(stop_cannon, stop_dt);

}

start_animation();

View the result here:

Missing

References


  1. It could be more complicated than this, because balls is also empty at the start of the simulation, before the cannon has ever fired. Fortunately, the cannon fires before physics ever runs, so we do not need an additional variable to tell us that the cannon has stopped firing (instead of not yet started).