Skip to content
Graham Wakefield edited this page Sep 17, 2019 · 3 revisions

JavaScript (JS) is a lightweight programming language that is perhaps the most widely-used programming language today. It became known as a scripting language for web pages, and is now the de facto 'language of the web', having evolved from lightweight scripting roots to a full-fledged application language with powerful capabilities. But it is also widely used as an embedded language for server runtimes and desktop applications.

It now is a language of growing importance at the intersection of media/culture technology. Processing, a tool taught in many media arts and design programmes, has been rewritten for Javascript as p5.js. Javascript is embedded in Max/MSP (and with Max 8, this will include Node.js).

Note: Despite having "Java" in the name, JS has nothing to do with the Java language.

Why JS?

  • Arguably the most widely-used programming language today.
  • It is multi-paradigm, supporting imperative, functional, declarative, object-oriented etc. styles.
  • It has become the de facto 'language of the web'. JS looks like an imperative, procedural language, however it also supports functional programming features such as first-class functions. It has been described as Lisp in C's clothing.
  • Although it is a dynamic language, modern implementations can achieve remarkably high performance through implicit Just-In-Time (JIT) compilation to machine code.
  • Multi-platform: A browser-based JS application will run on Windows, OSX, Linux, Android, iOS etc, without having to rewrite or rebuild anything. Sharing is as simple as a URL.
  • The JS community is vast, and the available libraries, modules, frameworks, extensions, etc. are correspondingly huge (over half a million!)
  • Web-based apps can easily connect with vast audiences and connect to social and other media networks, to explore collaborative interfaces, to interact with the increasing number of devices supporting HTTP and WebSocket APIs, etc.
  • Stackoverflow's founder famously said (and it seems to be true): "Any application that can be written in JavaScript will eventually be written in JavaScript."
  • Examples:

Tools

All you need to play with JS is:

A good modern web browser

Of all the browser options, Chrome and Firefox are usually at the forefront of developing web technology, such as having support for things like audio signal processing, 3D graphics, VR.

Here's Firefox; made by the not-for-profit Mozilla Foundation.

Here's Chrome, and here's the near-identical open-source alternative, Chromium, both made by Google.

Learn how to open the browser's developer tools console, where JavaScript errors will be posted, where new javascript statements can be immediately written and evaluated, where the HTML DOM can be inspected, etc.

The web browser as a platform

Why code for the browser?

  • The same code can run almost everywhere, without needing to rebuild: Windows, OSX, Linux, iOS, Android, and several others.
  • Through online editors such as Codepen or jsFiddle, you can develop in the browser too, sharing through the cloud.
  • These days, the capabilities of browsers have grown immensely, adding things like audio signal processing, MIDI, high-performance 3D graphics, AR and VR, etc. -- there's not a lot you can't do in the browser.

Codepen / jsFiddle / etc.

Like having a development environment in the cloud, these websites allow you to quickly code up HTML/CSS/Javascript browser code and see the results immediately.

Here's codepen -- Sign up (for free) to be able to save and share sketches.

Learning the JS language:

What version of JavaScript?

At the time of writing, it seems that for the most part it is safe to assume the ES6 version is safe to use. ES6, also known as Ecmascript2015, was a new specification of the JavaScript language that added many valuable new features, making it a significantly more compelling language for general use than it had been before. Originally released in 2015, now most of these features are supported by most browsers, as well as server runtimes such as Node.js.

Evolving browser standards and new features -- can I use them?

Can I use feature X? For example, what ES6 features are supported by different browsers?

JS cheat sheet

Strings

Make a string by bounding with matching quote marks (either single or double):

s = 'a string';
s = "a string";

// Special characters like newlines, tabs, have to be "escaped". Here are the most typical ones:
s = "a string with a \"quote\" in it, a newline: \n, a tab: \t, two slashes: \\ \/, and the character code for A: \u0041 ";

