Objectives

Examine some of JavaScript's constructs such as conditionals, loops, arrays, objects, functions and scope. Understanding some concepts such as Truthy and Falsy values. simple static web site, include an external JavaScript file and use Google Chrome developer tools to debug into the script at runtime.

Setup

For this lab, we will dispense with the need for html and a browser to run the code, and make use of webstorm (and node) to run the programmes.

Create a new empty project called js-basics. Follow the instructions on the first step of the last lab to configure the project with the same .jscsrc configuration as outlined there.

In the project, create a file called hello.js containing the following:

console.log('hello from webstorm');

You can now run this script by selecting the file in the context menu, and selecting Run:

We should see the message in the lower run panel. We can use this technique to run all the scripts in this lab.

Conditional Code

Sometimes a block of code should only be run under certain conditions. Flow control — via if and else blocks — lets you run code if certain conditions have been met.

// Control flow
const foo = true;
const bar = false;

if (bar) {
  conole.log('this code won\'t run');
}

if (bar) {
  conole.log('this code won\'t run');
} else {
  if (foo) {
    conole.log('this code will run');
  } else {
    conole.log('this code would run if foo and bar were both false');
  }
}

While curly braces aren't strictly required around single-line if statements, using them consistently, even when they aren't strictly required, makes for vastly more readable code.

Be mindful not to define functions with the same name multiple times within separate if/else blocks, as doing so may not have the expected result.

Truthy and Falsy Things

In order to use flow control successfully, it's important to understand which kinds of values are "truthy" and which kinds of values are "falsy." Sometimes, values that seem like they should evaluate one way actually evaluate another.

// Values that evaluate to true
'0';
'any string';
[];  // an empty array
{};  // an empty object
1;   // any non-zero number
// Values that evaluate to false
'';  // an empty string
NaN; // JavaScript's "not-a-number" variable
null;
undefined;  // be careful -- undefined can be redefined!

Exercise: 'if.js'

Write a program in a file called if.js to do the following:

  • Define two variables called option1 and option2.
  • Set option1 and option2 to true and false respectively.
  • Using an if statement print to the console:
    • "both True"
    • "both False"
    • "option1 only true"
    • "option2 only true"

Change the values manually to generate each of the outputs in turn.

Switch Statements

Rather than using a series of if/else blocks, sometimes it can be useful to use a switch statement instead. Switch statements look at the value of a variable or expression, and run different blocks of code depending on the value.

// A switch statement
switch (foo) {
  case 'bar':
    alert('the value was bar -- yay!');
    break;
  case 'baz':
    alert('boo baz :(');
    break;
  default:
    alert('everything else is just ok');
}

Exercise switch.js

Write program in a file called switch.js containing a switch statement. The switch is to check a variable called grade for good, excellent and outstanding strings. If should log to the console a suitable congratulatory message depending on which string is present.

Run the program by declaring and initialising the grade variable.

Explore the ES6 string substitution in your solution:

Loops

Loops let a block of code run a certain number of times:

// A for loop
// logs `try 0`, `try 1`, ..., `try 4`
for (let i = 0; i < 5; i++) {
  console.log(`try ` + i);
}

Note that in loops, the variable i is not 'scoped' to the loop block even though the keyword let is used before the variable name.

The for loop

A for loop is made up of four statements and has the following structure:

for ([initialisation]; [conditional]; [iteration])  {
 [loopBody]
}

The initialisation statement is executed only once, before the loop starts. It gives you an opportunity to prepare or declare any variables.

The conditional statement is executed before each iteration, and its return value decides whether the loop is to continue. If the conditional statement evaluates to a falsey value, then the loop stops.

The iteration statement is executed at the end of each iteration and gives you an opportunity to change the state of important variables. Typically, this will involve incrementing or decrementing a counter and thus bringing the loop closer to its end.

The loopBody statement is what runs on every iteration. It can contain anything. Typically, there will be multiple statements that need to be executed, and should be wrapped in a block ({...}).

Here's a typical for loop:

// A typical for loop
for (let i = 0, limit = 100; i < limit; i++) {

  // This block will be executed 100 times
  console.log('Currently at ' + i);

  // Note: the last log will be `Currently at 99`
}

The while loop

A while loop is similar to an if statement, except that its body will keep executing until the condition evaluates to false.

