Introduction
Welcome to my personal JavaScript toolbox!
This was created as a school project for the modul WebPr. Here, everything that happened is documented and (hopefully) sufficiently explain so that it can be understand and replicated. Keep in mind that this is written from the perspective of somebody who is already familiar with programming, so many basic concepts won't be explained anymore.
Start here with chapter one!
Strings
A small excursion into strings and how to declare them in JavaScript.
You can declare them the same way as you would declare in Java.
"This is a string"
You can also declare them like you would a single character in Java.
'This is also a String'
More interesting is the declaration with backticks(`
). With this you can declare a multiline strings and even use string interpolation!
This allows us to build nice little templates, which is why we also call these strings "template strings".
`
This
is
a
multiline
string :)
`
let x = 1;
`
This is {x} template
`
A more obscure way to declare strings is to use forward slashes(/
), like so:
/test/
//put this into a constructor
String(/eins/)
Huh? Why does this work? What does this do?
The answer is simple, forward slashes denote a regular expression in JavaScript. Because a regular expression is usually just a string that gets parsed, putting this into a string constructor also yields a string.
Arrays
A small excurse into more JavaScript arrays functions.
push()
Adds one or more elements to the end of an array and returns the new length of the array.
let fruits = ["apple", "banana"];
fruits.push("orange"); // returns 3
console.log(fruits); // output: ["apple", "banana", "orange"]
pop()
Removes the last element from an array and returns that element.
let fruits = ["apple", "banana", "orange"];
let lastFruit = fruits.pop();
console.log(lastFruit); // output: "orange"
console.log(fruits); // output: ["apple", "banana"]
shift()
Removes the first element from an array and returns that element.
let fruits = ["apple", "banana", "orange"];
let firstFruit = fruits.shift();
console.log(firstFruit); // output: "apple"
console.log(fruits); // output: ["banana", "orange"]
unshift()
Adds one or more elements to the beginning of an array and returns the new length of the array.
let fruits = ["banana", "orange"];
fruits.unshift("apple");
console.log(fruits); // output: ["apple", "banana", "orange"]
slice()
Is used to slice (hence the name) a part out of the array.
let fruits = ["banana", "orange", "grape"];
const sliced = fruits.slice(1,3); // 1 is inclusive, 3 not
console.log(sliced); // output: [ "orange", "grape" ]
const sliced2 = fruits.slice(1); // until the end of the array
console.log(sliced2); // output: [ "orange", "grape" ]
const sliced3 = fruits.slice(0, -1); // until -1 the length of the array
console.log(sliced3); // output: [ "banana", "orange" ]
splice()
Changes the contents of an array by removing, replacing, or adding elements at specific positions.
// removing elements
let fruits = ["apple", "banana", "orange", "grape"];
fruits.splice(1, 2); // Removes 2 elements starting from index 1
console.log(fruits); // Output: ["apple", "grape"]
// adding elements
let fruits = ["apple", "banana", "grape"];
fruits.splice(2, 0, "orange", "kiwi"); // Adds "orange" and "kiwi" at index 2
console.log(fruits); // Output: ["apple", "banana", "orange", "kiwi", "grape"]
//replacing elements
let fruits = ["apple", "banana", "orange"];
fruits.splice(1, 1, "kiwi"); // Replaces 1 element at index 1 with "kiwi"
console.log(fruits); // Output: ["apple", "kiwi", "orange"]
Create arrays
With the from
method, you can create an array from every object that has a length
property
Array.from("abc") //output ["a", "b", "c" ]
Array.from({length: 2}) //output [undfined, undefined]
This can also be used to create very large arrays in an easy way.
Array.from({length: 1000}, (it, idx) => idx) //output [0, 1, ..., 999 ]
Functions in JavaScript
JavaScript function initially look a lot like Java functions, just without the type system.
function myCoolFunction(a, b){
return a + b;
};
myCoolFunction(1, 2) // evaluates to 3
But they can also be written in other ways, because they are essentially just references to a "function" object.
// Same as above
const myCoolFunction => (a,b) => { return a + b; };
// With no arguments, always returns 1
const noArgs => () => { return 1; };
Those ways of writing functions also enables that we can write functions in JS, that return something without using the return
statement.
// This always return 1
const return = () => 1;
// BUT THIS RETURN UNDEFINED! DONT DO THIS!
function noReturn() { 1; }
const noReturn2 = () => { 1; };
As mention previously, functions are essentially just an object. This means we can also define functions as variables or even put them into an array.
function fun1(){ return 1; };
const myfun = fun1;
const funs = [null, undefined, fun1];
// The function can then be called like this
funs[2]()
That also allows us to pass functions as arguments to other functions. Kinda like Lambda expressions in Java.
function doit(whatToDo) {
return function bla(arg) {
return whatToDo(arg);
}
}
doit(fun1)();
// Same thing but written differently
const doit2 = callme => arg => callme(arg) ;
doit2(fun1)()
Scope
Strictly speaking JavaScript only has two scopes:
global
the entire window (if you are in a Browser)function
variables are alwayes local to the enclosing functions
Despite just having two scopes, JavaScript gives us four different ways to declare variables.
Number 1:
x = 12;
This creates a mutable global variable. This is not good for obvious reasons. DONT DO THIS!
Number 2:
var x = 12;
This creates a mutable hoisted variable. What does "hoisted mean? It means that the variable gets initialized at the beginning of the scope, even if we declare it further down. Which causes things like this:
x = 0;
function foo(){
document.writeln(x); //this prints undefined, not 0
var x = 12;
}
This is also not good for also obvious reasons. DONT DO THIS!
Number 3:
let x = 12;
This creates a mutable variable inside of the local scope. This is good, use this.
Number 4:
const x = 12;
This creates an immutable variable inside of the local scope. Immutable meaning not reasignable, not really immutable. This is good, use this.
Objects
The idea of objects is to be data structures with their associated methods and state. The same as in (almost) every other programming language.
Ways to create Objects
There are many different ways to create objects in JavaScript. Each with their advantages and disadvantages.
Open, Dynamic
Very simpel way to create objects. This way is very dynamic and easy to manipulate. This is also its main disadvantage, along with the inability to enforce and share its structure.
Using this
also has major problems, which you can read more about here.
const good = {
firstname : "Good",
lastname : "Boy",
getName : function() {
return this.firstname + " " + this.lastname
};
};
Closed, Explicit
This solves our problems from before. It is basically a constructor, which means that the structure is always the same. It is also not possible to change the first or lastname property afterwards. This may be good or bad, depending on what you want to do with it.
function Person(first, last) {
return {
getName: () => first + " " + last;
}
}
Mixed, classified
Here, both of the examples from before are mixed together.
This is essentially a very elaborate constructor from our first version. Each function has a property prototype
, which defines the name of it. With the "newish" new
keyword, a new empty object is silently passed to this function. Which then means we can use this
more like we are accustomed to from Java.
This is the "default" (if there is such a thing in JavaScript) way to create new objects.
const Person = ( () => { // lexical scope
function Person(first, last) { // ctor, binding
this.firstname = first;
this.lastname = last;
}
Person.prototype.getName = function() {
return this.firstname + " " + this.lastname;
};
return Person;
}) (); // IIFE
// new Person("Good", "Boy") instanceof Person
Prototyp
The prototype is a sort of "type" of object. It is itself also an objects. Using prototypes, it is possible to use things like instanceof
.
Classes
JavaScript has two keyword, which are relevant for this chapter: class
and extends
.
class
Keyword
The class
keyword is syntactic sugar for the mixed way to create objects. There are a couple of minor difference, which will no be covered here, but it looks very similar.
A new class can be created like this:
class Person {
constructor(first, last) {
this.firstname = first;
this.lastname = last;
}
getName() {
return this.firstname + " " + this.lastname;
}
}
// new Person("Good", "Boy") instanceof Person
extends
Keyword
JavaScript does NOT have real inheritance1.
You can use the extends
keyword like you would in Java. But there will be no warning if you forget a call to super
.
class constructor Student extends Person {
constructor (first, last, grade) {
super(first, last); // Do not forget!
this.grade = grade;
}
getGrade(){
return this.grade;
}
}
const s = new Student("Top","Student", 5.5);
But what does actually happen if this is not real inheritance? Instead prototype chaining is used.
What is Prototype Chaining
Each JavaScript object has a __proto__
object, which can link it to another object. In our example above, the Student
has a __proto__
object which links to the Person
prototype.
If you then call a method on a student object, it will first search if Student
itself has a method with that name. If not it will look at the prototype, inside of the __proto__
property. This will then go up the chain (see where the name comes from?) until the __proto__
property is null
. Then we know that no such method exists.
const s = new Student("Top","Student", 5.5);
s.getName(); // calls the method in Person
s.getGrade(); // calls the method in Student
Problems with Prototyp Chaining
Because everybody has access to the prototype via __proto__
everybody can change at any time this prototype. This will propagated to all other instances.
It is also possible to replace the prototype at runtime.
Object.setPrototypeOf(obj, proto);
Using Delegation Instead
Instead of using class
and extends
, we can also use delegation.
function Person(worker) {
const worklog = [];
return {
worklog: worklog,
work: () => worklog.push(worker.work()),
};
}
const manager = Person({ work: () => "" });
function Student(name) {
return {
name: name,
work: () => name + " filled quiz",
};
}
const p = Person(Student("top"));
Depending on what you consider real inheritance of course.
Type System
Because JavaScript has no compiler, types cannot be enforced by it. It can, however, be enforced by your IDE.
Built in Types
JavaScript has a whole list of primitive builtin types:
Boolean
Number
String
Object
Null
Undefined
Function
...
It has also a couple of classes, that can be used the same way in JsDocs:
Array
Date
Error
Map
...
JsDoc
JsDoc looks very similiar to JavaDoc. It has, on account of the missing compiler, a couple more features though. It can not only document but also define the input/output types or define entire "custom types".
This is a very basic definition:
/**
* This is a basic description of a function.
* @param { String } name - The name of the person.
* @param { Number } age - The age of the person.
* @returns { String } A greeting message.
*/
function greet(name) {
return `Hello, ${name}! You are going to be ${age + 1} next year.`;
}
Common Tags
There are a lot of tags in JsDoc. Here are the most frequently used listed, a couple of them have more infos further below.
@function
- Explicitly marks a block as a function, often for function expressions or anonymous functions.@param
- Describes a function parameter.@template
- Allows generic types, similar to TypeScript generics.@returns
- Describes what the function returns.@type
- Declares the type of a variable.@typedef
- Creates a custom type, like an object structure.@property
- Defines properties in a custom type.@constructor
- Indicates that a function is intended to be used as a constructor (for creating instances of an object).@callback
- Defines a function signature expected to be used as a callback type, specifying its parameters and return type.
Define Data Types
Union type: This is useful if the parameter can have multiple types.
/**
* Get a string to describe the age of a person
* @param { Number | String } age - The age of the person.
* @returns { String } A greeting message.
*/
function age(age) {
return `You are ${age} year(s) old!`;
}
Intersection type: Here the type must be a person who is also cool. This helps enact even more specific type checking.
/**
* Log how cool a person is
* @param { Person & Cool } person - A cool person.
*/
function cool(person) {
console.log(`Hey, ${person.name} is really cool!`)
}
String literal type: If you want to enable a couple of different strings. Very much like enums in Java.
/**
* Get a string to describe the greating of me or you
* @param { "me" | "you" } - arg Me or you
* @returns { String } A greeting message.
*/
function age(arg) {
return `Hello, you are ${arg}!`;
}
Capability:
This can be used to describe what kind of capabilities an object should have, i.e. it should have a properties age
that is of type number
.
/**
* Processes an object with properties `a` and `b`.
* @param {{a: string, b: Function}} obj - The object to process.
* @returns {void}
*/
function processObject(obj) {
console.log(`Value of a: ${obj.a}`); // We know a must be a string
obj.b(); // Invoke the function
}
Define Interfaces
With the typedef
tag, you can define your own data structures. These can then be used like types, i.e. @returns {Person}
.
/**
* @typedef {Object} Person
* @property {string} name - The name of the person.
* @property {number} age - The age of the person.
* @property {Function} greet - A method that returns a greeting message.
*/
Generics
Using @template
, you can use generics.
/**
* @template T
* @param {T[]} array - An array of elements of type T.
* @returns {T} The first element of the array.
*/
function getFirstElement(array) {
return array[0];
}
Functions: With this you can describe functions that use currying, in different degrees of precision.
The most simple way to describe something as a function, that takes A
and B
which produces C
.
/**
* A curried function that takes a string and returns another function.
* The returned function then takes a number and returns a boolean.
* @type { (string) => (number) => boolean }
*/
const isLongerThan = (str) => (length) => str.length > length;
To be a bit more precise, you can also name the parameters:
/**
* A curried function that takes a number `a` and returns another function.
* The returned function takes a number `b` and returns their sum as a string.
* @type { (a:number) => (b:number) => string }
*/
const addAndStringify = (a) => (b) => String(a + b);
It is also possible to throw generics into this mix:
/**
* A curried function that takes a value of any type `T` and returns another function.
* The returned function takes a string and simply returns the original `T` value.
* @type { <T> (a:T) => (message:string) => T }
*/
const echoAfterMessage = (a) => (message) => {
console.log(message);
return a;
};
Best Practices
If you are using types, or JsDoc in general, there are a couple of best practices you may want to follow:
- Set up your IDE properly: it will make your life so much easier
- Follow conventions: pick or define a convention and stick to it
- Add examples: makes it so much easier to figure out how a function call should look
- Link to documentation: use
@link
to link to relevant documentation - Validate the documentation: validate what you've documented with test cases
Immediatly Invoked Function Expression
An immediatly invoked function expression, short IIFE, are functions without names, that are executed immediatly.
JavaScript function initially look a lot like Java functions, just without the type system.
You can achieve this by writting an empty lambda function and define this as a function:
(() => {
//do stuff
})()
Ok thats cool but how did we get here? If we do it step by step it gets a lot easier to understand:
function foo(){} foo();
(function foot(){})(); //evluate directly, loose the function call
(function(){})() //loose the function name
(() => {})() //loose the keyword
Using IIFE can have a couple advantages:
- Encapsulates variables and their values which prevents pollution of (global) namespace
- Mimize risks of name collisions
Lambda Calculus
Lambda calculus describes can describe all the operations one can do on a computer. This can help us to understand (JavaScript) code.
Basics
Lambda calculus has three operations:
- α => Rename parameter
- β => Apply argument
- η => Cancel parameter
Technically we only need α and β, from those two we could then infer η. Too keep it simple, we will not do that and, instead, look at η as a single operation.
Alpha Translation (α)
This operation allows us to rename things. This is really simple, like so:
const id = x => x
Beta Reduction (β)
All this operation does is search x on the left and replace it with x on the right
.
Practically this may look like this:
(x => x)(1) //x is our foo(x) and (1) is the method call foo(1)
(1 => x)(1) //if we replace our x with the input it looks like this
(1 => 1)(1) //which means that the output x is also 1...
1 // and which means everything cancels each other out
Eta Reduction (η)
If we have an expression where on the right-most left is a parameter and on the right-most right is exactly an input, which is equivalent to the parameter, we can remove both of those.
Sounds complicated, is really simple:
x => foo (x) //right-most parameter and input is both x
foo // so this is foo
If we have more than one parameter or input, we always have to remember to focus on the right-most:
x => y => both(x)(y)
x => both(x) //remove the y
both //remove the x
Lambda II
This is a continuation of the "Lambda Calculus" chapter. So if you haven't read that one yet, do so here.
Atomic Lambdas
With the things we learned, we can defined atomic lambda terms, which are the most basic terms. You should have seen these already:
const id = x => x;
const konst => x => y => x;
These two allow us to derive pretty much everything needed from them. Basic things such as true
, false
, or
and so on. See a couple of those here:
const T = konst;
const F = snd;
const and = a => b => a (b) (a);
const or = a => b => a (a) (b);
//.. and more
Pair, Product Type
The preparation of those "basic" things, allow us to do more complex things. For example define a pair (or product) type.
const Pair = x => y => f(x)(y);
const fst = p => p(T);
const snd = p => p(F);
This basically defined a function which "holds" two values. The fst
and snd
functions allow us to retrieve the first and second value respectively.
If we expand this concept a bit it allows us to do crazy things. See the code below. It is very Haskell like but just in JavaScript.
//This defines a person "object" that is a pair of a names and an age. The names are also a pair themselves.
const person =
firstname =>
lastname =>
age =>
pair(pair(firstname)(lastname)(age));
//These functions can read the values
const firstn = p => fst(fst(p));
const lastn = p => snd(fst(p));
const firstn = p => snd(p);
Immutability and Lazy
Using this style of programming has two advantages: - the "objects" are immutable, means you cant change them - they are lazily evaluated, means they compute on demand not before
Either Function
Armed with that knowledge, we can create an either
function. This function should execute either one lambda or another, depending on the result of a first function.
const Left = x => f => g => f(x); //take the argument and execute function g
const Right = x => f => g => g(x); //take the argument and execute function g
const either = e => f => g => e(f)(g); //if e evaluates to true, execute f else execute g
//we then invoke it like that
const someFunc = true ? Left("This would be called if it were evaluated as false") : Right("This is actually called");
either (someFunc)(x => console.log(x => "Never called anyway"))(x => "This returns: " + x);
Higher Order Functions
Higher order functions are functions which receive other functions as arguments. Most commonly they are used on collection data structures, such as lists and arrays. For the sake of simplicity we will use arrays here.
Map
A very basic higher order function is map()
where a function is applied to each value inside of an array. The function then returns a new array.
[1,2,3].map(x => x *2);
// this results in a new array with the values [2,4,6]
Instead of writing the lambda each and every time, we can also define it beforehand. If we use currying we can even decide how much we want to abstract.
const multiply = a => b => a * b;
const multiplyBy2 = multiply(2)
[1,2,3].map(x => x *2);
[1,2,3].map(x => multiplyBy2(x));
[1,2,3].map(multiplyBy2);
Which way is the best is mostly personal preference.
Filter
The filter()
function creates a new array with all elements that pass the test defined by the provided function. It only returns the elements that evaluate to true.
const odd = x => x %2 == 1;
[1,2,3].filter(x => odd(x));// new array with the value(s) [2]
[1,2,3].filter(odd);// same thing as above but written differently
Reduce
The reduce()
function executes a provided reducer function on each element of the array, resulting in a single output value. It's used for aggregating data like summing up an array.
const sum = (accu, cur) => accu + cur;
[1,2,3].reduce(sum);// output: 6 (which is 1 + 2 + 3)
Apply Multiple Functions
These functions can of course be chained together.
const numbers = [1, 2, 3, 4, 5];
const result = numbers
.filter(num => num % 2 === 0) // results in [2, 4]
.map(num => num * 2) // results in [4, 8]
.reduce((acc, curr) => acc + curr, 0); // then finally returns 12
Logging
JavaScript logging is similar to logging in basically every other language.
Basic Log Levels
JavaScript offers several built-in logging methods, each with a specific purpose. The most commonly used are:
Log
Used for general output of information to the console. It's often used for basic debugging or status messages.
console.log('Just general output');
Info
Used to display informational messages that are not errors but important to note.
console.info('Some basic information');
Warn
Used to indicate potential problems or warnings that don't stop the execution of the program but might lead to issues.
console.warn('Hey this could be important');
Error
Used to log error messages when something goes wrong, especially when an exception occurs.
console.error('SOMETHING HAS GONE WRONG');
Debug
Used for detailed debugging information. In some browsers, these messages may be hidden by default unless you enable verbose logging.
console.debug('Psst, I am a sneaky message');
Logging Objects
It is also possible to pass entire objects or arrays of objects to the logging functions. These then get logged out nicely in JSON.
const user = {
name: 'Alice',
age: 30,
occupation: 'Engineer'
};
console.log('User object:', user);
This will result in something like this (may look different depending on your browser):
User object: Object { name: "Alice", age: 30, occupation: "Engineer" }
Of course the same also works for arrays of objects.
const users = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
{ name: 'Charlie', age: 35 }
];
console.log('Array of users:', users);
Which then results in:
Array of users: Array(3) [ 0: Object { name: "Alice", age: 30 }, 1: Object { name: "Bob", age: 25 }, 2: Object { name: "Charlie", age: 35 } ]
Fun fact: you can also display an array as a table with the console.table()
function!
Scripting
A scripting language, like JavaScript, is characterised mainly by the ability to execute arbitrary texts from sources like files, URLs, the user and so on. Other characteristics are that they are interpreted and thus have a lenient type system.
Normally this is a big no-no. A very very big no-no. But this is essentially what every browser does: load file from example.com
and execute the code that is saved there.
Use Cases
Scripting is used at an endless amount of places:
- Command line
- Automation (DevOps)
- Build System
- Templating
- Business Rules
- Formulae
- DSL (Data Structure Language)
Examples
Because that was a lot of theory, here a couple of examples.
"Auto" Imports
If we want to import JavaScript files with the <script>
tag into our HTML, we normally have to include each module in a seperate tag. This gets cumbersome fast. Instead we can dynamically create <script>
tags, whiche then import the needed files.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>All Tests</title>
</head>
<body>
<script>
["function", "lambda"].forEach(n =>
{
document.writeln('<script src="' + n + '/' + n + '.js"><' + '/script>')
document.writeln('<script src="' + n + '/' + n + 'Test.js"><' + '/script>')
}
);
</script>
</body>
</html>
Plotter
If we want to take a (math) function that a user gives us and evaluate it, we can do that a couple of different ways.
The easiest way would be to call the eval
function, which then evaluates the supplied string.
function start() {
const userFunction = document.getElementById("user_function");
const canvas = document.getElementById("canvas");
display(canvas, (x) => eval(userFunction.value));
userFunction.onchange = (_) =>
display(canvas, (x) => eval(userFunction.value));
}
// lot of code missing for brevities sake
One problem of this approche is that it is very very innefficient. Instead we can use the functional programming things we learned in the weeks before and rewrite it:
function start() {
const userFunction = document.getElementById('user_function');
const canvas = document.getElementById('canvas');
const f = _ => Function( "x", "return " + userFunction.value);
display(canvas, f() );
userFunction.onchange = _ => display(canvas, f() );
}
// lot of code missing for brevities sake
Check out the offical WEBPR repo for the entire code and even more cool examples!