Strings defined in back-tick quotes are different; these are called template strings, because they can embed bits of javascript code to define their content (this is called "string interpolation"). They can also embed formatting like newlines and tabs.

lang = "javascript";
// produces: this is a template string in javascript
s = `this is a template string in ${lang}`;

Strings are immutable but can be concatenated with + and manipulated through methods.

s = "hello " + "world"; // see also String.concat(...), 
"seven".length === 5; 
s = "a".repeat(5) === "aaaaa"; // see also padEnd()/padStart(), 
'cat'.toUpperCase() === 'CAT';  // also toLowerCase()

// substrings:
"hello".slice(1, 3) === "el"  
"hello".slice(-3, -1) === "ll" // i.e. counting back from the end
"hello".charAt(4) === "o"; // remember, count from 0, 1, 2, 3, 4.
// why "char"? a character is a string of length 1. 
"  zzz  ".trim() === "zzz";	// also trimStart(), trimEnd()

// searching:
"hello".indexOf(s, i) /// returns the first index of s, starting at position i. If not found, it returns -1. See also lastIndexOf(), which searches backwards
"hello".endsWith("lo") === true; // also .startsWith()
"hello".includes("hell") === true;
// for more complex searching, see RegExp methods like match(), search(), replace()

// string to array:
"hello".split("") // returns ["h", "e", "l", "l", "o"]
"bananas".split("a") // returns ["b", "n", "n", "s"]
"bananas".split("a", 2) // returns ["b", "n"]

More string methods

Numbers

There is no integer type, all numbers are floats. 10 is the same as 10.0 is the same as 1e1.

// working with whole numbers:
Math.floor(0.5) // 0.0
Math.ceil(0.5)  // 1.0
Number.isInteger(Math.ceil(x)) === true;

// strange numbers:
1/0 === Infinity
Number.isFinite(1/0) === false;
x = Math.sqrt(-1) // returns NaN, which means "Not a Number"
Number.isNaN(x) === true; // this is the safest way to test for NaN
// Note, (NaN == NaN) returns false!!!

function isUsableNumber(v) {
	return typeof v === "number" && isFinite(v);
}

// string <-> number
Number.parseFloat("27") === 27  // returns NaN on failure to parse the string into a number
Number.parseInt("5", 10) === Number.parseInt("0101", 2) === 5 // the 2nd argument is the base; base 2 means binary. returns NaN on failure to parse.
Math.PI.toString() === "3.141592653589793"
Math.PI.toFixed(2) === "3.14"
Math.PI.toPrecision(2) === "3.1"
Math.PI.toExponential(2) === "3.14e+0"

Booleans & truthiness

true and false are the only boolean values, but in most cases JavaScript uses a more permissive sense of truthiness/falsyness, because of automatic type coercion.

falsy values are false, null, undefined, '', 0, NaN; all other values are truthy. An if condition will test for truthiness, not truth. The ! unary operator uses truthiness (not truth), and the boolean operators &&, || also use truthiness.

To explicitly use truth (not truthiness), use the === comparators.

Control flow

The common forms:

if (<cond>) {
	// "then" clause
} else if (<cond2>) {
	// alternate then
} else {
	// all other cases
}

for (<init>; <cond>; <increment>) {
	// body
}

// note: the order is unpredictable.
for (<name> in <obj>) {
	// body
}

while (<expr>) {
	// until expr is falsy
}

Objects

An object maps keys (strings, including '') to values (anything at all except undefined). A key only appears once. Objects collect & organize data. Keys can be added, modified and removed.

var emptyobj = {};
var binarytree = {
	"left": {		// note: quotes around keys are optional
		"left": 10,	// so long as key name is valid syntax
		"right": 20	// for a JavaScript variable name
	},
	"right": {
		"left": 5,
		"right": 13
	}
}

// indexing objects:
binarytree["left"]["left"]; 	// 10
binarytree.left.left; 	// 10
binarytree.middle;		// undefined
binarytree.middle || 7;	// 7
binarytree.middle.middle;	// throw TypeError