while ([conditional])  {
  [loopBody]
}

Here's a typical while loop:

// A typical while loop
let i = 0;

while (i < 100) {
  // This block will be executed 100 times

  console.log(`Currently at ` + i);

  // increment i

  i++;
}

Notice that the counter is incrementing within the loop's body. It's possible to combine the conditional and incrementer, like so:

// A while loop with a combined conditional and incrementer

let j = -1;

while (++j < 100) {
  // This block will be executed 100 times

  console.log(`Currently at ` + j);
}

Notice that the counter starts at -1 and uses the prefix incrementer (++i).

The do-while loop

This is almost exactly the same as the while loop, except for the fact that the loop's body is executed at least once before the condition is tested.

do  {
  [loopBody]
} while ([conditional])

Here's a do-while loop:

// A do-while loop
do {
  // Even though the condition evaluates to false
  // this loop's body will still execute once.
  console.log(`Hi there!`);
}
while (false);

These types of loops are quite rare since only few situations require a loop that blindly executes at least once. Regardless, it's good to be aware of it.

Breaking and continuing

Usually, a loop's termination will result from the conditional statement not evaluating to true, but it is possible to stop a loop in its tracks from within the loop's body with the break statement.

// Stopping a loop
for (let i = 0; i < 10; i++) {
  if (i == 4) {
    break;
  }
}

You may also want to continue the loop without executing more of the loop's body. This is done using the continue statement.

// Skipping to the next iteration of a loop
for (let i = 0; i < 10; i++) {
  if (i == 5) {
    continue;
  }

  // The following statement will only be executed
  // if the conditional 'something' has not been met
  console.log(`I have been reached`);
}

Exercise: loops.js

In loop.js

  • ceate an array object of 8 elements.
  • insert some random numbers into each position in the array.
  • write a for loop to print each value to the console

Additionally, write code fragments to:

  • count and print out the number of elements greater than zero
  • compute the average of all the numbers

Arrays

Arrays are zero-indexed, ordered lists of values. They are a handy way to store a set of related items of the same type (such as strings), though in reality, an array can include multiple types of items, including other arrays.

To create an array, either use the object constructor or the literal declaration, by assigning the variable a list of values after the declaration.

// A simple array with constructor
const myArray1 = new Array('hello', 'world');

// literal declaration, the preferred way
const myArray2 = ['hello', 'world'];

The literal declaration is generally preferred. See the Google Coding Guidelines for more information.

If the values are unknown, it is also possible to declare an empty Array, and add elements either through functions or through accessing by index:

//Creating empty arrays and adding values
let myArray3 = [];

// adds 'hello' on index 0
myArray3.push('hello');

// adds 'world' on index 1
myArray3.push('world');

// adds '!' on index 2
myArray3[2] = '!';

'push' is a function that adds an element on the end of the array and expands the array respectively. You also can directly add items by index. Missing indices will be filled with 'undefined'.

//Leaving indices
var myArray4 = [];

myArray4[0] = 'hello';
myArray4[1] = 'world';
myArray4[3] = '!';

console.log(myArray4); // [ 'hello', 'world', undefined, '!' ];

If the size of the array is unknown, 'push' is far more safe. You can both access and assign values to array items with the index.

//Accessing array items by index
const myArray5 = ['hello', 'world', '!'];

console.log(myArray5[2]); // '!'

Exercise: Arrays 1

In your current project create a file called 'arrays.js' in the js folder to do the following:

  • Declare an array called 'todolist'.
  • Initialise this array to contain 5 strings, which describe some simple tasks (make up some).
  • write a for loop then logs each of these tasks to the console
  • pass the entire array to a single call to console.log

Observe the results in webstorm

Array Methods and Properties

.length

The .length property is used to determine the amount of items in an array.

const myArray6 = ['hello', 'world', '!'];

console.log(myArray6.length); // 3

You will need the .length property for looping through an array:

// For loops and arrays - a classic
const myArray7 = ['hello', 'world', '!'];

for (let i = 0; i < myArray7.length; i = i + 1) {
  console.log(myArray7[i]);
}

Except when using for/in loops:

// For loops and arrays - alternate method
const myArray8 = ['hello', 'world', '!'];

for (let i in myArray8) {
  console.log(myArray8[i]);
}

.concat

Concatenate two or more arrays with .concat:

