A Brief Javascript Tutorial: Part 2
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;
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!
Comments
Post a Comment