Creative Coding I: Design & Communication
Prof. Dr. Lena Gieseke | l.gieseke@filmuniversitaet.de | Film University Babelsberg KONRAD WOLF
Script 04 - JavaScript
- Creative Coding I: Design \& Communication
- Script 04 - JavaScript
Introduction
JavaScript, often abbreviated as JS, is an interpreted programming language (meaning it runs as is and you donāt need to compile it to execute your code), which allows you to implement complex things on web pages. Every time a web page does more than just sit there and display static information for you to look at, e.g., displaying timely content updates, interactive maps, animated 2D/3D graphics, scrolling video jukeboxes, etc., you can bet that JavaScript is probably involved. Alongside HTML and CSS, JavaScript is one of the three core technologies of the World Wide Web (we will come back to this). The vast majority of websites use JS, and all major web browsers have a dedicated JavaScript engine to execute it.
As a multi-paradigm language, JavaScript supports event-driven, functional, object-oriented and prototype-based programming styles (donāt worry about this if you donāt understand). Although there are strong outward similarities between JavaScript and Java, including language name, syntax, and respective standard libraries, they are two distinct languages are and differ greatly in design.
Please note that in class we only use modern JavaScript based on ECMAScript 2015.
Resources
Tutorials and References
I mainly use two resources for Javascript:
- The Modern JavaScript Tutorial
- In detail, easy to follow explanations š
- In my scripts I often just copy&paste from this tutorial (with the reference given, of course)
- MDN JavaScript Reference
- As reference
- There are also tutorials and explanations on specific example scenarios
- A bit hard to navigate
An interesting read is also Eloquent JavaScript. But it is lengthy and meant to be read as a whole.
Strict Mode
This is a directive for using modern JavaScript. It has to be at the top of the script. Always use it as āmodernā mode changes the behavior of some built-in features and not using strict mode might lead to unexpected behavior.
All examples in class assume strict mode, unless (very rarely) specified otherwise. In p5 we donāt care about this for now.
Semicolons
A semicolon should be present after each statement, even if it could possibly be skipped.
There are languages where a semicolon is truly optional and it is rarely used. In JavaScript, though, there are cases where a line break is not interpreted as a semicolon, leaving the code vulnerable to errors.
If youāre an experienced JavaScript programmer, you may choose a no-semicolon code style like StandardJS. Otherwise, itās best to use semicolons to avoid possible pitfalls. The majority of developers put semicolons.
On a side note: Once in while I might forget to put a semicolon. That doesnāt mean that you should too.
Resources
Variables
Dynamic Typing
JavaScript is a loosely typed or a dynamic language. Variables in JavaScript are not directly associated with any particular value type, and any variable can be assigned (and re-assigned) values of all types:
let foo = 42; // foo is now a number
foo = 'bar'; // foo is now a string
foo = true; // foo is now a boolean
Variable Definition
There are three ways to declare a variable:
let
const
var
let
and const
behave exactly the same way, except that const
variables cannot be reassigned.
But var
is a very different beast, that originates from old times. Itās generally not used in modern scripts, but it actually just has a different functionality. For now just remember to use let
.
There are two main differences of var
:
- Variables have no block scope. They are either function-wide or global and are visible through blocks.
- Variable declarations are always processed at function start.
[The old āvarā] [Using variable declarations to improve readability]
Data Types
There are 7 basic types in JavaScript.
number
for numbers of any kind: integer or floating-pointstring
for strings. A string may have one or more characters, thereās no separate single-character typeboolean
for true/falsenull
for unknown values ā a standalone type that has a single value nullundefined
for unassigned values ā a standalone type that has a single value undefinedobject
for more complex data structures- All other types are called primitive because their values can contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store collections of data and more complex entities.
symbol
for unique identifiers
The typeof
operator allows us to see which type is stored in a variable.
- Two forms:
typeof x
ortypeof(x)
- Returns a string with the name of the type, like āstringā.
- For null returns āobjectā ā this is an error in the language, itās not actually an object.
console.log(typeof 42);
// expected output: "number"
console.log(typeof 'blubber');
// expected output: "string"
console.log(typeof true);
// expected output: "boolean"
console.log(typeof declaredButUndefinedVariable);
// expected output: "undefined";
Equality Check
A regular equality check ==
has a problem. It cannot differentiate 0
from false
:
console.log( 0 == false ); // true
The same thing happens with an empty string:
console.log( '' == false ); // true
This happens because operands of different types are converted to numbers by the equality operator ==
. An empty string, just like false, becomes a zero.
What to do if weād like to differentiate 0 from false?
A strict equality operator ===
checks the equality without type conversion.
In other words, if a
and b
are of different types, then a === b
immediately returns false without an attempt to convert them.
console.log( 0 === false ); // false, because the types are different
There is also a āstrict non-equalityā operator !==
analogous to !=
.
The strict equality operator is a bit longer to write, but makes it obvious whatās going on and leaves less room for errors.
The Modern Javascript Tutorial: Data Types
Resources
Data Structures
In JavaScript mainly objects and arrays (which are a specific kind of object) provide ways to group several values into a single value.
Objects
Objects are associative arrays with several special features. Most objects in JavaScript have properties, the exceptions being null
and undefined
.
Definition
Properties are stored as key:value pairs, where:
- Property keys must be strings or symbols (usually strings).
- Values can be of any type.
let object_name = {
key1: value1,
key2: value2
}
let user = { // an object
name: "Sully", // the key "name" stores the value "Sully"
age: 30 // the key "age" stores the value 30
};
This is the same as:
let user = { name: 'Sully', age: 30 };
Accessing Properties
To access a property, we can use:
obj.property
- The dot notation
user.name;
obj['property']
-
Square brackets notation
user['name'];
-
Square brackets allow to take the key from a variable
let currentKey= 'name';
user[currentKey];
Additional operators
- To delete a property:
delete obj.prop
- To check if a property with the given key exists:
'key' in obj
- To iterate over an object:
for(let key in obj)
loop
let user = { name: "Sully", age: 30 };
console.log('name' in user); //true
for(let key in user)
{
// keys
console.log( key );
// console output: name, age
// values for the keys
console.log( user[key] );
// console output: Sully, 30
}
If you assign a value to a key that doesnāt exist in the object, the property will be automatically added:
let user = { // an object
name: "Sully", // by key "name" store value "Sully"
age: 30 // by key "age" store value 30
};
user.hobby = 'singing';
// now user is { name: 'Sully', age: 30, hobby: 'singing' }
You can test for the existence of a key with:
"key" in object // true if property "key" exists in object
Nested Objects
let user = {
name: 'John',
age: 20,
// nested object
preferences: {
color: 'dark',
tabs: 5
}
}
// accessing property of user object
console.log(user.preferences); // {color: 'dark', tabs: 5}
// accessing property of the preferences object
console.log(user.preferences.tabs); // 5
Values By Reference
Objects are assigned and copied by reference. In other words, a variable stores not the object value, but a reference (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object. All operations via copied references (like adding/removing properties) are performed on the same single object.
let user = { name: 'Sully' };
let admin = user;
admin.name = 'Pete'; // changed by the "admin" reference
console.log(user.name);
// console output: 'Pete' -> changes are seen from the "user" reference
To make a āreal copyā (a clone) we can use for example
Object.assign(dest[, src1, src2, src3...])
- Arguments
dest
, andsrc1
, ā¦,srcN
(can be as many as needed) are objects. - It copies the properties of all objects
src1, ..., srcN
into dest. In other words, properties of all arguments starting from the 2nd are copied into the 1st. Then it returns dest.
let user = { name: "Sully" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);
// now user = { name: "Sully", canView: true, canEdit: true }
If the receiving object (user) already has the same named property, it will be overwritten:
let user = { name: "Sully" };
// overwrite name, add isAdmin
Object.assign(user, { name: "Pete", isAdmin: true });
// now user = { name: "Pete", isAdmin: true }
To copy all properties of user into the empty object and returning it
let user = {
name: "Sully",
age: 30
};
let clone = Object.assign({}, user);
Be aware of objects as property values
let user = {
name: "Sully",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true, same object
// user and clone share sizes
user.sizes.width++; // change a property from one place
alert(clone.sizes.width); // 51, see the result from the other one
To fix that, we should use the cloning loop that examines each value of user[key]
and, if itās an object, then replicate its structure as well. That is called a deep cloning.
Thereās a standard algorithm for deep cloning that handles the case above and more complex cases, called the Structured cloning algorithm. In order not to reinvent the wheel, we can use a working implementation of it from the JavaScript library lodash, the method is called _.cloneDeep(obj).
Objects in JavaScript are very powerful. Here weāve just scratched the surface of a topic that is really huge.
The Modern Javascript Tutorial: Objects
Methods
(We will come back to thisā¦)
The value of a key:value pair can be a function:
let cat = {
name: 'Ernie',
age: 5,
// using function as a value
makeSound: function() { console.log('meow') }
};
cat.makeSound(); // meow
this
To access a property of an object from within a method of the same object, you need to use the this
keyword. this
accesses itself.
let cat = {
name: 'Ernie',
age: 5,
// using function as a value
makeSound: function() { console.log('meow') },
getName: function() { console.log('My name is', this.name) }
};
cat.getName();
Classes
If you want to work with several objects of the same type (meaning having the same key:value pairs), you can make as many copies of one object as you want (keep the copy by reference problematic in mind though).
Or you could create a template for an object and then derive object instances of that template.
The basic syntax is:
class MyClass {
// class methods
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
class Cat
{
constructor(name, age) {
this.name = name;
this.age = age;
}
makeSound() { console.log('meow') }
getName() { console.log('My name is', this.name) }
}
let ernie = new Cat('Ernie', 5);
console.log(ernie); // -> Cat { name: 'Ernie', age: 5 }
ernie.makeSound(); // -> meow
new Cat()
creates a new object of the class template.- The constructor() method is called automatically by
new
, so we can initialize the object there. - Hence, when
new Cat('Ernie', age)
is called:- A new object is created.
- The constructor runs with the given argument and assigns this.name to it.
- With the created object we can then call class methods, such as
ernie.makeSound()
.
Careful
- It is in the constructor that you create class variables by using
this.variablename=value
- There is no
function
for class methods. - There are no commas between class methods.
You can also already declare and initialize the class variables at the top of your class (these are then called public instance fields), which makes the class a bit more readable:
class Cat
{
name = 'catname';
age = 0;
constructor(name, age) {
this.name = name;
this.age = age;
}
makeSound() { console.log('meow') }
getName() { console.log('My name is', this.name) }
}
let ernie = new Cat('Ernie', 5);
console.log(ernie.name); // -> Ernie
Careful
- There is no
let
keyword for creating the class variables - To access these variable anywhere else inside of the class, you still must use āthis.ā
Arrays
For an ordered collection, where we have a 1st, a 2nd, a 3rd element and so on, arrays are used. An array can store elements of any type.
let emptyArr = [];
let fruits = ["Apple", "Orange", "Plum"];
// mix of values
let arr = [ 'Apple', { name: 'John' }, true, function() { console.log('hello'); } ];
// get the object at index 1 and then show its name
console.log( arr[1].name ); // John
// get the function at index 3 and run it
arr[3](); // hello
fruits.pop(); // removes the last element, hence "Plum"
// The call fruits.push(...) is equal to fruits[fruits.length] = ....
fruits.push('strawberry'); // appends the element to the end of the array
One of the oldest ways to cycle array items is the for loop over indexes:
let fruits = ["Apple", "Orange", "Pear"];
for (let i = 0; i < fruits.length; i++)
{
console.log( fruits[i] );
}
But for arrays there modern form of loop, for..of
:
let fruits = ["Apple", "Orange", "Plum"];
// iterates over array elements
for (let fruit of fruits)
{
console.log( fruit );
}
Question:
let fruits = ["Apples", "Pear", "Orange"];
// push a new value into the "copy"
let shoppingCart = fruits;
shoppingCart.push("Banana");
// what's in fruits?
console.log( fruits.length ); //?
Internals
An array is a special kind of object. The square brackets used to access a property arr[0]
actually come from the object syntax. Numbers are used as keys.
They extend objects providing special methods to work with ordered collections of data and also the length property. But at the core itās still an object.
Remember, there are only 7 basic types in JavaScript. Array is an object and thus behaves like an object.
For instance, it is copied by reference:
let fruits = ["Banana"]
let arr = fruits; // copy by reference (two variables reference the same array)
console.log( arr === fruits ); // true
arr.push("Pear"); // modify the array by reference
console.log( fruits ); // Banana, Pear - 2 items now
But what makes arrays really special is their internal representation. The engine tries to store its elements in the contiguous memory area, one after another, and there are other optimizations as well, to make arrays work really fast.
But they all break if we quit working with an array as with an āordered collectionā and start working with it as if it were a regular object.
For instance, technically we can do this:
let fruits = []; // make an array
fruits[99999] = 5; // assign a property with the index far greater than its length
fruits.age = 25; // create a property with an arbitrary name
Thatās possible, because arrays are objects at their base. We can add any properties to them.
But the engine will see that weāre working with the array as with a regular object. Array-specific optimizations are not suited for such cases and will be turned off, their benefits disappear.
The ways to misuse an array:
- Add a non-numeric property like
arr.test = 5
- Make holes, like: add
arr[0]
and thenarr[1000]
(and nothing between them). - Fill the array in the reverse order, like
arr[1000]
,arr[999]
and so on.
Please think of arrays as special structures to work with the ordered data. They provide special methods for that. Arrays are carefully tuned inside JavaScript engines to work with contiguous ordered data, please use them this way. And if you need arbitrary keys, chances are high that you actually require a regular object {}
.
The Modern Javascript Tutorial: Arrays
JSON
JSON stands for JavaScript Object Notation and is widely used as a data storage and communication format on the Web, even in languages other than JavaScript. JSON is a data format that has its own independent standard and libraries.
JSON looks similar to JavaScriptās way of writing arrays and objects, with a few restrictions. All property names have to be surrounded by double quotes, and only simple data expressions are allowe:no function calls, bindings, or anything that involves actual computation. Comments are not allowed in JSON.
A journal entry might look like this when represented as JSON data:
{
"squirrel": false,
"events": ["running", "climbing", "digging", "being cute"]
}
let json = `{
name: "John", // mistake: property name without quotes
"surname": 'Smith', // mistake: single quotes in value (must be double)
'isAdmin': false // mistake: single quotes in key (must be double)
"birthday": new Date(2000, 2, 3), // mistake: no "new" is allowed, only bare values
"friends": [0,1,2,3] // here all fine
}`;
JavaScript gives us the functions JSON.stringify
and JSON.parse
to convert data to and from this format. The first takes a JavaScript value and returns a JSON-encoded string. The second takes such a string and converts it to the value it encodes. If an object has toJSON
, then it is called by JSON.stringify.
let string = JSON.stringify({squirrel: false,
events: ["weekend"]});
console.log(string);
// console output: {"squirrel":false,"events":["weekend"]}
console.log(JSON.parse(string).events);
// console output: ["weekend"]
Error Handling
The try..catch
syntax allows us to ācatchā errors so the script can, instead of dying, do something more reasonable.
try
{
// code...
}
catch (err)
{
// error handling
}
It works like this:
- First, the code in
try {...}
is executed. - If there were no errors, then
catch(err)
is ignored: the execution reaches the end oftry
and goes on, skipping catch. - If an error occurs, then the try execution is stopped, and control flows to the beginning of
catch(err)
. Theerr
variable (we can use any name for it) will contain an error object with details about what happened.
Keep in mind that try..catch
works synchronously, meaning directly after each other. The following error catching doesnāt work:
try
{
setTimeout(() => noSuchName, 1000); // script will die here
}
catch (err)
{
alert( "No error here" );
}
The function parameter for setTimeout
is executed later, when the engine has already left the try..catch
construct and not in ācatching modeā anymore.
The Error Object
When an error occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to catch:
try{}
catch (err) // <-- the "error object", could use another name instead of err
{
// error handling
}
For all built-in errors, the error object has two main properties:
name
: Error name. For instance, for an undefined variable thatās āReferenceErrorā.message
: Textual message about error details.
There are other non-standard properties such as stack
available in most environments but we will not look into that.
try
{
lalala; // error, variable is not defined!
}
catch(err)
{
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
Example
let json = "{ bad json }";
try
{
let user = JSON.parse(json); // <-- when an error occurs...
alert( user.name ); // doesn't work
}
catch (e)
{
// ...the execution jumps here
alert( "Our apologies, the data has errors." );
alert( e.name );
alert( e.message );
}
Functions
Functions in JavaScript can be quite special when for example created by a function expression or the function being anonymous. For now we only care about the most basic function declarations.
You declare functions in JavaScript as in the following examples (the alert()
method displays an alert dialog in your browser):
function showMessage()
{
alert( 'Hello everyone!' );
}
showMessage();
function showMessage(from, text) // arguments: from, text
{
alert(from + ': ' + text);
}
showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)
function showMessage(from, text = "no text given")
{
alert( from + ": " + text );
}
showMessage("Ann"); // Ann: no text given
function sum(a, b)
{
return a + b;
}
let result = sum(1, 2);
alert( result ); // 3
Optional Arguments
The following code is allowed and executes without any problem:
function square(x)
{
return x * x;
}
console.log(square(4, true, "kittens"));
// console output: 16
We defined square
with only one parameter. Yet when we call it with three, the language doesnāt complain. It ignores the extra arguments and computes the square of the first one.
JavaScript is extremely open-minded about the number of arguments you pass to a function. If you pass too many, the extra ones are ignored. If you pass too few, the missing parameters get assigned the value undefined
.
The downside of this is that it is possibl:likely, eve:that at some point youāll accidentally pass the wrong number of arguments to functions. And no one cares.
The upside is that this behavior can be used to allow a function to be called with different numbers of arguments. For example, this minus function tries to imitate the -
operator by acting on either one or two arguments:
function minus(a, b)
{
if (b === undefined) return -a;
else return a - b;
}
console.log(minus(10));
// console output: -10
console.log(minus(10, 5));
// console output: 5
If you write an = operator after a parameter, followed by an expression, the value of that expression will replace the argument when it is not given.
For example, this version of power makes its second argument optional. If you donāt provide it or pass the value undefined, it will default to two, and the function will behave like square.
function power(base, exponent = 2)
{
let result = 1;
for (let count = 0; count < exponent; count++)
{
result *= base;
}
return result;
}
console.log(power(4));
// console output: 16
console.log(power(2, 6));
// console output: 64
Rest Parameters
The following is a fairly new feature, introduced with ECMAScript 2018.
It can be useful for a function to accept any number of arguments. For example, Math.max
computes the maximum of all the arguments it is given.
To write such a function, you put three dots before the functionās last parameter, as the following.
function max(...numbers)
{
let result = -Infinity;
for (let number of numbers)
{
if (number > result) result = number;
}
return result;
}
console.log(max(4, 1, 9, -2));
// console output: 9
When such a function is called, the rest parameter is bound to an array containing all further arguments. If there are other parameters before it, their values arenāt part of that array. When, as in max, it is the only parameter, it will hold all arguments.
You can use a similar three-dot notation to call a function with an array of arguments.
let numbers = [5, 1, 7];
console.log(max(...numbers));
// console output: 7
This āspreadsā out the array into the function call, passing its elements as separate arguments. It is possible to include an array like that along with other arguments, as in max(9, ...numbers, 2)
.
Square bracket array notation similarly allows the triple-dot operator to spread another array into the new array.
let words = ["never", "fully"];
console.log(["will", ...words, "understand"]);
// console output: ["will", "never", "fully", "understand"]
Resources
Pass by Reference vs. by Value
Javascript is pass-by-value for primitive datatype. This means that when you pass arguments to a function, JavaScript makes a copy of their values and works inside of the function with those copies. In case of arrays and objects, Javascripts mimics the behavior of pass-by-reference, which is under the hood again pass-by-value.
Higher Order Functions
In mathematics and computer science, a higher-order function is a function that does at least one of the following:
- takes one or more functions as arguments,
- returns a function as its result.
This means that functions operate on other functions, either by taking them as arguments or by returning them. As functions are regular values in JavaScript, they can be handled almost the same way.
Higher-order functions allow us to abstract over actions, not just values. They come in several forms. For example, we can have a function that creates a new function.
We actually have already used a higher order function by adding a function as callback to an event listener:
function setup() {
button = createButton('submit');
button.mousePressed(greet);
}
function greet() {
// ...
}
[Wikipedia: Higher-order Function]
Three classical exemplary higher-order functions are map
, filter
, and reduce
for working with arrays.
Each programming language supporting programming in the functional style supports at least the three functions map, filter, and reduce. The names of the three functions have variations in the different programming languages.
map
applies a function to each element of its listfilter
removes all elements of a list not satisfying a conditionreduce
successively applies a binary operation to pairs of the list and reduces the list to a value.
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(getLength);
function getLength(item)
{
return item.length;
}
console.log(lengths); // 5,7,6
Reduce syntax:
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
let sum = [15.5, 2.3, 1.1, 4.7].reduce(getSum, 0);
function getSum(total, num)
{
return total + Math.round(num);
}
console.log(sum); // 24
There are two ways to make this code much more compact: anonymous functions and the most modern way: arrow functions.
Anonymous Functions
Anonymous functions are called anonymous because they arenāt given a name in the same way as normal functions. As the name is missing as connector to the function we can not refer to it from another place in the code. Hence, anonymous functions are directly placed, where they are needed (or stored in a variable, we come back to this).
An anonymous function is a function without a name.
This example:
function setup()
{
let canvas = createCanvas(512, 512);
canvas.doubleClicked(changeColor);
background(240);
}
function changeColor()
{
background(random(255), random(255), random(255));
}
can be rewritten to using an anonymous function as follows:
function setup()
{
let canvas = createCanvas(512, 512);
// The callback as anonymous function
canvas.doubleClicked(function ()
{
background(random(255), random(255), random(255));
});
background(240);
}
The above makes use of this principle. The value of the first argument of the .doubleClicked()
event is a function.
Using an anonymous function with map
:
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(function (item)
{
return item.length;
});
console.log(lengths); // 5,7,6
Anonymous function can also be stored in and invoked (called) by using a variable name.
const greatMath = function (a, b) {return a * b};
let result = greatMath(4, 3);
console.log(result);
This is also called a function expression. Function expressions stored in variables do not need function names. They are always invoked (called) using the variable name.
Function expressions and functions as arguments are possible because functions in Javascript are just a special type of object. This means they can be used in the same way as any other object. They can be stored in variables, passed to other functions as parameters or returned from a function using the return statement. Functions are always objects, no matter how they are created.
The subtle difference between a functions declaration (the ānormalā way of declaring functions) and a function expression is when a function is created by the JavaScript engine:
- A function expression is created when the execution reaches it and is usable only from that moment on.
- A function declaration can be called earlier than it is defined as the function was stored in a pre-processing step.
For example, a global function declaration is visible in the whole script, no matter where it is. Thatās due to internal algorithms. When JavaScript prepares to run the script, it first looks for global function declarations in it and creates the functions. We can think of it as an āinitialization stageā.
And after all function declarations are processed, the code is executed. So it has access to these functions.
For example, this works:
//Function Declaration
sayHi("Hans"); // Hello Hans
function sayHi(name)
{
console.log("Hello " + name);
}
The Function Declaration sayHi
is created when JavaScript is preparing to start the script and is visible everywhere in it.
If it were a Function Expression, then it wouldnāt work:
//Function Expression
sayHi("Hans"); // error!
const sayHi = function(name)
{
console.log("Hello " + name);
};
[javaScript.info] [javascript.info]
Arrow Functions
Arrow functions allow an even shorter syntax for writing function expressions (starting with ECMAScript 2015) by eliminating in certain cases the function keyword, the return keyword, and even the curly bracketsā¦ whhhhaaat? š±
// ES5
const myFunction = function (param1, param2)
{
// do something
}
becomes
// ECMAScript 2015
const myFunction = (param1, param2) =>
{
// do something
}
An arrow comes after the list of parameters and is followed by the functionās body. It expresses something like
this input (the parameters) produces this result (the body) in short as
this input => this result.
If there are no parameters, the ()
just stay empty:
// ES5
const myFunction = function ()
{
// do something
}
// ECMAScript 2015
const myFunction = () =>
{
// do something
}
With this, we can make the anonymous function for changing the canvas color in a p5 sketch even more compact:
// ES5
canvas.doubleClicked(function ()
{
background(random(255), random(255), random(255));
});
// ECMAScript 2015
canvas.doubleClicked(() => background(random(255), random(255), random(255)));
If there is only one parameter, we can omit the ()
.
// ES5
const myFunction = function (param1)
{
// do something
}
// ECMAScript 2015
const myFunction = param1 =>
{
// do something
}
// ES5
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(function (item)
{
return item.length;
});
console.log(lengths); // 5,7,6
// ECMAScript 2015
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
One more function expression example:
// ES5
const result = function(x, y)
{
x += 100;
return x * y;
}
// ECMAScript 2015
const result = (x, y) =>
{
x += 100;
return x * y;
}
Also, when there is only one line of code, you can omit the return and the {}.
// ES5
const result = function(x, y)
{
return x * y;
}
// ECMAScript 2015
const result = (x, y) => x * y;
Once again, when there is only one parameter, you can also omit the parentheses around the parameter list.
// ES5
const result = function(x)
{
return x * x;
}
// ECMAScript 2015
const result = x => x * x;
For now you can remember that functions, anonymous functions, function expression and arrow functions do the same thing (they do have slight differences but nothing we need to be bothered about at this point). Arrow functions were added in 2015, mostly to make it possible to write function expressions more compactly.
[javaScript.info] [Eloquent JavaScript]
Closures
On a Side Note: this section about closures is advanced and optional!
The ability to treat functions as values brings up an interesting question. What happens to the (local) bindings or scope when the function call that created them is no longer active?
JavaScript variables can belong to the local or global scope. Global variables can be made local (private) with closures.
To understand this letās take a bit of a detour.
Global Variables
A function can access all local variables defined inside the function:
function myFunction()
{
let a = 4;
return a * a;
}
But a function can also access variables defined outside the function:
let a = 4;
function myFunction()
{
return a * a;
}
In the last example, a is a global variable. In a web page, global variables belong to the window object.
A local variable can only be used inside the function where it is defined. It is hidden from other functions and other scripting code.
Global and local variables with the same name are different variables. Modifying one, does not modify the other.
Variables created without the keyword let
, are always global, even if they are created inside a function.
Variable Lifetime
Global variables live as long as your application (your window / your web page) lives.
Local variables have short lives. They are created when the function is invoked, and deleted when the function is finished.
A Counter Dilemma
Suppose you want to use a variable for counting something, and you want this counter to be available to all functions.
You could use a global variable, and a function to increase the counter:
// Initiate counter
let counter = 0;
// Function to increment counter
function add()
{
counter += 1;
}
// Call add() 3 times
add();
add();
add();
// The counter is now 3
There is a problem with the solution above: Any code on the page can change the counter, without calling add().
The counter should be local to the add() function, to prevent other code from changing it:
// Initiate counter
let counter = 0;
// Function to increment counter
function add()
{
let counter = 0;
counter += 1;
}
// Call add() 3 times
add();
add();
add();
// However, now the counter is 0
It did not work because we display the global counter instead of the local counter.
We can remove the global counter and access the local counter by letting the function return it:
// Function to increment counter
function add()
{
let counter = 0;
counter += 1;
return counter;
}
// Call add() 3 times
add();
add();
add();
// Now the counter is 1.
It did not work because we reset the local counter every time we call the function.
A JavaScript inner function can solve this.
Nested Functions
All functions have access to the global scope. In fact, in JavaScript, all functions have access to the scope above them.
JavaScript supports nested functions. Nested functions have access to the scope above them.
In this example, the inner function plus()
has access to the counter variable in the parent function:
function add()
{
let counter = 0;
function plus() {counter += 1;}
plus();
return counter;
}
This could have solved the counter dilemma, if we could reach the plus() function from the outside.
We also need to find a way to execute counter = 0
only once.
We need a closure.
Setup
let add = function ()
{
let counter = 0;
return function ()
{
counter += 1;
return counter;
}
};
let result = add();
console.log(result);
console.log(result());
console.log(result());
console.log(result());
// the counter is now 3
The function add
returns not a value but a function expression.
This way result
becomes a function. The miraculous part is that the function saved in result
can access the counter
in the parent scope.
The counter
is protected by the scope of the anonymous function.
This is called a JavaScript closure. A closure is a function having access to the parent scope, even after the parent function has closed.
The key to remember is that when a function gets declared, it contains a function definition and a closure.
The closure is a collection of all the variables in scope at the time of creation of the function.
A good mental model is to think of function values as containing both the code in their body and the environment in which they are created. When called, the function body sees the environment in which it was created, not the environment in which it is called. Or imagine this as a backpack. A function definition comes with a little backpack. And in its pack it stores all the variables that were in scope at the time that the function definition was created.
The following code shows another example of this. It defines a function, wrapValue
, that creates a local binding. It then returns a function that accesses and returns this local binding.
function wrapValue(n)
{
let local = n;
return () => local;
// return function()
// {
// return local;
// }
}
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// console output: 1
console.log(wrap2());
// console output: 2
This works as youād hopeāboth instances of the binding can still be accessed. This situation is a good demonstration of the fact that local bindings are created anew for every call, and different calls canāt trample on one anotherās local bindings.
With a slight change, we can turn the previous example into a way to create functions that multiply by an arbitrary amount.
function multiplier(factor)
{
return number => number * factor;
// return function(number)
// {
// return number * factor;
// }
}
let twice = multiplier(2);
console.log(twice(5));
// console output: 10
let triple = multiplier(3);
console.log(triple(5));
// console output: 15
In the example, multiplier is called and creates an environment in which its factor parameter is bound to 2. The function value it returns, which is stored in twice, remembers this environment. So when that is called, it multiplies its argument by 2.
Thinking about programs like this takes some practice.
[Eloquent JavaScript: Functions] [w3schools.com: JavaScript Closures]
Asynchronism
In a synchronous programming model, things happen one at a time. When you call a function that performs a long-running action, it returns only when the action has finished and it can return the result. This stops your program for the time the action takes.
An asynchronous model allows multiple things to happen at the same time. When you start an action, your program continues to run. When the action finishes, the program is informed and gets access to the result (for example, the data read from disk).
Callbacks
One approach to asynchronous programming is to make functions that perform a slow action take an extra argument, called a callback function. The action is started, and when it finishes, the callback function is called with the result.
Or, you can use callbacks to create dependencies between functions (if this, then thatā¦):
function setup()
{
let canvas = createCanvas(512, 512);
canvas.doubleClicked(changeColor);
background(240);
}
function changeColor()
{
background(random(255), random(255), random(255));
}
Or better (see the arrow function syntax):
function setup()
{
let canvas = createCanvas(512, 512);
canvas.doubleClicked(() => background(random(255), random(255), random(255)));
background(240);
}
Error-First Callbacks
Functions have not only the option to hand data back and forth but also errors. This means that callback functions get both, an error and data as input arguments.
Error first callbacks is a convention common to many JavaScript libraries. For example, most asynchronous methods exposed by the server environment Node.js core API follow the error-first callback pattern. However, the language JavaScript itself does not enforce this pattern.
In the error-first callback pattern is:
- the first argument of the callback reserved for an error object. If an error occurred, it will be returned by the first
err
argument. - the second argument of the callback reserved for any successful response data.
If no error occurred, err will be set to null and any successful data will be returned in the second argument.
const fs = require('fs');
function errorFirstCallback(err, data)
{
if (err)
{
console.error('There was an error', err);
return;
}
console.log(data);
}
fs.readFile('./does_not_exist', 'utf8', errorFirstCallback);
fs.readFile('./does_exist', 'utf8', errorFirstCallback);
Keep in mind that the above is not a client side function, recognized by the browser, but it is server side NodeJS code (we will come back to this).
The callback functionality is based on the principle of higher order functions.
Nesting Callbacks
Letās say we have a function for loading a script with a callback function, which is executed with the script is loaded:
loadScript('script1.js', () => {console.log("ho ho ho, happy holidays")});
Now, we want to load to scripts sequentially, meaning one after the other. The natural solution would be to put the second loadScript call inside the callback, like this:
loadScript('script1.js', () => {
console.log("ho ho ho, happy holidays")
// Loading a second script
loadScript('script2.js', () => {console.log("...and a happy new year!")});
});
After the outer loadScript
is complete, the callback initiates the inner one.
What if we want one more scriptā¦?
loadScript('script1.js', () => {
console.log("ho ho ho, happy holidays")
// Loading second script
loadScript('script2.js', () => {
console.log("...and a happy new year!")
// Loading a third script
loadScript('script3.js', () => {console.log("Now it is back to work.")});
});
});
Can this be good?
Pyramid of Doom
At a first glance, nesting callbacks is a viable way of asynchronous coding. And indeed it is. For one or maybe two nested calls it is just fine.
But letās image the above example as error-first callbacks:
loadScript('script1.js', (err, script) => {
if (err)
{
handleError(err);
}
else
{
console.log("ho ho ho, happy holidays")
// Loading second script
loadScript('script2.js', (err, script) => {
if (err)
{
handleError(err);
}
else
{
console.log("...and a happy new year!")
// Loading a third script
loadScript('script3.js', (err, script) => {
if (err)
{
handleError(err);
}
else
{
console.log("Now it is back to work.");
}
}
}
});
Ahhhhā¦ š³
As calls become more nested, the code becomes deeper and increasingly more difficult to manage. This ultimately might lead to a so-called pyramid of doom. The pyramid of nested calls grows to the right with every asynchronous action. Soon it spirals out of control.
This way of coding isnāt very good āš»
For this example it would actually be worthwhile to go back to making every action a standalone function once again, which is more readable and easier to handle:
loadScript('script1.js', step1);
function step1(error, script)
{
if (error)
{
handleError(error);
} else
{
// ...
loadScript('script2.js', step2);
}
}
function step2(error, script)
{
if (error)
{
handleError(error);
} else
{
// ...
loadScript('script3.js', step3);
}
}
function step3(error, script)
{
if (error)
{
handleError(error);
} else
{
// ...continue after all scripts are loaded (*)
}
};
Luckily, there are other ways to avoid such pyramids. One of the best ways is to use promises.
Promises
On a Side Note: The following chapter is a tiny bit advanced in its level of detail. Make sure that you understand the general concept of promises and how to use then
& catch
and async
& await
(as they might becoming from a given library or framework). As a beginner you do not need to know how to write Promise
objects yourself.
A promise is an asynchronous action that may complete at some point and produce a value. A promise object is able to notify anyone who is interested when its value is available.
Promises provide a clean and elegant syntax and methodology to handle async calls.
Understanding Promises
I promise to bring you cake by next week.
You donāt know if you will actually get that cake by next week or not. I can either really bring cake, or stand you up and withhold the cake if I donāt feel like it.
That is a promise. A promise has 3 states. They are:
- Pending: You know about the promise but donāt know if you will get that cake.
- Fulfilled: I brought you cake.
- Rejected: I didnāt feel like baking and I didnāt bring any cake afterall.
Creating a Promise
The general constructor syntax for a promise object is:
let promise = new Promise((resolve, reject) =>
{
// executor (the producing code, "promise of cake")
});
The function passed to new Promise
is called the executor. When the promise is created, this executor function runs automatically. It contains the producing code, that should eventually produce a result. In terms of the analogy above, the executor is me bringing you cake.
The resulting promise object has two internal properties:
state
: initially pending, then changes to either fulfilled or rejected,result
: an arbitrary value of your choosing, initiallyundefined
.
When the executor finishes the job, it should call one of the functions that it gets as arguments:
resolve(value)
: to indicate that the job finished successfully- sets
state
tofulfilled
- sets
result
tovalue
- sets
reject(error)
: to indicate that an error occurred- sets
state
torejected
- sets
result
toerror
- sets
In the following, an example of a Promise constructor and a simple executor function with its producing code.
On a Side Note: The setTimeout
function, available both in Node.js and in browsers, waits a given number of milliseconds (a second is a thousand milliseconds) and then calls a function.
let lenaFeelsLikeBacking = true;
// Promise
let promiseOfCake = new Promise( (resolve, reject) =>
{
if (lenaFeelsLikeBacking)
{
let cake =
{
type: 'chocolate',
taste: 'superdelicious'
};
// this emulates that the baking takes some time...
setTimeout(() => resolve(cake), 1000);
}
else
{
let reason = new Error('Lena didn\'t feel like baking');
reject(reason);
}
}
);
There can be only a single result or an error.
The executor must call only one resolve or reject. The promiseās state change is final.
All further calls of resolve and reject are ignored:
let promise = new Promise((resolve, reject) =>
{
resolve("done");
reject(new Error("ā¦")); // ignored
setTimeout(() => resolve("ā¦")); // ignored
});
Also, resolve
/reject
expect only one argument and will ignore additional arguments.
Consumers: then
and catch
A Promise object serves as a link between the executor (the āproducing codeā or āLena might bakeā) and the consuming functions (you, waiting for cake), which will receive the result or error.
Consumer functions can be registered (subscribed) using the methods .then
and .catch
.
The syntax of .then
is:
promise.then(
(result) => { /* handle a successful result */ },
(error) => { /* handle an error */ }
);
The first argument of .then
is a function that:
- runs when the Promise is resolved, and
- receives the result.
The second argument of .then
is a function that:
- runs when the Promise is rejected, and
- receives the error.
let lenaFeelsLikeBacking = true;
// Promise
let promiseOfCake = new Promise( (resolve, reject) =>
{
if (lenaFeelsLikeBacking)
{
let cake =
{
type: 'chocolate',
taste: 'superdelicious'
};
setTimeout(() => resolve(cake), 1000);
}
else
{
let reason = new Error('Lena didn\'t feel like baking');
reject(reason);
}
}
);
// Call the promise
promiseOfCake.then
(
result => console.log(result),
error => console.error(error),
);
If weāre interested only in successful completions, then we can skip the error function argument to .then
:
promiseOfCake.then(result => console.log(result));
If weāre interested only in errors, then we can use null as the first argument:
.then(null, errorHandlingFunction)
promiseOfCake.then(null, error => console.error(error));
Or we can use catch
, which is exactly the same:
.catch(errorHandlingFunction)
promiseOfCake.catch(error => console.error(error));
The call .catch(f)
is a completely the same to .then(null, f)
- itās just a shorthand.
What you will see most is the following syntax:
promiseOfCake
.then(result => console.log(result))
.catch(error => console.error(error));
Chaining Promises
Sequencing
Letās return to the problem mentioned in the section about callbacks. We have a sequence of tasks to be done one after another.
new Promise((resolve, reject) =>
{
setTimeout(() => resolve(1), 1000);
}).then((result) =>
{ // (**)
console.log(result); // 1
return result * 2;
}).then((result) =>
{ // (***)
console.log(result); // 2
return result * 2;
}).then((result) =>
{
console.log(result); // 4
return result * 2;
});
The idea is that the result is passed through the chain of .then
handlers.
Here the flow is:
- The initial promise resolves and returns the value 1 (*),
- Then the
.then
handler is called (**). - The value that it returns is passed to the next
.then
handler (***) - ā¦and so on.
As the result is passed along the chain of handlers, we can see a sequence of logs: 1 ā 2 ā 4.
On the return of a call to promise.then
it returns an arbitrary thenable object, which is treated the same way as a promise. On that thenable object we can call .then
again.
When a promise becomes resolved and returns a value, the next .then
is called with that value.
Returning Promises
Letās say you are promising your friends some of the cake I promised to bring to class. For this we need to create an additional promise.
Multiple promises can be chained if the returned value of a .then
handler is once again a promise.
If the returned value is a promise, then the further execution is suspended until that returned promise settles. After that, the result of that promise is given to the next .then
handler.
For instance:
new Promise((resolve, reject) =>
{
setTimeout(() => resolve(1), 1000);
}).then(result =>
{
console.log(result); // 1
return new Promise((resolve, reject) =>
{ // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(result =>
{ // (**)
console.log(result); // 2
return new Promise((resolve, reject) =>
{
setTimeout(() => resolve(result * 2), 1000);
});
}).then(result =>
{
console.log(result); // 4
});
Here
- the first
.then
logs 1 and returnsnew Promise(ā¦)
in the line (*). - After one second it resolves, and the result (the argument of
resolve
, here itāsresult*2
) is passed on to the handler of the second.then
in the line (**). It logs 2 and does the same thing.
So the output is again 1 ā 2 ā 4, but now with 1 second delay between alert calls.
Returning promises allows us to build chains of asynchronous actions.
Example Promising Cake
You can only fullfil your promise of cake to your friends after the promiseOfCake
is fulfilled by me.
let youGiveOutCake = true;
// let youGiveOutCake = false;
let promiseOfPassingOnCake = cake =>
{
let promise = new Promise((resolve, reject) =>
{
if (youGiveOutCake)
{
let message = 'Hey friend, I have ' + cake.taste + ' '
+ cake.type + ' cake for you.';
setTimeout(() => resolve(message), 1000);
}
else
{
let reason = new Error('You are selfish.');
reject(reason);
}
}
)
return promise;
}
// Chain multiple promises
promiseOfCake
.then(promiseOfPassingOnCake)
.then(fulfilled => console.log(fulfilled))
.catch(error => console.log(error.message));
Promise chaining is great at error handling. When a promise rejects, the control jumps to the closest rejection handler down the chain. Thatās very convenient in practice.
There is even an implicit error handling. The code of the executor and promise handlers has an invisible try..catch around it. If an error happens, it gets caught and treated as a rejection.
We may have as many .then
as we want, and then use a single .catch
at the end to handle errors in all of them.
For instance, this code:
new Promise((resolve, reject) =>
{
throw new Error("Whoops!");
}).catch(error); // Error: Whoops!
ā¦Works the same way as this:
new Promise((resolve, reject) =>
{
reject(new Error("Whoops!"));
}).catch(error); // Error: Whoops!
The invisible try..catch around the executor automatically catches the error and treats it as a rejection.
Similarly, if we throw inside .then
handler an error, meaning we reject a promise, the control jumps to the nearest error handler.
new Promise((resolve, reject) =>
{
resolve("ok");
}).then((result) =>
{
throw new Error("Whoops!"); // rejects the promise
}).catch(error); // Error: Whoops!
Thatās so not only for throw, but for any errors, including programming errors as well:
new Promise((resolve, reject) =>
{
resolve("ok");
}).then(result =>
{
blabla(); // no such function
}).catch(error); // ReferenceError: blabla is not defined
The final .catch not only catches explicit rejections, but also occasional errors in the handlers above.
Example loadScript
Letās come back to the example of loading three scripts after each other.
function loadScript(src)
{
return new Promise((resolve, reject) =>
{
let script = ...
if(script) resolve(script)
else reject(new Error("Script load error.");
});
}
loadScript("script_one.js")
.then(script => loadScript("script_two.js"))
.then(script => loadScript("script_three.js"))
.then(script =>
{
// use functions declared in the scripts
one();
two();
three();
})
.catch((error) => console.log(error.message));
Here each loadScript
call returns a promise, and the next .then
runs when it resolves. Then it initiates the loading of the next script. So scripts are loaded one after another.
We can add more asynchronous actions to the chain. Please note that this code is flat, it grows down, not to the right. There are no signs of a pyramid of doom, which is what we want.
Please also note that technically it is possible to write .then
directly after each promise, without returning them, like this:
loadScript("script_one.js").then(script1 =>
{
loadScript("script_two.js").then(script2 =>
{
loadScript("script_three.js").then(script3 =>
{
// this function has access to variables script1, script2 and script3
one();
two();
three();
});
});
});
This code does the same: it loads 3 scripts in sequence. But it grows to the right. So we have the same problem as with callbacks. Use chaining (return promises from .then) to evade it.
There are cases where itās ok to write .then
directly, but thatās an exception rather than a rule.
Summary then & catch
Once again, if this was confusing, focus for now on knowing what then
and catch
are and how to use them. You will need this for working with given functions returning promises, coming from a library or a framework. However, you will not need to write promises yourself any time soon.
Async/Await
Now that I tortured you with the core functionality of promises, I am also telling you that there is actually an easier syntax for working with promisesā¦ because that is just how I am š. However, this is fairly new syntax and not that widespread in the world of web developments yet.
Async
When placing the keyword async
in front of a function, the function automatically returns a promise. Specifically, whatever the function returns, becomes wrapped in a resolved promise.
On a Side Note: alert
shows a message and waits for the user to press the ok-button. Used as in the example below, it automatically shows the return value passed from myFunc
.
async function myFunc()
{
return 1;
}
myFunc().then(alert); // 1
The above is the same as:
function myFunc()
{
return Promise.resolve(1);
}
myFunc().then(alert); // 1
Await
With the keyword await
you can postpone the execution of code in an async function until a promise returns its result.
async function myFunc()
{
let promise = new Promise((resolve, reject) =>
{
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // wait until the promise resolves (*)
alert(result); // "done!"
}
myFunc();
The function execution āpausesā at the line (*) and resumes when the promise settles, with result
becoming its result. So the code above shows ādone!ā in one second.
await
suspends the function execution until the promise settles, and then resumes it with the promise result.
Keep in mind that await
only works inside of a async
function!
If a promise resolves normally, then await promise
returns the result. But in the case of a rejection, it throws the error, just as if there were a throw statement at that line.
async function myFunc()
{
throw Error("Whoops!");
}
The above is the same as:
function myFunc()
{
await Promise.reject(Error("Whoops!"));
}
In real-world situations, the promise may take some time before it rejects. In that case there will be a delay before await throws an error.
We can catch that error using try..catch
, the same way as a regular throw
inside of the function:
async function myFunc()
{
try {
let response = await (Error("Whoops!"));
} catch(err) {
alert(err); //Whoops!
}
}
In the case of an error, the control jumps to the catch block. We can also wrap multiple lines:
async function myFunc()
{
try {
let response1 = await (Error("Whoops!"));
let response2 = await (Error("Whoops Again!"));
} catch(err) {
alert(err); // catches both errors
}
}
Or, if we donāt do the error catching inside of the function, we can catch the return of the function:
async function myFunc()
{
return await (Error("Whoops!"));
}
myFunc().catch(alert); //Whoops!
Example
We can re-write the following example code, which is chaining Promises to using async/await instead of .then/catch:
On a Side Note: fetch()
allows you to make (HTTP) requests to servers from web browsers and returns a promise. E.g. let response = fetch(url);
.
function loadJson(url)
{
return fetch(url)
.then(response =>
{
if (response.status == 200)
{
return response.json();
}
else
{
throw Error(response.status);
}
});
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404
As async function the above becomes:
async function loadJson(url) // (1.)
{
let response = await fetch(url); // (2.)
if (response.status == 200) {
return response.json(); // (3)
}
throw Error(response.status);
}
loadJson('no-such-user.json')
.catch(alert); // Error: 404 (4.)
- The function
loadJson
becomesasync
. - All
.then
inside are replaced withawait
. - The
if
statement waits for the promise fromfetch
to resolve before it is executed. - The error thrown from
loadJson
is handled by.catch
. We canāt useawait loadJson(ā¦)
there, because weāre not in anasync
function.
Summary
The async
keyword before a function has two effects:
- Makes it always return a promise.
- Allows
await
to be used in it.
The await
keyword before a promise makes the JavaScript engine wait until that promise settles, and then:
- If itās an error, the exception is generated ā same as if throw error were called at that very place.
- Otherwise, it returns the result.
Together they provide a great framework to write asynchronous code that is easy to both read and write.
With async/await
we rarely need to write promise.then/catch
, but we still shouldnāt forget that they are based on promises, because sometimes, when not inside of an async
function, we have to use these methods.
Modules
JavaScript modules allow you to break up your code into separate files. This makes it easier to maintain the code-base. JavaScript modules rely on the import
and export
statements.
Keep in mind, that modules only work with the HTTP(s) protocol. A web-page opened via the file:// protocol cannot use import / export.
Export
You can export a function or variable from any file. Exported values can then be imported into other programs with the import declaration.
There are two types of exports, named exports and default exports. You can have multiple named exports per module but only one default export.
Named Exports
You can create named exports for specific lines individually, or all at once at the bottom of a file.
For an in-line individual export, after the export
keyword, you can use let
, const
, and var
declarations, as well as function
or class
declarations:
// person.js
export const name = "Jesse";
export const age = 40;
For exporting all at once at the bottom, you can also use the export { name1, name2 }
syntax to export a list of names declared elsewhere:
// person.js
const name = "Jesse";
const age = 40;
export {name, age};
Default Exports
You can only have one default export in a file.
// message.js
const message = () => {
const name = "Jesse";
const age = 40;
return name + ' is ' + age + 'years old.';
};
export default message;
Named exports are useful when you need to export several values. When importing this module, named exports must be referred to by the exact same name (optionally renaming it with as), but the default export can be imported with any name. For example:
// file test.js
const k = 12;
export default k;
// some other file
import m from './test'; // note that we have the freedom to use import m instead of import k, because k was default export
console.log(m); // will log 12
You can also rename named exports to avoid naming conflicts:
export {
myFunction as function1,
myVariable as variable,
};
Import
You can read-only import modules into a file in two ways, based on if they are named exports or default exports. Named exports are constructed using curly braces. Default exports are not.
In total there are four forms of import declarations:
- Named import: import { export1, export2 } from āmodule-nameā;
- Default import: import defaultExport from āmodule-nameā;
- Namespace import: import * as name from āmodule-nameā;
- Side effect import: import āmodule-nameā;
import
declarations can only be present in modules, and only at the top-level (i.e. not inside blocks, functions, etc.).
Import From Named Exports
Import named exports from the file person.js
:
import { name, age } from "./person.js";
Import From Default Exports
Import a default export from the file message.js
:
import message from "./message.js";
Style Guide
I highly recommend to early on pick a style you want to follow when developing code. This is a personal choice but there are several standards developed by JavaScript communities. It is alway a good idea to follow a standard.
- JavaScript Standard Style
- Google JavaScript Style Guide
- Principles of Writing Consistent, Idiomatic JavaScript
- Airbnb JavaScript Style Guide()
As an example on what people can become hang up upon in the following the spaces vs. tabs discussion.
Spaces and Tabs
The tabs vs. space discussion is as long as time itself.
Here the evaluation of 400,000 GitHub repositories, 1 billion files, 14 terabytes of code:
[400,000 GitHub repositories, 1 billion files, 14 terabytes of code: Spaces or Tabs?]
Arguments for tabs
- If you use tabs instead of spaces, you can configure in most IDEs how much indentation each space; you can set the value to four spaces, while someone who insists on two spaces can also be satisfied.
Arguments for spaces
- With only two spaces more code can be fit on to one line and still fit within the screen, without horizontal scrolling.
- āCallback hellā is real in JavaScript and the average program have many more indents than the average C or PHP program.
- Keeps the file size down by 4%, since unlike many other languages, JavaScript typically isnāt compiled and often not compressed before being sent to the user.
- Other languages have a standard with four spaces, and for this reason, many developers still use four spaces in JavaScript as a habit.
[Stackoverflow: Coding Style in JavaScript - Why prefer 2 spaces not 4 spaces?]
Resources
The End
šŖš¼ š š