//Concatenating Arrays
const myArray9 = [2, 3, 4];
const myArray10 = [5, 6, 7];

const myArray11 = myArray9.concat(myArray10);
console.log(myArray11); // [ 2, 3, 4, 5, 6, 7 ]

.join

.join creates a string representation of the array. Its parameter is a string that works as a separator between elements (default separator is a comma):

//Joining elements
const myArray12 = ['hello', 'world', '!'];

console.log(myArray12.join(' '));  // 'hello world !';
console.log(myArray12.join());     // 'hello,world,!'
console.log(myArray12.join(''));   // 'helloworld!'
console.log(myArray12.join('!!')); // 'hello!!world!!!';

.pop

.pop removes the last element of an array. It is the opposite method of .push:

//pushing and popping
let myArray13 = [];

myArray13.push(0); // [ 0 ]
myArray13.push(2); // [ 0 , 2 ]
myArray13.push(7); // [ 0 , 2 , 7 ]
myArray13.pop();     // [ 0 , 2 ]

.reverse

As the name suggests, the elements of the array are in reverse order after calling this method:

//reverse
const myArray14 = ['world', 'hello'];

// [ 'hello', 'world' ]
myArray14.reverse();
console.log(myArray14);

.shift

Removes the first element of an array. With .pop and .shift, you can recreate the method of a queue):

//queue with shift() and pop()
let myArray15 = [];

myArray15.push(0); // [ 0 ]
myArray15.push(2); // [ 0 , 2 ]
myArray15.push(7); // [ 0 , 2 , 7 ]
myArray15.shift();   // [ 2 , 7 ]

console.log(myArray15);

.slice

Extracts a part of the array and returns that part in a new array. This method takes one parameter, which is the starting index:

//slicing
const myArray16 = [1, 2, 3, 4, 5, 6, 7, 8];
const myArray17 = myArray16.slice(3);

console.log(myArray17);  // [ 1, 2, 3, 4, 5, 6, 7, 8 ]
console.log(myArray17); // [ 4, 5, 6, 7, 8 ]

.splice

Removes a certain amount of elements and adds new ones at the given index. It takes at least 3 parameters:

// splice method
myArray.splice(index, length, values, ...);
  • Index - The starting index.
  • Length - The number of elements to remove.
  • Values - The values to be inserted at idx.

For example:

//splice example
const myArray18 = [0, 7, 8, 5];
myArray18.splice(1, 2, 1, 2, 3, 4);

console.log(myArray18); // [ 0, 1, 2, 3, 4, 5 ]

.sort

Sorts an array. It takes one parameter, which is a comparing function. If this function is not given, the array is sorted ascending:

//sorting without comparing function
const myArray19 = [3, 4, 6, 1];

myArray19.sort(); // 1, 3, 4, 6

console.log(myArray19);  // 1, 3, 4, 6
//sorting with comparing function
function descending(a, b) {
  return b - a;
}

const myArray20 = [3, 4, 6, 1];

myArray20.sort(descending); // [ 6, 4, 3, 1 ]

The return value of descending (for this example) is important. If the return value is less than zero, the index of a is before b, and if it is greater than zero it's vice-versa. If the return value is zero, the elements index is equal.

.unshift

Inserts an element at the first position of the array:

//unshift
const myArray21 = [];

myArray21.unshift(0); // [ 0 ]
myArray21.unshift(2); // [ 2 , 0 ]
myArray21.unshift(7); // [ 7 , 2 , 0 ]

.forEach

In modern browsers it is possible to traverse through arrays with a .forEach method, where you pass a function that is called for each element in the array.

The function takes up to three arguments:

  • Element - The element itself.
  • Index - The index of this element in the array.
  • Array - The array itself.

All of these are optional, but you will need at least the 'element' parameter in most cases.

//native forEach
function printElement(elem) {
  console.log(elem);
}

function printElementAndIndex(elem, index) {
  console.log('Index ' + index + ': ' + elem);
}

function negateElement(elem, index, array) {
  array[index] = -elem;
}

myArray22 = [1, 2, 3, 4, 5];

// prints all elements to the console
myArray22.forEach(printElement);

// prints 'Index 0: 1' 'Index 1: 2' 'Index 2: 3' ...
myArray22.forEach(printElementAndIndex);

