- You can run JavaScript code in a web browser's developer console (press
F12).
- In the console, any statement will be executed and the result will be printed:
const a = 10; // undefined - variable assignment returns undefined
2 + 2; // 4 - expression returns its value
- You can also use
console.log(...); to print values explicitly.
- Statements are terminated by a semicolon (
;). Although they can be usually omitted due to ASI (Automatic Semicolon Insertion), it's a good practice to always include them - there are cases where omitting them would cause an unexpected behavior.
- Comments are written using
// for single-line comments and /* ... */ for multi-line comments (and /** ... */ for JSDoc).
- Use
const to declare variables (or let if you need to reassign them). Do not use var. More on that later.
- Many pitfalls of JavaScript can be avoided by using strict mode. It can be enabled by adding
'use strict'; at the top of a file. Note that many modern environments (e.g., ES6 modules) enable strict mode by default.
- We won't cover "features" that are only available in non-strict mode.
-
MDN Web Docs - comprehensive documentation of JavaScript and web APIs
- Use this whenever you need to look something up.
-
JavaScript.info - beginner-friendly tutorial
- Less like a reference, more like a bedtime reading.
-
ECMAScript specification - the official specification of JavaScript (very detailed and technical)
- If you feel like developing your own JavaScript engine.
-
You Don't Know JS - in-depth but easy-to-read book series
- Most opinions (especially in book 3) aged like milk but the technical stuff is still relevant.
- Javascript has 7 primitive types:
null, undefined, boolean, number, bigint, string, and symbol.
- You can get the type of a variable using the
typeof operator:
- Except for
null, which returns:
-
undefined means an absence of a value while null is an explicitly invalid value.
-
bigint and symbol are more advanced types that we won't cover in this course.
- Number is represented as
double (64-bit floating point).
- Special numeric values include
NaN, Infinity, and -Infinity.
- It's precise enough for integer arithmetic up to
2^53 - 1 (cca 9e15). Use Number.MAX_SAFE_INTEGER to get this value.
- Strings use either single quotes (
'a') or double quotes ("b"). It's just a matter of preference (but single quotes are better).
- There are also template literals which use backticks (
`Hello, ${name}!`). You can write any expression inside ${...}.
- All primitive types are immutable and passed by value.
- There is also the
object type, which includes arrays and functions (they are just special objects). All objects are passed by reference.
- The
typeof operator isn't very consistent:
typeof {}; // "object" - object
typeof []; // "object" - array
typeof function() {}; // "function" - function
- Use
Array.isArray(value) to check whether the value is an array.
- When operating on values of different types, JavaScript will coerce one or both values to a common type:
10 + '2'; // "102" - everything can be coerced to string
10 - '2'; // 8 - subtraction isn't defined for strings so they are coerced to numbers
10 - 'a'; // NaN - 'a' can't be coerced to a number
- The same works for the equality operator
==.
- To avoid unexpected coercion, use the strict equality operator
===, which doesn't perform type coercion:
10 == '10'; // true
10 === '10'; // false
- Coercion is problematic if it's "unexpected". However, there are some useful idioms:
const negative = !value; // coerces to boolean and negates the result
const boolean = !!value; // coerces to boolean (double negation)
const number = +value; // coerces to number
const string = '' + value; // coerces to string
- There are only 7 falsy values in JavaScript:
undefined, null, false, '' (empty string), +0 (i.e., 0), -0, and NaN. All other values are truthy.
- Logical operators
&& and || first coerce values to boolean, but then return one of the original values:
0 || 'default'; // "default"
42 && 'value'; // "value"
- A chain of
|| returns the first truthy value (or the last value). A chain of && returns the first falsy value (or the last value).
- Objects are unordered collections of key-value pairs. Keys are strings (or symbols), and values can be of any type.
- An object is created using curly braces
{}:
const object = {
key1: 'value1', // basic key
['key 2']: 42, // useful for keys with invalid characters (e.g., ' ' or '-')
foo() { // functions can be values too
return 'bar';
}
};
- Object properties can be accessed using dot notation or bracket notation:
object.key1; // "value1"
object['key 2']; // 42
object.foo(); // "bar"
- Object properties can be added, modified, or deleted:
object.key3; // undefined
object.key3 = true;
object.key3; // true
delete object.key1;
object.key1; // undefined
- You can use the
in operator to distinguish between non-existing and undefined properties:
'key1' in object; // false
'key3' in object; // true
- All primitive types (except for
null and undefined) have automatic object wrappers that provide additional methods and properties:
'hello'.length; // 5
true.toString(); // "true"
(42).toString(); // "42" - brackets are needed to avoid ambiguity with decimal points
1.23.toFixed(1); // "1.2"
- There is also a mechanism called prototypes that mimics OOP features (classes, inheritance, etc.). It will be covered later.
- Functions are first-class citizens in JavaScript, meaning they can be assigned to variables, passed as arguments, and returned from other functions.
function add(a, b) {
return a + b;
}
const subtract = function(a, b) {
return a - b;
};
- Arrow functions have a different syntax and lexical
this binding (more on this later):
const multiply = (a, b) => {
return a * b;
}
const divide = (a, b) => a / b; // implicit return
- Functions can also have properties because they are objects:
function foo() {
return 'bar';
}
foo.property = 'foo';
foo.property; // "foo"
- Just beware that can doesn't mean should.
- Functions are variadic (they can take any number of arguments). There is no function overloading and no named arguments. However, you can simulate this by passing objects as arguments:
function createUser({ name, age, isAdmin = false }) {
// ...
}
const user = createUser({ name: 'Jo', age: 42 }); // isAdmin will be false by default
- Arrays are ordered collections of values of any type. They are created using square brackets
[]:
const array = [ 1, 'two', { value: 'three' }, [ 4 ] ];
array[0]; // 1
array[4]; // undefined
- Although you can write to any index, leaving gaps can lead to unexpected behavior. Always prefer methods like
push and pop to modify arrays:
array.push(5); // adds 5 to the end
array.pop(); // removes and returns the last element
- Arrays can have properties as well, since they are objects. There is also a special
length property that reflects the number of elements in the array.
- Array functions bring functional programming concepts to JavaScript:
const numbers = [ 1, 2, 3, 4, 5 ];
const odd = numbers.filter(n => n % 2 !== 0); // [ 1, 3, 5 ]
const doubled = odd.map(n => n * 2); // [ 2, 6, 10 ]
const sorted = doubled.toSorted((a, b) => b - a); // [ 10, 6, 2 ]
sorted.forEach(n => console.log(n)); // prints 10, 6, 2
const sum = sorted.reduce((n, ans) => n + ans, 0); // 18
- The original arrays are never modified. However, there are some older mutating methods (
sort, splice, etc.). Each of them has a non-mutating counterpart (toSorted, toSpliced, etc.).
- To iterate over values of arrays and other iterables in the old-fashioned way, use the
for...of loop:
const array = [ 'a', 'b', 'c' ];
for (const value of array)
console.log(value); // "a", "b", "c"
const object = { x: 1, y: 2, z: 3 };
const entries = Object.entries(object); // [ [ "x", 1 ], [ "y", 2 ], [ "z", 3 ] ]
for (const [ key, value ] of entries) // array destructuring, see below
console.log(key, value); // "x" 1, "y" 2, "z" 3
- For iterating over enumerable string properties of an object, use the
for...in loop:
for (const key in object)
console.log(key, object[key]); // "x" 1, "y" 2, "z" 3
- Note the even though you can sometimes mix these loops (after all, arrays are objects), it's might lead to unexpected results (e.g.,
for...in for arrays returns indexes as strings, not numbers).
Destructuring and Spread Operator
- Objects can be destructured into variables:
const object = { a: 1, b: 2, c: 3, d: 4 };
const { a, b: x, ...rest } = object; // a = 1, x = 2, rest = { c: 3, d: 4 }
- Properties can be renamed during destructuring using the
b: x syntax. Remaining properties can be collected using the ... (spread operator). We can even destructure nested objects.
- The same works for arrays (except renaming):
const array = [ 1, 2, 3, 4 ];
const [ x, y, ...rest ] = array; // x = 1, y = 2, rest = [ 3, 4 ]
- The spread operator can also be used to create arrays:
const array1 = [ 2, 3 ];
const array2 = [ 5, 6 ]
const joined = [ 1, ...array1, 4, ...array2, 7 ]; // [ 1, 2, 3, 4, 5, 6, 7 ]
- Objects work in the same way. However, properties with the same name will be overwritten by the latter ones.
- Lastly, we can collect (and pass) function arguments:
function sum(...numbers) {
return numbers.reduce((a, ans) => a + ans, 0);
}
const array = [ 1, 2, 3 ];
sum(0, ...array, 4); // 10
- Note that only the last argument can be spread - function
foo(a, b, ...rest) is valid, but bar(...rest, a, b) is not.
- This section is only applicable when running JavaScript in a web browser.
- Other environments (e.g., Node.js) provide different APIs.
- All browser APIs are accessible through the global
window object.
- All properties of
window are also accessible as global variables.
- If you create a global variable (by using
var in the global scope), it will be added as a property of window (and vice versa).
- Do not abuse this feature.
- Some useful properties of
window include:
-
document - the root of the DOM tree
-
screen - information about the user's screen
-
location - get/set the current URL
-
history - browse/manipulate the browser history
-
navigator - information about the browser, from language to clipboard
-
cookieStore, localStorage, ... - various storage mechanisms
-
setTimeout, setInterval, ... - timers (see Event Loop)
- Stands for Document Object Model.
- It's a JS API for interacting with HTML and XML documents.
- I.e., the following concepts can be accessed as JS objects.
- By "properties" and "methods", we mean properties and methods of these objects.
- The DOM represents the HTML document as a tree of nodes.
- Basically anything in the HTML document is a node - a tag, text, comment, etc.
- The root node is called
document.
- A special type of nodes are elements that represent HTML tags (e.g.,
<div>, <p>, <a>, etc.).
- You can get all child nodes of a node using the
childNodes property. To get only element children, use the children property.
- For example, the following
section has 2 child elements: h2 and p. However, it has 5 child nodes (including text nodes for whitespace).
<section>
<h2>Heading</h2>
<p>Some text.</p>
</section>
- Start with the
document, which is accessible as a global variable.
- It's better to not use the
Node properties like childNodes, parentNode, firstChild, lastChild, nextSibling, and previousSibling.
- However,
parentElement is useful.
- Instead, use
Element properties like children, firstElementChild, lastElementChild, nextElementSibling, and previousElementSibling.
- To access text children, use
node.textContent or htmlElement.innerText.
- Both return the text content of the whole subtree.
- The difference is that
innerText returns the "rendered" text (i.e., what would you get if you copy-pasted it from the browser).
- There is also
element.innerHTML and element.outerHTML, which return the HTML content as string.
- The second one includes the element itself.
- On
document, you can call getElementById(id) to get the element with the given ID (or null if not found).
- On any element (including the
document), you can use the following methods to find elements in its subtree:
element.getElementsByClassName(className) // live `HTMLCollection`
element.getElementsByTagName(tagName) // live `HTMLCollection`
element.querySelectorAll(selector) // static `NodeList`
element.querySelector(selector) // first matching `Element` (or `null`)
- Beware that
HTMLCollection is live (updates automatically when the DOM changes). This can lead to unexpected behavior (e.g., when iterating over it and modifying the DOM at the same time).
- Use
[ ...collection ] to convert it to a static array.
-
selector is a CSS selector. Although the method searches in the element's subtree, the CSS is relative to the HTML document.
- Use
:scope pseudoclass to refer to the current element in the selector.
-
textContent, innerText, innerHTML, and outerHTML are also setters.
- They replace all descendants of the element (and the element for
outerHTML) by the given text (or HTML).
- It's generally better to avoid setting HTML directly. Do not use these setters with user input unless you are absolutely sure what you are doing. You can easily create a script element with any kind of JS code so proper sanitization is a must.
- Use
document.createElement(tagName) to create a new element.
- Such element will be detached (not part of the DOM tree).
- Then use
Element methods like parent.append(new), parent.prepend(new), sibling.before(new), sibling.after(new), or sibling.replaceWith(new) to add it to the DOM.
- If you use an already attached node as
new, it will be removed from its previous position first.
- You can actually provide any number of
new arguments, including strings (which will be converted to text nodes).
- There are also older
Node methods like appendChild(new), insertBefore(new, sibling), or replaceChild(new, sibling) (all are called on parent).
- To remove an element, use
old.remove() or parent.removeChild(old).
- A very simple example of dynamically adding a new row to a table of users:
const user = { name: 'Joe', year: 1984, email: 'joe@ma.ma' };
const table = document.getElementById('users-table');
// Get the `<tbody>`. Another approach would be to use `table.tBodies[0]` but that only works for tables.
const tableBody = table.getElementsByTagName('tbody')[0];
const newRow = document.createElement('tr');
for (const key of [ 'name', 'year', 'email' ]) {
// It should be fine to dirrectly append to `newRow` here because the row itself is not yet attached to the DOM.
const cell = document.createElement('td')
newRow.append(cell);
if (key === 'email') {
const link = document.createElement('a')
cell.append(link);
link.href = `mailto:${user.email}`;
link.textContent = user.email;
} else {
cell.textContent = user[key];
}
}
// Let's insert the new row at the start of the table so that it's immediately visible.
table.prepend(newRow);
- Use
element.getAttributeNames() to get an array of all attribute names of the element.
- You can then use
getAttribute(name), setAttribute(name, value), and removeAttribute(name) to manipulate them.
- However, many attributes have corresponding properties on the element object itself - e.g.,
element.id, or element.className. They are both getters and setters.
- Specific elements may have additional properties - e.g.,
input.value, a.href, img.src, etc.
- To work with CSS classes, use the
element.classList property, which provides methods like add, remove, toggle, replace, and contains.
- For styles, use the
element.style property, which allows you to get/set individual CSS properties:
// Explicit methods with actual property names.
element.style.getPropertyValue('font-size'); // "16px"
element.style.setPropertyValue('font-size', '20px');
// Basically the same but shorter. Notice the camelCase naming (instead of kebab-case).
element.style.fontSize; // "20px"
element.style.fontSize = '16px';
- To get the computed style (i.e., after applying all CSS rules), use
window.getComputedStyle(element).
- Returns read-only object containing all CSS properties. It's live (updates automatically when styles change).
- Sometimes, you may want to store custom data on elements. For this, use the
element.dataset property, which provides a simple key-value store.
- For each camelCased property in
dataset, there is a corresponding cebab-cased data-* attribute in the HTML.
- Values are always strings. You need to convert them manually if you want to store other types.
<button class="toggle-button" data-is-open="false">Show</button>
- This is an example of a simple hide/show toggle button. When clicked, we toggle its state:
const button = ...; // get the button element from the event
const wasOpen = button.dataset.isOpen === 'true'; // convert to boolean
const isOpen = !wasOpen; // toggle
button.dataset.isOpen = String(isOpen); // convert to string
button.textContent = isOpen ? 'Hide' : 'Show';
- Another usecase for the
data-* attributes is to pass data from the server to client-side JS code.
- There are many more properties on nodes and elements that we don't cover here - for example, the element's dimensions, position on the screen, scroll position, etc. Check the MDN documentation for more information.
- JS can't do parallelism ...
- It is single-threaded, meaning only one piece of code can run at a time.
- ... but it can do concurrency.
- It employs event loop to handle multiple asynchronous operations at once.
- There are two queues - tasks and microtasks:
- If the microtasks queue is not empty, the first microtask is executed. Otherwise, the first task is executed.
- Therefore, if a microtask spawns another microtask, it will be executed before the next task.
- Microtasks include promise callbacks and mutation observers (more on that later).
- Tasks include UI events (e.g., clicks and inputs) and timers (e.g.,
setTimeout).
- This whole process is independent on the JS engine - it has to be provided by the environment (i.e., browser, Node.js, etc.).
- In browser, after each task, the rendering engine updates the screen if needed.
- A consequence of this model is that any long-running process will block the event loop, making the page unresponsive - UI won't update and events won't be handled.
-
Never run expensive computations directly in the main thread. Use Web Workers or move the computation to the server instead.
- You can schedule code to run later using
setTimeout (once) or setInterval (repeatedly):
const intervalId = setInterval(() => {
console.log('This runs every 2 seconds');
}, 2000);
setTimeout(() => {
console.log('This runs after 7 seconds');
clearInterval(intervalId); // stop the interval
}, 7000);
// Output:
// "This runs every 2 seconds"
// "This runs every 2 seconds"
// "This runs every 2 seconds"
// "This runs after 7 seconds"
- Both functions take a callback and a delay in milliseconds. The callback will be scheduled as a task after the delay.
- In the callback,
this will be set to window. More on that later, but in general, avoid passing object methods to the timers - wrap them in arrow functions instead:
setTimeout(() => object.method(), 1000);
- You can pass string instead of a function, but do not do that. It turns it into
eval ...
- Both timers return an id. Pass it to
clearTimeout resp. clearInterval to cancel them.
- In general, an event is something that happens and you can respond to it by registering an event listener.
- We will only cover DOM events here. Other environments (e.g., Node.js) have their own event systems.
- Events can be registered via the
EventTarget interface. This is actually a parent for Window, Node (and thus Element and Document), and many others.
const input = document.getElementById('name-input');
// Register an event listener for the 'input' event (fired whenever the content changes).
input.addEventListener('input', event => {
console.log('Input content: ', event.target.value);
});
- Multiple event listeners can be registered for the same event.
- There are other ways to register event listeners (e.g., using
onclick properties or even inline in HTML), but do not use them. They are outdated and have many limitations.
- Un-registering event listeners is tricky, because you need to provide the same (by reference) callback function:
function listener(event) {
console.log('Input content: ', event.target.value);
};
input.addEventListener('input', listener);
// ... some time later ...
input.removeEventListener('input', listener);
- Another option is to use the
signal option with an AbortController:
const controller = new AbortController();
input.addEventListener('input', event => {
console.log('Input content: ', event.target.value);
}, { signal: controller.signal });
// ... some time later ...
controller.abort(); // remove the event listener
- An Event might have a default action associated with it (e.g., clicking on a link navigates to another page). You can prevent it by calling
event.preventDefault():
[ ...document.getElementsByTagName('a') ].forEach(link => {
link.addEventListener('click', event => {
event.preventDefault(); // prevent navigation
});
});
- Note: this is for demonstration purposes only. Do not disable UI elements without a good reason.
- After the event is fired and all listeners have been executed, the event bubbles up to the parent element, firing the same event on it, and so on up to the
document. You can stop this behavior by calling event.stopPropagation():
const outer = document.getElementById('some-id');
const inner = outer.firstElementChild;
outer.addEventListener('click', () => {
console.log('Outer clicked! Do sth ...');
});
inner.addEventListener('click', event => {
event.stopPropagation(); // prevent the outer listener from being called
console.log('Inner clicked! Do sth else ...');
});
- There are many types of events. Some of the most common ones include:
-
click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave, wheel - mouse
-
keydown, keyup, keypress - keyboard
-
input, change, submit, focus - form inputs
-
DOMContentLoaded, load, beforeunload - document lifecycle