// Warning: indexing will also check methods and prototypes:
binarytree.toString;	// returns a function

// adding & removing keys:
emptyobj.x = 10;
emptyobj.x;	// 10
delete emptyobj.x;
emptyobj.x;	// undefined

// objects are passed by reference (i.e. they are not copied):
z = binarytree.left;
z.right = 999;
binarytree.left.right;	// 999

Arrays

JavaScript arrays are objects, with a few special features, and are intended to be indexed by numeric keys. Like objects, array values need not be the same type. Values can be of any type, and arrays can grow and shrink in size (very different to C/C++ arrays).

var emptyarray = [];
var myarray = [
	"zero", "one", "two", "three", "four"
];
emptyarray[1]; 		// undefined
myarray["1"]; 		// "one"
myarray[1]; 		// "one"
emptyarray.length;	// 0
myarray.length;		// 5

for (var i=0; i < myarray.length; i++) {
	console.log(i, myarray[i]);
}

// grow:
myarray[myarray.length] = "five";
myarray.push("six"); 	// myarray is now: 
// ["zero", "one", "two", "three", "four", "five", "six"]
myarray.length;		// 7

// shrink:
myarray.pop();	// returns "six", myarray is now:
// ["zero", "one", "two", "three", "four", "five"
myarray.length = 3;
// myarray is now ["zero", "one", "two"]

// .length is not necessarily to be trusted:
emptyarray[1000] = 1;	
emptyarray.length;	// 1001

Because arrays are really objects, we can add non-numeric properties and methods to individual arrays.

Don't use delete on arrays; it will leave undefined holes in them. Instead use .splice(index, elements_to_remove, elements_to_add):

["a", "p", "q", "c"].splice(1, 2, "b");	// ["a", "b", "c"]

Some common methods of arrays:

array.concat(items...) will create a new array (a shallow copy) and push items onto the end. array.slice(start, end) will create a shallow copy from a sub-section of the array. Do not confuse slice with splice.

array.join(separator) will turn the array into a string.

array.shift() removes and returns the first element, array.pop() removes and returns the last element. array.unshift(item) prepends to the start of the array, array.push(item) appends to the end.

array.sort(comparefunc). The default comparefunc is alphabetic, so it will sort numbers incorrectly. Provide your own comparefunc, which takes two arguments and returns negative if the first item is lower, positive if the second item is lower, and zero if they are equal. E.g.:

// sort numbers properly:
array.sort(function(a, b) { return a - b; });

Multidimensional arrays are just arrays whose values are arrays. E.g. to create a 3x3 array of zeroes:

var z = [
	[0, 0, 0],
	[0, 0, 0],
	[0, 0, 0]
];
// or
var z = [];
for (var r=0; r<3; r++) {
	z[r] = [];
	for (var c=0; c<3; c++) {
		z[r][c] = 0;
	}
}

Note, since arrays are objects, testing for an array needs a different method:

arr = [1, 2, 3];
typeof arr; // "object"
Array.isArray(arr); // true

Functions

  • Functions are first-class objects
  • Functions provide scope

Functions are objects; they can have properties. Functions also store their context. As objects, they can be stored in variables, objects, arrays etc, and passed around as arguments & return values of other functions. For example, passing a function as an argument is commonly done when specifying callbacks.

If a function is a property of another object, it is called a method. Functions themselves can also have methods.

<name> = function <optional name>(<argument list>) {
	<body>
}

add = function(a, b) {
	return a + b;
}

A function can be invoked with more or less values than specified in its definition. Extra arguments are ignored, missing arguments are undefined. When invoked, functions also receive hidden variables this and arguments.

arguments is an array-like object of all the arguments passed to the function, including excess arguments. It is not really an array, and only has the .length property and support for [] indexing. It is there to allow functions with variable numbers of arguments:

sum = function() {
	var i, sum = 0;
	for (i=0; i<arguments.length; i++) {
		sum += arguments[i];
	}
	return sum;
}

Scope

Variable scope

Javascript has three ways to declare a variable (const, let and var). Rule of thumb: Use const whwere possible, and let otherwise. Never use var.

let and const variables have block scope. That means, the variable is visible from that point until the end of the containing block (the closing } brace or end of the script), and cannot be redefined in this scope. That is, within any {block}, code can read and modify variables within its block, as well as any defined above it in outer blocks. Even nested inner functions can access and modify variables defined their outer function block scope -- even if the inner function has a longer lifetime (by being returned).

The difference between const and let: a const cannot be re-assigned to a new value; this can make understanding/debugging code easier.

Older versions of JS only supported declaring variables with var. These have function scope, not block scope. Within a function, the var statement defines the function's private variables, which are visible from the start of the function onwards (i.e. before the actual declaration!), and can even be redefined with additional var statements. This is especially problematic with nested blocks, such as if and for loops, whose variables are also global to the containing function. It can be an easy source of hard-to-identify bugs. AVOID.

classes

Javascript ES6 provides a standardized way to work with classes. A class is a kind of template from which many instances can be constructed; all instances share the same implementation, but have their own unique identity and data. An instance is also an object, so it can have properties inside (these are sometimes called fields or data members). A class can have functions defined inside it, which are known as "methods", which can be called by instances. The unique identity of an instance is referred to via this in the method bodies. A special method name of constructor defines how an instance is created:

class Agent {

	// `constructor` defines what happens whenever `let a = new Agent(x, y)` is called
	constructor(x, y) {
		// store data members under the instance by assinging to `this`:
		this.x = x;
		this.y = y;
		this.radius = Math.random();
	}

	// other methods can be called on instances, e.g. a.move()
	move() {
		this.x += 1;
	}

};

let a = new Agent(10, 10);
let b = new Agent(20, 20);
a.move();
b.move();

Getters & setters are special methods that can be used like instance properties. Static methods are special methods that belong to the class, rather than to the instance:

class Rect {
	constructor(w, h) {
		this.width = w;
		this.height = h;
	}

	// define methods to implement special behaviours for property access:
	get area() { return this.width * this.height; }
	set area(n) { this.width = n / this.height; }

	// define a function within a class; it is not an instance method, but can be used by other methods
	static whatIsThis() {
		return "a rectangle";
	}
}

let r = new Rect(10, 10);
console.log(r.area); // 100
r.area = 20;
console.log(r.width); // 2
// note that static method is called from the class, not from the instances:
console.log(Rect.whatIsThis()) // "a rectangle"

By the way, get and set can also be applied to plain objects too, not just classes/instances.

Inheritance

Classes can extend ("sub-class") another class; inheriting the methods of the parent. Be sure to call super(...) in the constructor.

class Animal { 
  constructor(name) {
    this.name = name;
  }
  speak() { console.log("zzz"); }
  isAnimal() { return true; }
};

class Dog extends Animal {
	constructor(name) {
		super(name);	// run the Animal constructor
	}

	speak() { 
		console.log("woof"); 
		super.speak(); // call a super-class method
	}
}

let a = new Dog();
console.log(a.isAnimal());  // true
a.speak(); // "woof", "zzz"

More weird stuff:

function nameGenerator() {
	return "zap";
}

class Foo {

	// i.e., the same way we can generate property names in regular objects
	[ nameGenerator() ] () {
		console.log("this is a method whose name was generated programmatically!");
	}
}

let f = new Foo();
foo.zap(); // this is a method whose name was generated programmatically!

Each object links to a "prototype" from which properties can be inherited. By default, objects link to Object.prototype. The prototype is used when we index a key that the object doesn't contain; in that case, it automatically tries to index the prototype; and so on until we reach Object.prototype, in which case we return undefined. This is delegation.

  • Note: prototypes are not like strongly-typed classes, as they can be dynamically modified.
  • Use obj.hasOwnProperty(key) to bypass the prototype chain.