// myArray is now [ -1, -2, -3, -4, -5 ]
myArray22.forEach(negateElement);

Exercise: Arrays 2

Take the last code fragment - foreach above, and incorporate into your arrays.js file. In webstorm, see if you can figure out how to debug and single step through the program.

Hints:

  • debug is available on the context menu
  • double clicking in the editor margin will set a breakpoint.

Objects

Objects contain one or more key-value pairs. The key portion can be any string. The value portion can be any type of value: a number, a string, an array, a function, or even another object. When one of these values is a function, it’s called a method of the object. Otherwise, they are called properties.

As it turns out, nearly everything in JavaScript is an object — arrays, functions, numbers, even strings — and they all have properties and methods.

// Creating an object literal
const myObject = {
  sayHello: function () {
    console.log('hello');
  },

  myName: 'Rebecca',
};

myObject.sayHello(); // "hello"
console.log(myObject.myName); // "Rebecca"

When creating object literals, note that the key portion of each key-value pair can be written as any valid JavaScript identifier, a string (wrapped in quotes), or a number:

const someString = 'some string';

const myObject2 = {
  validIdentifier: 123,
  someString: 456,
  99999: 789,
};

console.log(myObject2);

Exercise: Objects

Create a file called 'objects.js' your current project and bring in the first code fragment in this page and run it. Then do the following:

  • Introduce a new field into the object called 'email'.
  • Initialise the email to some valid address
  • Make the 'hello' function also print out the email address as well as the name.

Using this code as a guide, create a new object call myLocation. It should have location-name, latitude, longitude and description as its fields. It should then have a method 'showLocation' to log the current location to the console.

Functions

Functions contain blocks of code that need to be executed repeatedly. Functions can take zero or more arguments, and can optionally return a value.

Functions can be created in a variety of ways, two of which are shown below:

Study Airbnb styleguide section on functions.

// Function Declaration
function foo1() {
  /* do something */
}
// Named Function Expression
const foo2 = function () {
  /* do something */
};

Using Functions

// A simple function
const greet1 = function (person, greeting) {
  var text = greeting + ', ' + person;
  console.log(text);
};

greet1('Rebecca', 'Hello');
// A function that returns a value
const greet2 = function (person, greeting) {
  var text = greeting + ', ' + person;
  return text;
};

console.log(greet2('Rebecca', 'hello')); // 'hello, Rebecca'
// A function that returns another function
const greet = function (person, greeting) {
  var text = greeting + ', ' + person;
  return function () {
    console.log(text);
  };
};

const greeting = greet('Rebecca', 'Hello');
greeting();

Exercise: Functions

Create a new javascript file called 'function.js' Incorporate this function here into it:

// A simple function
function greet(person, greeting)  {
  const text = greeting + ', ' + person;
  console.log(text);
};

greet('Rebecca', 'Hello');

Introduce new function call to greet other people - and make sure the console displays the greeting.

Immediately-Invoked Function Expression (IIFE)

A common pattern in JavaScript is the immediately-invoked function expression. This pattern creates a function expression and then immediately executes the function. This pattern is extremely useful for cases where you want to avoid polluting the global namespace with code — no variables declared inside of the function are visible outside of it.

// An immediately-invoked function expression
(function () {
  var foo = 'Hello world';
})();

console.log(foo);   // undefined!

Functions as Arguments

In JavaScript, functions are 'first-class citizens' — they can be assigned to variables or passed to other functions as arguments. Passing functions as arguments is an extremely common idiom in jQuery.

// Passing an anonymous function as an argument
const myFn1 = function (fn) {
  var result = fn();
  console.log(result);
};

// logs 'hello world'
myFn1(function () {
  return 'hello world';
});
// Passing a named function as an argument

const myFn2 = function (fn) {
  var result = fn();
  console.log(result);
};

const myOtherFn = function () {
  return 'hello world';
};

myFn2(myOtherFn); // 'hello world'

Scope

"Scope" refers to the variables that are available to a piece of code at a given time. A lack of understanding of scope can lead to frustrating debugging experiences.

When a variable is declared inside of a function using the var keyword, it is only available to code inside of that function — code outside of that function cannot access the variable. On the other hand, functions defined inside that function will have access to to the declared variable.

When a var is declared and initialized within a function, the declaration is silently hoisted to the top of the function but not the assignment. The variable has what is termed function scope. This contrasts to block scope with which we are familiar from Java.

