JavaScript Update
Notes on modern additions to JavaScript.
Use Strict
Adding this declaration makes JavaScript a bit less dodgy.
"use strict"; thing = 10; // No assigning to undeclared variables. if (true) { // No function declarations inside blocks. // This is dodgy because it's confusing: // functions are hoisted anyway in JavaScript. function shouldBreak() { console.log("This shouldn't work in strict mode."); } shouldBreak(); var shouldWork = function() { console.log("This should work in strict mode."); } shouldWork(); } (function globalThis() { // Functions which are not methods and not bound // don't get the global object as their /this/ object in // strict mode. console.log("The /this/ object should be undefined", this); })(); delete undeclared; // Deleting unqualified variables is disallowed. delete this.undeclared; // Deleting qualified variables still allowed. console.log(010); // Octal numbers disallowed. // Variables declared inside eval aren't leaked. eval("var leaked = 10"); console.log("leaked should be undefined", leaked); with ({ x: 1}) { // With is disallowed. console.log("Shouldn't get here", x) } "hello".length = 10; // Setting immutable properties disallowed.
Let and Const
JavaScript variables are normally function-scoped.
The let keyword makes them block-scoped (so if-else, for and while constructs get a new scope). The const keyword does the same, and also prevents rebinding that variable.
Modules
A file is considered to be a module.
You can write export in front of a variable or named function to export it.
Another file can import specific values from that module using import statements:
import * from 'someModule'; import {A as a, B as b} from 'someModule'; // There are other options, but these cover most of it.
Numbers
ES6 (2016) added an exponentiation operator.
console.log(2 ** 8);
256
Strings
You can make a multiline string by ending a line with a \
.
String literals can be written using backticks instead of quotes. These strings are multiline, and get expression interpolation:
console.log(
`${2 ** 8}`
);
256
There are some new string padding methods. So hey, maybe something good came out of all the left-pad trouble:
console.log( "hello".padStart(10, "oi"), "goodbye".padEnd(13, "ai"), );
oioiohello goodbyeaiaiai
Template Literals
These are for building DSLs. They let you define a string which gets passed to a function for processing in both raw and interpolated/escaped forms.
Regular Expressions
Regular expression literals are surrounded by forward slashes.
/[0-9]+/; // regex which matches numeric digits.
Sets and Maps
Set behaves like sets do in most other languages: values are
deduplicated. Uniqueness is tested using ===
, except for NaN,
which is equal to itself (this behaviour differs from ===
).
Map is a better dictionary than a normal object. You can use any values for the keys. Iteration order is guaranteed to be the same as insertion order.
It uses the same equality testing for keys as Set does.
There are WeakSet and WeakMap equivalents, for if you need to keep a reference to something, but still allow it to be garbage collected. Useful for implementing caches.
Arrays
Several useful functions were added in ES5, which let you do some basic functional programming.
Array.isArray()
Array.prototype.every()
Array.prototype.filter()
Array.prototype.forEach()
Array.prototype.indexOf()
Array.prototype.lastIndexOf()
Array.prototype.map()
Array.prototype.reduce()
Array.prototype.some()
- checks if thing is one of the array's (or object's) values.
We can abbreviate Array.prototype.someMethod.call()
as [].someMethod.call()
;x
These are useful, but quite limited. Consider using Lodash.
JSON
JSON.parse()
and JSON.stringify()
were both new in ES5.
Functions
We were always able to write anonymous functions:
console.log( [1, 2, 3].map(function(a) { return a * 2; }) );
[ 2, 4, 6 ]
Now we can also write them with a fat arrow syntax.
console.log( [1, 2, 3].map((a) => a * 2) );
[ 2, 4, 6 ]
Arrow functions don't have this and arguments special parameters. If you use these, they'll come from the enclosing scope.
Functions can now also have default parameters:
(function(a = "hello") { console.log(a); })();
hello
Parallelism
You can now allocate shared memory using SharedArrayBuffer. You make a clone of the object produced, and then give it to some other thread (usually a Worker).
const myCopy = new SharedArrayBuffer(sizeInBytes); const yourCopy = {myCopy}; // Shouldn't really keep this constant around. someWorker.postMessage(yourCopy);
The Atomics object has some functions to let you make synchronized updates to this shared memory.
Setting and getting:
Atomics.load(typedArray, index)
Atomics.store(typedArray, index, value)
- returns the current value
- like exchange, but checks if the current value is what we expected first.
Notifications:
Atomics.wait(typedArray, index, value, timeout)
- wait at
typedArray[index]
, as long astypedArray[index] === value
now. Atomics.wake(typedArray, index, value)
- trigger waiting workers
Arithmetic and bitwise operations on (typedArray, index, value):
- add
- sub
- and
- or
- xor
The typed arrays here must be backed by a SharedArrayBuffer
. They must be aligned correctly, and the sizes must all be correct.
At the moment, the only type of array allowed is Int32Array.
Objects
By default in JavaScript, an object's prototype is decided by the
prototype property of its constructor. (For object literals, this is
Object.prototype
.)
This is needlessly complicated, but they wanted it to look like Java.
Make sure not to return anything (other than possibly this
) from
your constructor function. If you do, it will become your prototype,
and everything you did to this
will be ignored.
"use strict"; const C = function() { this.urgh = "argh"; }; C.prototype.yaaaarp = function() { return "I've got a brand new combine harvester."; }; const c = new C(); console.log("prototype on constructor:", C.prototype); console.log("prototype on instance:", Object.getPrototypeOf(c)); console.log( "constructor property on prototype property on constructor:", C.prototype.constructor.name ); console.log("constructor on instance:", c.constructor.name); console.log("Property set in constructor function exists on instance:", c.urgh); console.log( "Property set in constructor function missing from prototype:", Object.getPrototypeOf(c).urgh ); console.log("Method on prototype available to instance:", c.yaaaarp()); console.log( "Don't return anything from the constructor:", new ( function D() { return {}; } )().constructor.name );
prototype on constructor: C { yaaaarp: [Function] } prototype on instance: C { yaaaarp: [Function] } constructor property on prototype property on constructor: C constructor on instance: C Property set in constructor function exists on instance: argh Property set in constructor function missing from prototype: undefined Method on prototype available to instance: I've got a brand new combine harvester. Don't return anything from the constructor: Object
Accessing the prototype (use instead of __proto__
):
Object.create(prototype, propertyDescriptors)
- create an object with the given prototype. See property descriptors below.
Object.getPrototypeOf(thing);
Object.setPrototypeOf(thing, newPrototype);
- this is a slow operation.
Classes
EcmaScript 2015 added class-based inheritance. This is just some syntactic sugar over prototypical inheritance.
In fact, classes are just functions and you can assign them to variables.
We have single-inheritance, static methods (also inherited).
However, you can make a function that takes a class and returns a new class extending it. You can therefore make a style of mixin which is actually just a really long generated inheritance chain.
class MyLovelyWhat { constructor(what) { this.what = what; } toString() { return `My lovely ${this.what}`; } static describe() { return "Static method on the MyLovelyWhat base class"; } speak() { } } // Classes are actually just a special kind of function, so you can // assign them to variables too. const MyLovelyHorse = class extends MyLovelyWhat { constructor() { super("horse"); } speak() { return "Neigh!"; } } const lovely = new MyLovelyHorse(); console.log(`${lovely.toString()}, running through the fields.`); console.log(`Horse says ${lovely.speak()}`); console.log(`What says ${new MyLovelyWhat("Huh").speak()}`); // Static methods *are* inherited. console.log(`What class calls ${MyLovelyWhat.describe()}`); console.log(`Horse class calls ${MyLovelyHorse.describe()}`);
My lovely horse, running through the fields. Horse says Neigh! What says undefined What class calls Static method on the MyLovelyWhat base class Horse class calls Static method on the MyLovelyWhat base class
Properties
There are three main types of properties:
- data properties
- these are the normal ones
- accessors
- with getters and setters
- internal properties
- can't touch this
To look at an object's properties:
Object.keys(thing)
- returns s list of keys.
Object.values(thing)
- returns a list of values.
Object.entries(thing)
- returns a list of (key, value) pairs.
Object.getOwnPropertyDescriptors(thing);
- gives you some helpful reflection information.
A property descriptor looks like this:
{ value: "whatever", writable: true, // can you change the property's value? enumerable: true, // if this is false, hide the property from some operations // if this is false, you can't delete the property or change // anything about it except for its value configurable: true }
We can modify some of these things:
Object.defineProperty(thing, name, propertyDescriptor)
- add a property to an object. There's also a
defineProperties(thing, propertyMap)
batch variant. Object.preventExtensions(thing);
- no more properties on this object, but you can still delete them.
Object.seal(thing);
- as above, but you can't delete or configure properties either. This is something like locking the object to a class.
Object.freeze(thing);
- as above, but all properties are made read-only.
Accessors
These are a lot like C# properties, having a getter and an optional setter.
Some of the special new things in JavaScript are implemented using these.
var thing = { // Inherits from Object get bleh() { // Careful not to use the same name here — it's easy to set up // an infinite recursion by mistake. return this._bleh || "meh"; }, set bleh(bleh) { this._bleh = bleh; } }; console.log(thing.bleh); thing.bleh = "heh"; console.log(thing.bleh);
meh heh
Accessors have slightly different property descriptors to data properties:
{ get: function() { return "whatever"; }, // missing set function means this property is immutable enumerable: true, configurable: true }
Iteration
Iterable objects have a Symbol.iterator
method. You need to use thing[Symbol.iterator]
to get to this: its name interferes with the normal dot syntax.
Calling this method gets you an iterator. This has a next()
method, which gives a you an object: {result: something, done: false}
.
There's a new for-of syntax which loops over iterators, must like foreach loops in other languages.
You can make a function into a generator by putting an *
after it and using the yield keyword.
Lastly, there's a special yield* someOtherGenerator()
call which yields all the things that generator would yield.
function* myThing() { yield 1; yield 2; yield 3; } function* myOwningThing() { yield* myThing(); yield 4; yield 5; } for (const x of myOwningThing()) { console.log(x); }
1 2 3 4 5
Asynchrony
JavaScript has gotten a new Promise object, which tells you the state of a computation which may not have happened yet. This has some methods:
.then(result => {})
- when computation finishes
.catch(error => {})
- when computation fails
.finally(() => {})
- resource cleanup after one of then and catch has happened
It's also gotten a new async keyword which can be put in front of a function, to declare that it may give up control.
Inside an async function declaration, you are allowed to use the await keyword. You write this in front of another async operation. When execution reaches this point, we schedule that operation and yield control.
Asyncronous Iteration
We can make an asynchronous version of an iterable, which will have a Symbol.asyncIterator
method on it to make an asynchronous iterator.
The next()
method on this asynchronous iterator is asynchronous and returns Promises, where a normal iterator just returns values straight away.
This means that we can use this next method together with the await keyword.
There is also a new for-await-of loop which mirrors the for-of loop, and lets us easily loop over an asynchronous iterable.
Asynchronous iterators can also take advantage of the yield* someOtherGenerator();
syntax.
(These features only available in NodeJS 10 and later, or NodeJS 9 with the harmony flag node --harmony my-file.js
.)
async function* myAsyncThing() { yield 1; yield 2; yield 3; } const it = myAsyncThing(), itFn = (d) => { console.log("Fiddly manual async iteration", d); return it.next(); }; const finalPromise = it.next().then(itFn).then(itFn).then(itFn); async function awaitExample() { const iterable2 = myAsyncThing(); console.log("Using 'await'", await iterable2.next()); console.log("Using 'await'", await iterable2.next()); console.log("Using 'await'", await iterable2.next()); for await (const x of myAsyncThing()) { console.log("Using 'for await'", x); } } const finalFunctionPromise = awaitExample();
Destructuring
You can use ...
to mean "all the remanining properties" on either the left-hand side of an assignment, in an object or array literal, or in function arguments.
This is called gathering and spreading depending on context, since it can either destructure an object or array, or it can flatten it to make construction more succinct.
// Array destructuring — works on any iterable const arr = [1, 2, 3, 4, 5]; const [first, second, ...rest] = arr; console.log("Array destructured into variables", first, second, rest); console.log("Variables flattened into array", [first, second, ...rest]); (function(a, b, ...c) { console.log("Function rest argument", a, b, c); })(1, 2, 3, 4); // Object destructuring const thing = { "x": 1, "this": 2, "that": 3, "t'other": 4 }; const {x, ...whatever} = thing; console.log("Object destructured into variables", x, whatever); console.log("Variables flattened into object", {x, ...whatever}); (function(a, b, {x, y, ...others}) { console.log("Function object destructuring", a, b, x, y, others); })(-1, 0, {x: 1, y: 2, feh: 3, bleh: 4, meh: 5});
Array destructured into variables 1 2 [ 3, 4, 5 ] Variables flattened into array [ 1, 2, 3, 4, 5 ] Function rest argument 1 2 [ 3, 4 ] Object destructured into variables 1 { this: 2, that: 3, 't\'other': 4 } Variables flattened into object { x: 1, this: 2, that: 3, 't\'other': 4 } Function object destructuring -1 0 1 2 { feh: 3, bleh: 4, meh: 5 }
It's obviously possible to make a bit of a mess here, but it also makes the option object pattern a bit neater.
There's also a tiny bit of extra helpful syntax for when you want to make a key with the same name and value as some other variable:
const thing = {x: 1, y: 2}; console.log("Inserting variable into a key of the same name", {thing}); // This is helpful for cloning objects: console.log("Cloned", thing);
Inserting variable into a key of the same name { thing: { x: 1, y: 2 } } Cloned { x: 1, y: 2 }
JavaScript Testing
I quite like tape.