A Brief JavaScript Tutorial: Part 2 0.9

A BRIEF JavaScript TUTORIAL: PART 2

In the previous chapter we covered everything that you need to know to follow through to the end of Section One.

In Section Two, we’ll create a professional quality, production-ready three.js application that we can use as a template for creating applications of any size.

We’ll also be fully embracing modern JavaScript. In particular, that means using classes and splitting our code up into small pieces called modules.

Classes in JavaScript

In the previous chapter, we introduced the new keyword and showed how it related to constructor functions.

function Person ( name, age ) {

  this.name = name;
  this.age = age;

}

const elle = new Person( 'Eloise', 96 );

In ES6, this concept for formalized into classes, making for a much clearer syntax:

class Person {

  constructor( name, age ) {

    this.name = name;
    this.age = age;

  }

  printName() {

    console.log( name );

  }

}

const elle = new Person( 'Eloise', 96 );

There’s a lot more to classes in JavaScript than this, of course, and if you’re completely new to the concept you may need to do some extra research to follow a couple of chapter in Section Two. However, for the rest of the book, you’ll be fine.

ES6 Modules

In Section One, we’ll put all of our JavaScript into one app.js file, which was fine while it’s just a few lines long. As our code grows in complexity though, we’ll need to rethink this approach. There have been quite a few different solutions put forward over the years, but fortunately, since the advent of ES6, we can use the built-in solution, which makes use of the import and export keywords.

Using this, we can split functionality into separate files. The simplest method is to use default exports, which allows us to export a single object such as a variable, function, array, or object, from a file:

// sum.js

export default function sum ( a, b ) {

  return a + b;

}

Then we can import the sum() function into another file and use it there. Note that it doesn’t have a fixed name since we used export default, so we can call it whatever we like in the other file.

// main.js

import add from './sum.js';

const y = add ( 2, 3 ); // y = 5

If we need to export more than one thing from a file, or if we want to make sure that the name cannot be changed, we can use named exports. Note that there are a number of ways of writing these exports, but we’ll just look at one here:

// math.js

const sum = ( a, b ) => a + b;

const sub = ( a, b ) => a - b;

export { sum, sub };
// main.js

import { sum, sub } from './sum.js';

const y = sum ( 2, 3 ); // y = 5
const z = sub ( 2, 3 ); // z = -1

Alternatively, we can import everything at once from a file:

// main.js

import * as mathUtils from './sum.js';

const y = mathUtils.sum ( 2, 3 ); // y = 5
const z = mathUtils.sub ( 2, 3 ); // z = -1

This approach is commonly taken with three.js:

import * as THREE from 'three.module.js';

Just as with classes, there’s a lot more to import and export than this, but again, you won’t need to know more than the basics to follow along with the code used here.

Code Bundling

Once we’ve created these modules, we’ll generally need to combine them back into a single file to be used by our website. In a professional web application, several things are done at this stage to make the file as small and lightweight as possible - for example, the comments are stripped out, the code may be “minified” which means rewriting it to be as small as possible, including renaming variables and removing unneeded spaces and punctuation.

Popular bundling tools include Parcel, Rollup, or WebPack, and we’ll take a look at each of these in Section Two.

Browser Modules

Until very recently, in order for the browser to understand modules, they would need to be bundled back into a single file using the above technique - and in a production-ready app, we’ll still need to do that for the foreseeable future.

For the sake of our experiments in this book though, as long as we’re using an up to date browser we can use browser modules, meaning that we can tell the browser to use the script as a module. Doing this is very simple, we just need to add type="module" to the <script> element

<script type="module" src="path/to/main.js"></script>

Once we’ve done this, code with import and export statements will run directly in the browser!

Note Regarding Codesandbox.io

By default, Codesandbox automatically bundles modules for us using Parcel and ignores the type="module" on the <script> element.

This means that we can get the best of both worlds here. If we download any of the examples and run them locally on an up to date browser, they will run using browser modules, while if we use them in Codesandbox then they will be bundled and be guaranteed to work on older devices that don’t support browser modules.

This means that we can share the CodeSandbox URL with friends and colleagues and be confident that it will work as expected.

The Spread Operator

Suppose that we’ve made an array of objects:

const objects = [ cube, sphere, tetrahedron ];

Normally, to add them to our scene, we would have to do this:

scene.add(

  objects.cube,
  objects.sphere,
  objects.tetrahedron,

);

The spread operator is three dots: ... and allows is to do the above with a more concise syntax - in other words, it spreads out the array:

scene.add( ...objects );

Combining Objects with Spread

We can do something similar to combine two objects. We’ll use this to overwrite an object containing default parameters with our custom parameters:

const defaults = {
  color: 'red',
  size: 'large'
}

const custom = {
  color: 'blue',
}

const final = { ...defaults, ...custom }

We can combine any number of objects in this manner, and the values to the right will take precedent - in this case, that means that the final object will look like this:

final = {
  color: 'blue',
  size: 'large'
}

That is, the default red will get overwritten by the custom blue.

Looping Over an object’s Values Using Object.values

In the previous section, we showed how to loop over an array using Array.forEach:

const arr = [ 1, 2, 3, 4 ];

let sum = 0;
arr.forEach( ( element ) => {

  sum += element;

} );

// now sum = 10

There are times when it would be useful to do something similar with an object. Unfortunately, there’s no such thing as Object.forEach, so we’ll need to use this unwieldy approach:

const catWeights = {
  ginger: 1,
  gemima: 3,
  geronimo: 30
}

let totalWeight = 0;
Object.values( catWeights ).forEach( ( value ) => {

  totalWeight += value;

} );

The trick here is that Object.values( numbers ) will return an array with the object’s values, and then we can use forEach as normal:

const x = Object.values( catWeights ); // x = [1, 2, 3]

let totalWeight = 0;
x.forEach( ( value ) => {

  totalWeight += value;

} );

Asynchronous JavaScript: Promises and Async/Await

Callback functions, such as the one we introduced at the end of Section One, were until recently the only way to write asynchronous code in JavaScript.

If you recall, performing an asynchronous operation, such as loading a model, using a callback function, looks like this:

// normal "synchronous" code
const x = 23;
const y = 4;

// Now we come to an operation that will take a long time so we switch to
// "asynchronous" code
const onLoadCallback = ( myModel ) => {

  addModelToScene( myModel );

};

loadModel( 'path/to/model.file', onLoadCallback );

// and now we switch back to synchronous code while waiting for the model to load
const sum = x + y;

The problem with this code style is that it makes it very hard to write clean, modular code. For example, it’s common for most of the functionality of your app to need the model to be loaded to work, which means that you onLoadCallback function will just keep growing in size, and end up ugly and hard to maintain.

Also, the myModel variable will not be accessible outside of the callback, and, if you are like most people creating three.js apps, that will consistently surprise and frustrate you:

const onLoadCallback = ( myModel ) => {

  scene.add( myModel );

};

loadModel( 'path/to/model.file', onLoadCallback );

addAnimationToModel( myModel ); // No! This won't work

It doesn’t matter how many fancy tricks you try in your code, there’s no way to get the above code to work in a clean way that supports modules.

Wouldn’t it be great if we could somehow load the model and use it immediately in the normal way:

const x = 23;
const y = 4;

const myModel = loadModel( 'path/to/model.file' );

scene.add( myModel );

const sum = x + y;

It turns out that since the introduction of Promises and Async/Await syntax in ES6, there is a way!

There are a couple of steps required to make this work. First of all, it will only work inside a function, so we’ll need to rewrite our code like this (taking out everything except model loading for clarity):

function init() {

  const myModel = loadModel( 'path/to/model.file' );

  scene.add( myModel );

}

init();

That’s not a big deal since we would have been doing that anyway to keep our code clean. Next, we’ll need to mark the init() function as async, and tell our code to await the result of loadModel:

async function init() {

  const myModel = await loadModel( 'path/to/model.file' );

  scene.add( myModel );

}

init();

And that’s it! We can now write the rest of our code exactly as before and let the async and await take care of the complexities for us!

…except that it’s not, quite. You see, unfortunately, the three.js loaders are kind of old fashioned and are not designed to work with async/await style code. That means that we’ll need to create a helper function createAsyncLoader, which will turn the loader into an async loader that we can use in the above style. Using this, our final code will look like this:

import createAsyncLoader from './vendor/utility/createAsyncLoader.js';

const loadModelAsync = createAsyncLoader( loadModel );

async function init() {

  const myModel = await loadModelAsync( 'path/to/model.file' );

  scene.add( myModel );

}

init();

We’ll cover all of this in more detail in Chapter

This createAsyncLoader, while just a couple of lines of code in total, is probably the most complex piece of code we’ll write in this book. The important thing to know, is that you don’t need to understand it in order to be able to use it.

This applies to a lot of the code that we’re using here, but especially the asynchronous code. If you’re new, or even not so new, to JavaScript, it will take you some time to fully and intuitively understand this and in the meantime, you can go right on ahead and get all the benefits of using async/await syntax.

Typed Arrays

Typed arrays are very similar to normal JavaScript arrays, with a couple of restrictions that allow for increased performance.

three.js uses them under the hood and lot, mainly in the creation of Geometry, using them to hold large collections of similar data specifying things such as positions in 3D space. We’ll use them directly in Section Eight, once we come to creating custom geometries.

There are quite a few kinds of Typed Arrays. For example, the Uint8Array, which holds unsigned integer values of 8 bits - this means it can hold values between 0 and 255, inclusive.

We’ll be using the Float32Array, meaning that the values it stores will be interpreted as 32-bit floating point numbers.

We can create a new Float32Array like this:

const arr = [ 1, 2, 3, 4, 5, 6 ];

const typedArr = new Float32Array( arr );

Or, we can create an empty Float32Array of length 6 like this:

const typedArr = new Float32Array( 6 );

The main important difference, aside from increased performance, between typed arrays and normal arrays, is that once the length is fixed once created. Both of the typed arrays we created above have length 6 and this cannot be changed after they have been created.

However, aside from this, we can treat them like normal JavaScript arrays, accessing elements by index and using forEach and other methods:

const typedArr = new Float32Array( [ 1, 2, 3, 4, 5, 6 ] );

const x = typedArr[ 3 ]; // x = 4

let sum = 0;

typedArr.forEach( ( value ) => {

  sum += value;

} );

// sum = 21

However, typed arrays don’t have any array methods that would change their size, such as push()

const typedArr = new Float32Array( [ 1, 2, 3, 4, 5, 6 ] );

typedArr.push( 7 ); // No!