Functions can also stand in as classes (the function becomes the class constructor). THhs classes can also inherit from a function: the function becomes the super constructor of the new class. This is good because lots of older pre-ES6 code uses function-based classes.

// Here is a class defined as a function:
// define a function to be a constructor
// typically we capitalize constructor names:
let Quo = function(string) {
	// add "status" property to new object:
	this.status = string;
	// note, no return value needed (the returned object is implicit)
}

// add a method to the new prototype:
Quo.prototype.get_status = function() {
	return this.status;
}

// invoke constructor:
let q = new Quo("confused");

class Vadis extends Quo {
	constructor(stat) {
		super(stat);
	}
}

let v = new Vadis("que?");
v.get_status(); // "que?"

Exceptions

Throw exceptions with throw <exception-object>;, and catch with the try...catch control flow. A throw will immediately exit the current flow. The exception-object can be anything, but typically an object with .name and .message properties for the catch to deal with:

try {
	throw { name: "MyError", message: "It all went horribly wrong" };
} catch (e) {
	console.log("error " + e.name + ": " + e.message);
}

Dynamic evaluation

eval() takes a string, parses it as JavaScript, and runs it. setTimeout and setInterval can also take string arguments and use eval. new Function similarly compiles arbitrary code. This capability is extremely powerful, but very very dangerous and difficult to debug.

JSON

JSON (Javascript object notation) is a widely-used text format for storing and streaming data, and is a sub-set of the notation used to declare objects in Javascript. However, it is less permissive, for example object key names must be quoted:

{ "one": 1 }

Most JS environments now provide JSON.parse() and JSON.stringify() wich convert between JSON strings and Javascript objects, and vice versa.

The weird this

What this refers to depends on how the function was invoked:

  • as a method: this refers to the object invoking its method.
  • as a constructor (using new): this refers to the new object being created.
  • via function.apply(): this is the first argument of apply (the second argument is a list of arguments passed to the function)
  • via function.call(): this is the first argument of call (the remaining arguments are passed to the function)
  • as a function: this refers to the global object. This is a major cause of bugs! In particular, calling a constructor without using new is a common mistake that leads to a terrible mess.
    • Capitalize constructors to help you notice these bugs

Functions can be defined anywhere an expression can appear, even inside other functions (in which case they can access private variables of the outer function). However, because of the way this is defined, inner functions do not inherit this from the outer function. The usual workaround is as follows:

myobj.outer = function() {
	let that = this;	// make a local reference
	let inner = function() {
		// can't use "this" here, as it would refer to the global object
		that.value = that.value + that.value;
	}
	
	inner();
}

If you can understand this, then you have mastered one half of JavaScript's complexity.

Regular Expressions

A regular expression specifies the syntax of a simple (type-3) language. They are used to search and replace information from strings. RegExps are usually fast, but are difficult to write (and read!), which means they are common sources of bugs.

A pattern is a template for text that may have fixed and optional or variable components. Patterns either match a strong, or they don't. A capture within a pattern identifies the part of the pattern that we want to return.

Regular expressions are denoted with / delimiters:

var r = /pattern/;

