This is more opinion than fact, but it is shared opinion on the webs. Var has been effectively replaced by the two new instantiation words: let and const. Although Var does something different, and therefore might still be considered, the thing that it does should be considered 'not cool', and therefore all future code can effectively drop the Var command.
Let is the all around replacement for Var. It does everything the Var command would have been used to do, with one major difference. It can be scoped to any block statement. Where as Var could only be scoped to a function block, Let can be scoped to any block, including if, while, for, and switch. When using this for the first time, this can lead to issues if you're not prepared for this new mindset.
let a = 5;
for(let b=0;b<10;b++){ a = b; }
console.log(a,b); // Uncaught ReferenceError: b is not defined
Ultimately this should lead to cleaner code, and better mindsets. Define your variables properly before using them, and keep them defined to their appropriate scope.
Const is essentially the same as Let, except that it's immediate value becomes immutable. This means a const value once defined cannot be redefined, however it's properties and iterables can still be modified.
const a = 5;
a++; // Uncaught TypeError: Assignment to constant variable.
const b = {};
b.name = 'George';
console.log(b.name); // George
Here are some familiar old ways of writing a function. Making a function with the function name()
syntax will hoist the function to the top of the stack, and make it available to anything anywhere. Using the foo = function()
syntax will anonymize the function as a variable value, and only be available to future code, similarly a function created as an object method is only available after its creation.
function foo(){ return 'bar'; }
var foo = function(){ return 'bar'; }
var some = {
foo : function(){ return 'bar' }
}
Arrow functions are a new way of writing functions. It does not outright replace the old function syntax, but it can be used more effectively in a number of conditions. Arrow functions are never hoisted. They are only ever used as inline values on variable names, and as such, should probably always be set up as const values to make them immutable.
const foo = () => 'bar';
const some = {
foo : () => 'bar'
}
Arrows can be setup with all manner of parameters, but they can be written in slightly different ways, depending on how many are present. With zero arguments, you must represent the parameters of an arrow function using parenthesese. When using only one argument, you can drop the parens, but then you must bring them back for more than 1 argument.
Understand that if you just copied the next example into a browser, it would fail, since a const can't be defined more than once.
const foo = () => 'bar'; // No arguments
const foo = a => 'bar'+a; // Single Argument
const foo = (a,b) => a+'bar'+b; // Multiple Arguments
As an expression body, an arrow function should always be one thing, and it will return that one thing as a value. With a statement body, the arrow function can have multiple statements, and then return a value like normal.
const foo = a => 'bar'+a; // Single expression
const foo = a => { // Multiple statements
console.log(a);
return a;
}
Arrow functions do not have their own this
value. They share their this
with their parent. This makes them wonderful for certain method callback functions, but actually not as great for simple event callbacks. An event callback might want to know what element just called it, but an arrow callback would not know that information. Of course you can use an arrow function there as well, just use e.target
to get the same value as the anonymous function's lexical this
.
This next example jumps ahead a bit to show something useful.
document.querySelector("a").addEventListener("click",function(e){
console.log(this.href); // `this` will be the element that was clicked
[...this.attributes].forEach(o=>
console.log(`${this.nodeName} - ${o.nodeName}`) // `this` is still the original event scope
);
});
document.querySelector("a").addEventListener("mouseover",e => {
console.log(e.target.href); // e.target will be equivalent to the anonymous function's `this`
console.log(this); // Window Object
});
The template literal is a new syntax for writing strings, which uses backticks instead of quotes to indicate a string. Template literals can be single line or multi line.
let foo = `<div>Text</div>`; // single line template
let bar = `
<div>
<span>Text</span>
</div>
`; // multi line template
But unlike the single and double quoted strings, backtick templates can interpolate variables and directives without concatenation. This allows for code that is much easier to follow, and the creation of complex template structures.
const foo = 'bar';
console.log(`'foo' equals ${foo}`); // 'foo' equals bar
// A simple templater can be created with backtick strings
const templater = fn => data => fn(data); // curried function that expects template first, then data object
const showName = templater(o=>`My Name is ${o.firstName} ${o.lastName}`); // create the template function
showName({firstName:'George',lastName:'Foobar'}); // My Name is George Foobar
Defining default values for the arguments of a function can lead to more functionality.
const addFiveOrSomething = (a, b = 5) => a + b;
addFiveOrSomething(4); // 9
addFiveOrSomething(4,10); // 14
The rest
and spread
operators are ellipses that indicate an array should be exploded to an interface that would normally take multiple individual comma separated values, or imploded from a list to an array. Arrow function don't have an arguments array, and so one of the things that can combat this is the Rest operator. This can also be useful when you make a function that expects one named value, and then want to gather the rest of the values into an array.
const howManyArgs = (...args) => args.length;
howManyArgs(0,1,2); // 3;
const thisTimesThose = (num, ...nums) => num * nums.length;
thisTimesThose(5,0,1,2,3); // 20
Sometimes it can be useful to pass an array as the arguments for a function. It can also be useful to turn an array-like object into an array, such as a querySelectorAll result.
const showName = (first,last) => `${last}, ${first}`;
showName(...['George','Foobar']); // Foobar, George
// Spread an array-like inside an array constructor to gain Array methods
[ ...document.querySelectorAll('a') ].forEach(o => console.log(o.href) );
Destructuring values can be extremely valuable when converting between arguments and returns. This quickly takes values from objects or arrays and extracts them to variable names.
Destructuring arrays can easily pull out and name specific values from within an array. This is especially useful for certain functions that return an array of values.
let [a,b] = [2,8];
a; // 2
// Skip values you don't care about
let [c,,d] = [2,4,8];
d; // 8
// Use a spread operator to destructure the rest of the values
let [fullmatch, ...matches] = /^(\w+)@(\w+)\.(\w+)$/.exec('george@gmail.com');
fullmatch; // 'george@gmail.com'
matches; // ['george', 'gmail', 'com']
Objects can be destructured automatically, or renamed to something new.
const obj = {name:'George',type:'Teacher'};
let {name,type} = obj; // Shorthand destructure to the same names
let {name:subName,type:subType} = obj; // Destructure to new names: subName and subType
The for
loop is a shorthand version of a common while loop. It's usefulness becomes apparent when dealing with arrays and known length iterables. It can be improved by predefining certain constantly checked values, or by iterating in a reverse order.
for(let i=0; i<5; i++) console.log(i);
for(let i=0,l=5; i<l; i++) { // With multiple instantiations
console.log(i); // As a statement body
}
for(let i=5; i>=0; i--) console.log(i); // Reverse loop
A For..In
loop can be handy for looping through all array items, or even through object properties. With For..In loop, you must define an iterator that will represent each index as the array is looped through.
const arr = [2,4,8];
for(let i in arr) console.log(`${i}=${arr[i]}`); // 0=2, 1=4, 2=8
A For..Of
loop, on the other hand, loops just like the For..In but its iterator represents the current value. This is especially useful, when using some newer iterables, but also when you just don't care about the index of the current element.
const arr = [2,4,8];
for(let o of arr) console.log(o); // 2, 4, 8
If you want to work with React, Angular, or higher level VueJS, you'll encounter import pretty quickly. You can import without using those kinds of node status frameworks. Add a type attribute of module.
<script type="module" src="script.js"></script>
The core of modules is exporting data from one file for access by another. This allows a developer to import what's necessary without polluting the scope with unwanted variable names.
// helpers.js
export rand = (n,x) => Math.round(Math.random()*(x-n)+n);
export rebounce = (c,f,a,t=100) => !c?!setTimeout(()=>f.apply(c,a),t):true;
You can also set up an export at the end of a document.
// otherhelpers.js
const rand = (n,x) => Math.round(Math.random()*(x-n)+n);
const rebounce = (c,f,a,t=100) => !c?!setTimeout(()=>f.apply(c,a),t):true;
export {rand,rebounce};
Once you've exported some things, you can import those into another script.
// script.js
import * as h from "helpers";
h.rand(0,5); // a random number from 0 to 5;
You can also import specific values if needed.
// otherscript.js
import {rand} from "helpers";
rand(20,40); // a random number from 20 to 40;
These array methods aren't all new. But many people still haven't worked them into their workflow yet, so let's get a handle on these concepts. Most of these involve some sort of loop, and most of them have a mostly similar structure for use.
The forEach
method loops through an array and returns nothing when it completes. It's like running a for loop on an array. The map
method loops through an array and returns a new array with its values potentially altered from the original. It is most useful when wanting to edit the values of every single element in an array, but still retain the original.
Both of these function and many of these other array methods require a callback function which will be called upon each element in the array, and this callback will get passed three values: the current object, the current index, and the original array.
let arr = [1,2,3,4];
arr.forEach((o,i,a) => o * 2); // 2, 4, 6, 8
arr; // [2,4,6,8]
// Map will return a new array
arr.map((o,i) => o * i); // [0, 4, 12, 24]
arr; // [2,4,6,8]
The reduce
method is a lot like map, except that instead of ending up with one new array, you will end up with one new... anything. Often times, it can be used to reduce an array of numbers to a single number, but it can just as easily be used to reduce an array of objects to one string or template. It gets used slightly differently than most of the other array iteratives. The predicate gets passed the same three values: object, index, array, and an extra reducer value. The reduce method itself can be passed an optional starting value.
let arr = [{name:'Ginny',age:16}, {name:'Ron',age:17}, {name:'George',age:19}, {name:'Fred',age:19}];
// Templating
arr.reduce((r,o,i,a) => r + `${o.name}<br>`, ''); // Ginny<br>Ron<br>George<br>Fred<br>
// Templater
const templater = fn => arr => arr.reduce((r,o,i,a) => r + fn(o,i,a),''); // Curried Function
const showFamily = templater(o => `<p>${o.name}<p>`);
showFamily(arr); // <p>Ginny</p><p>Ron</p><p>George</p><p>Fred</p>
// Mean age
Math.round( arr.reduce((r,o) => r + o.age, 0) / arr.length ); // 18
The filter
method reduces an array by returning only elements that pass a boolean test. It doesn't affect the original array, returning instead a new array. The find
method works the exact same way, except where Filter will return an array even if it only finds one item, Find will only ever return one object or undefined. Both of these functions' callbacks will get passed the object, index, and array values.
let arr = [1,2,3,4];
// Filter returns an array based on a predicate boolean
arr.filter(o => o % 2); // [2,4]
arr.filter(o => o == 2); // [2]
arr.filter(o => o == 5); // []
// Find returns one object or a boolean
arr.find(o => o > 2); // 3
arr.find(o => o == 5); // undefined
The some
method will test to see if any of the elements inside an array pass a test, and it will return a boolean. The every
method does the same kind of test, but it only wants to see if ALL the elements pass the test.
let arr = [1,2,3,4];
arr.some(o => o > 2); // true
arr.every(o => o == 2); // false
These are not new at all, but just as a refresher, pop
and push
remove and add items from the end of an array, shift
and unshift
remove and add from the beginning of an array.
let arr = ['red','green','blue'];
// Pop removes from the tail, and returns that value
arr.pop(); // blue
arr; // ['red','green']
// Push adds to the tail, and returns the new length
arr.push('magenta'); // 3
arr; // ['red','green','magenta']
// Shift removes from the head, and returns that value
arr.shift(); // red
arr; // ['green','magenta']
// Unshift adds to the head, and returns the new length
arr.unshift(...['yellow','cyan']); // 3
arr; // ['yellow','cyan','green','magenta']
Also not new, splice
can add or remove from anywhere in an array. The slice
method can return part of an array or all of it as a shallow copy.
let arr = ['red','green','blue'];
// Splice can remove and add from anywhere in an array, and returns an array of affected items
arr.splice(1,0,...['white','black']); // []
arr; // ['red','white','black','green','blue']
arr.splice(-1,1,'gray'); // ['blue']
arr; // ['red','white','black','green','gray']
// Slice returns a shallow copy of a part of an array
arr.slice(); // ['red','white','black','green','gray']
arr.slice(1); // ['white','black','green','gray']
arr.slice(-1); // ['gray']
Object methods contain a lot of really cool things that can help when dealing with certain kinds of data and Object Inheritance.
The assign
method lets you take one object, and stamp it's properties onto another object. It will affect the original object, and return that same object when it completes. The create
method lets you Take one object or object primitive and create a new instance of that same kind of object, even without a proper constructor.
// Assign properties to an object
let ctx = document.querySelector("canvas").getContext('2d');
Object.assign(ctx, {strokeStyle:'blue', lineWidth:3});
// Instantiate a new object of a similar kind
let animal = {type:'animal',age:0};
let dog = Object.create(animal);
dog.type = 'dog';
animal.type; // animal
The keys
method returns an array of all the keys of an array, array-like, or object. The values
method returns an array of all the values of those kinds of things.
Object.keys({a:2,b:4,c:8}); // ['a','b','c']
Object.values({a:2,b:4,c:8}); // [2,4,8]
The entries
method will take an array, array-like, or object, and turn it into a matrix of arrays with key value pairs. Combine this with For..of and destructuring and you can make a foreach much like php.
let arr = [1,2,4,8];
Object.entries(arr); // [[0,1],[1,2],[2,4],[3,8]]
for(let [key,val] of Object.entries(arr)) {
console.log(`${key} = ${val}`); // 0 = 1, 1 = 2, 2 = 4, 3 = 8
}
The apply
and call
methods for functions work very similarly. Both methods call a function with a supplied this value and an amount of parameters. Use call when you want to call a function with individual parameters, and use apply when you want to call a function with an array of parameters.
[].forEach.call(
document.querySelectorAll('a'),
o => o.addEventListener('click', e => console.log(e,o))
);
Math.max.apply(null, [1,2,4,8]); // 8
Use the JSON object to convert data between strings and objects. This is especially useful to store and retrieve complex data for localStorage and sessionStorage.
The parse
method will take a genuine JSON string, and turn it into Javascript objects. It will fail if the string does not follow strict JSON guidelines.
JSON.parse(`[{"name":"George"}, {"name":"Fred"}]`); // [{name:"George"},{name:"Fred"}]
JSON.parse(`{age:0}`); // Uncaught SyntaxError: Unexpected token
The stringify
method will take a Javascript object, and turn it into a JSON string.
JSON.stringify([{name:"George"},{name:"Fred"}]); // '[{"name":"George"}, {"name":"Fred"}]'
Using the JSON object with sessionStorage and localStorage can give you much better and easier data storage capacity. Of course using Storage means you're limited to JSON file types of data, so no functions, but it still makes it so much easier to store complex values.
const dataGet = (k,s='localStorage') => JSON.parse( window[s].getItem(k) );
const dataSet = (k,v,s='localStorage') => window[s].setItem( k, JSON.stringify(v) );
A promise
is returned from asynchronous concepts. Promises are something that may happen some time in the future. There are many parts of Javascript that already return a promise.
Instantiate a new Promise and then chain off a then method to define what to do the promise resolves.
new Promise(function(resolve,reject){
// some code
resolve('Finished');
})
.then(data => {
console.log(data);
})
The setTimeout
method works asynchronously, and might be the easiest way of seeing how promises really work.
new Promise(function(resolve,reject){
setTimeout(()=> resolve('Finished'), 3000);
})
.then(data => {
console.log(data); // Doesn't show up for 3 seconds
})
The fetch
API produces a promise and can be used to up your ajax game. Fetch a document, and then do something with the returned data. There's usually an intermediary point where we return the body of whatever document we fetched, and then parse that into json. This process itself produces another promise.
// Downloading data
fetch("http://somesite.com/script.php")
.then(response => response.json())
.then(data => {
console.log(data); // should be a json object of the url's body
})
// Uploading data
fetch("http://somesite.com/script.php", {
method: 'POST',
body: JSON.stringify({name:"George"}),
headers:{
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log(data);