Study the section on Hoisting in the Airbnb style guide.

Prior to ES6 JavaScript possessed function scope (and global scope). This still applies to var type for the purpose of backward compatibility. But two new variable types have been introduced in ES6, const and let, and these have block scope.

Furthermore, variables that are declared inside a function without the var, const or letkeyword are not local to the function — JavaScript will traverse the scope chain all the way up to the window scope to find where the variable was previously defined. If the variable wasn't previously defined, it will be defined in the global scope which can have unexpected adverse consequences.

Whereas it is important for the purpose of code maintenance to recognize and understand the behaviour of the var type, its future use is discouraged. Instead, one should use const and let. Use const where reassignment is not envisaged, otherwise use let.

The first block of code below demonstrates the difference between function scope and block scope:

/**
 * The variables a and b have block scope and are 'visible'
 * only within the if (flag) {...} block.
 * The variable c has function scope and is 'visible' within the entire
 * function myFunction block, including within the nested if block.
 */
function myFunction(flag) {
  if (flag) {
    const a = 1;  // The scope is inside the if-block
    let b = 2;  // The scope is inside the if-block
    var c = 3;  // The scope is inside the function

    // Both a and b in scope
    console.log(a);  // 1
    console.log(b);  // 2
    console.log(c);  // 3

  }

  // Only b is in scope here.
  console.log(a); // Uncaught ReferenceError because a has block scope
  console.log(b); // Uncaught ReferenceError because b has block scope
  console.log(c);  // 3 Because b has function scope
}

myFunction(true);
// Functions have access to variables defined in the same scope
const foo = 'hello';
function sayHello() {
  console.log(foo);
};

sayHello(); // 'hello'
console.log(foo); // 'hello'
/**
 * Code outside the scope in which a variable was defined does not have access
 * to the variable
 */

function sayHello() {
  const foo = 'hello';
  console.log(foo);
};

sayHello(); // hello
/**
 * Variables with the same name can exist in different scopes
 * with different values
 */
const foo2 = 'world';

function sayHello() {
  const foo2 = 'hello';
  console.log(foo2);
};

sayHello(); // logs 'hello'
console.log(foo2); // logs 'world'
// Functions can see changes in variable values after the function is defined
function myFunction() {
  let foo2 = 'hello';

  function myFn() {
    console.log(foo2);
  };

  foo2 = 'world';
  return myFn;
};

const f = myFunction();

f(); // 'world'

#

/**
 * Scope insanity
 * Immediately invoked function expression (IIFE)
 * a self-executing anonymous function
 */

(function () {

  var baz = 100;

  var bim = function () {
    console.log(baz);
  };

  bar = function () {
    console.log(baz);
  };

}());

/**
 * The function bar() is defined outside of the anonymous function
 * because it wasn't declared with var and is therefore an implied global.
 * This is not recommended practice: adding to the global namespace.
 * And bar() has access to baz because outer function variables (and parameters) are available 
 * to inner functions: this is referred to as closure.
 */
bar(); // => 100

Attempting to access baz outside the IIFE above will fail:

(function () {
...
...
}());

/**
 * var baz has function scope and is not visible outside the IIFE.
 * Therefore attemting to access it here will generate an error:
 * Uncaught ReferenceError: baz is not defined
 */
console.log(baz);

The function bim() is also undefined outside the IIFE and so attempted invocation will fail.

(function () {
...
...
}());

/**
 * bim() is not defined outside of the anonymous function,
 * An attempt to invoke it will fail with this error:
 * Uncaught ReferenceError: baz is not defined
 */
bim();

All of the above behaviour applies in non-strict mode. Where ES6 strict mode is applied, the IIFE above will fail, generating an error warning that bar is not defined. That is, bar may no longer be defined as an implied global variable - which can only be good.

Exercise: Scopes

Take the each of the above code fragements separately,

  • incorporate into a javascript file called 'scope.js'
  • debug though each in webstorm. Keep a close eye on the scope variable pane, and confirm that the behaviour is as expected.

Exercises

Archive of the project so far:

Download and make sure you can open this project in WebStorm successfully. Verify that the Jscsrc guidelines are all showing green. Deliberately introduce some style violations, and get used to the error reports from the style plugin.

Experiment with the debugger in WebStorm