The language of patterns is really hard to remember and to debug. Recommend using an online Regex editor to build up your patterns, such as https://regexr.com.

  • anchors:
    • ^ at the start anchors the pattern at the beginning of the string. Without it, the pattern can skip characters until it finds a matching start point.
    • $ at the end anchors the pattern at the end of the string
  • character classes:
    • . matches any character
    • [...] describes a set, range or both of characters. E.g. [A-Za-z] matches all alphabetic characters.
    • [^...] as above, but negative: matches any character not in the set described by ...
    • \... is an escape: it is used to match literals such as /, , ^, $, +, -, *, |, ?, ., {, }, [, ], (, ), etc. that would otherwise be interpreted as regular expression syntax
    • \d matches any digit, i.e. the same as [0-9]
    • \n matches a newline, \t matches tab, \u0041 matches "A", etc.
    • \s matches most whitespace characters
    • \w matches alphabetic and numeric characters
    • \1 matches whatever was captured in group 1, etc.
    • any other characters are literals -- the same character must appear in sequence in the input
  • modifiers:
    • ? as a suffix means the preceding pattern is optional
    • * as a suffix matches the preceding pattern zero or more times
    • + as a suffix matches the preceding pattern one or more times
    • {3} as a suffix matches the preceding pattern a specific number of times, in this case 3 times.
    • {0,3} as a suffix matches the preceding pattern a specific number of times, in this case 0, 1, 2, or 3 times.
    • a|b the vertical bar | means either a or b will match
  • captures & groups:
    • (...) means the pattern in ... should be captured.
    • (?:...) means the pattern in ... is a non-capturing group. It is useful for applying prefix/suffix modifiers to a group without capturing them.
  • flags (placed after the final /):
    • i: ignore case throughout, i.e. e matches both e and E.
    • m: multi-line, ^ and $ can match line-end characters
    • g: global, can match multiple times

Use regex.exec(string) to apply the pattern to a string; it will return an array of captures.

Use regex.test(string) to simply test for matches; it will return true or false.

string.match(regexp) applies a regular expression to a string. If the g flag is given, it produces an array of all the matches (instead of an array of captures). Otherwise it is the same as regex.exec.

string.replace(s, v) replaces all instances of s with v. s can be a regular expression. If s is a string, it only replaces once. v can be a string or a function; if it is a function, it is called for each match and the result used as the replacement. If it is a string, the "$" character has special meaning:

  • $& the matched text
  • $1 capture group 1
  • $$ the "$" character

string.search(regex): like string.indexOf.


The bad parts

Some older features of JS still exist, for backward compatibility, but are highly discouraged. Others simply can't be avoided, but should be borne in mind, as they can be the cause of bugs.

True and false are approximate. Use === not ==, !== not != etc.

JS has "truthy" and "falsy" values. Falsy values include the number 0, the string "", the values null and undefined and NaN, as well as the value false. Everything else is truthy.

Also, the string "7" is equal to the number 7 in JS's approximate sense, so (7 == "7") returns true, whereas it is false in the strict equality test of (7 === "7").

Types are weird

JavaScript primitive types are: number, string, true, false, null, undefined, and object. The object type includes Function, Array, RegExp, user-defined classes, etc. The weirdness is that typeof x might return "object" for things like an array. To detect if an object is an array, use Array.isArray(x).

Floating point numbers are weird

All kinds of unexpected fun can happen from the fact that floats cannot accurately represent many numbers. This isn't a JS problem as much an IEEE problem, and affects most programming languages used today.

But it can be surprising, e.g. 0.1 + 0.2 === 0.3 returns false. (To test things like this, consider looking for small differences, e.g. Math.abs(0.1 + 0.2 - 0.3) < 0.0001. Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER return the smallest and largest whole numbers that can be represented. Number.MIN_VALUE, Number.MAX_VALUE is the smallest and largest float numbers that can be represented.

Also because of IEEE, NaN !== NaN, which is really weird.

Inheritance is weird

  • Iterating objects may pick up properties and methods from the prototype chain.
    • Use .hasOwnProperty().

Other tips

Generally, JavaScript tries to make things easier for you, which eventually makes you confused.

  • Optional syntax means that mistakes are often not found by the parser
    • Always put semicolons, always wrap blocks in { }.
  • There are lots of bad syntax possibilities and bad library functions in JavaScript that you will find in production code and online tutorials. Don't trust what you find.

On the other hand, regular expressions are hard to read and write, causing many bugs (and security vulnerabilities!)