JavaScript Notebook

Delve into the world of JavaScript with this extensive guide designed for programmers at all levels. From basic syntax to advanced functionalities, this post explores the core concepts and latest features of JavaScript, providing practical examples and expert tips to enhance your coding skills. Whether you're a beginner looking to understand the basics or an experienced developer aiming to refine your JavaScript techniques, find valuable insights and resources here.

ยท
...

Welcome to my JavaScript notebook! Here, I’ve compiled a variety of tips, tricks, and simple snippets that I’ve gathered over time. These notes come from various sources, including my professional work, problem-solving on LeetCode, and personal projects. My aim is to document these insights for my own reference and to share them with anyone who might find them useful or interesting. Whether you’re a fellow developer or just curious about JavaScript, I hope you’ll find something valuable in these pages.

Array

Creating an array filled with empty arrays:

1let filledWithEmptyArrays = Array(3).fill().map(() => []);

Creating a 2D array (e.g., for a visitation matrix):

1let visited = Array.from({ length: l }, () => Array.from({ length: l }, () => 0));

Creating an array filled with zeros:

1Array(26).fill(0);

Creating an array filled with false:

1Array(n).fill(false);

dp

1let dp = new Array(N).fill(0);

2D array

1const N = 5;
2const M = 5;
3let dp = Array.from({ length: N + 1 }, () => new Array(M + 1).fill(0));

output:

1dp [
2  [ 0, 0, 0, 0, 0, 0 ],
3  [ 0, 0, 0, 0, 0, 0 ],
4  [ 0, 0, 0, 0, 0, 0 ],
5  [ 0, 0, 0, 0, 0, 0 ],
6  [ 0, 0, 0, 0, 0, 0 ],
7  [ 0, 0, 0, 0, 0, 0 ]
8]

visited

0 = Unvisited, 1 = Visiting, 2 = Visited; Or simply use boolean, depends on the use case.

1let visited = new Array(numCourses).fill(0);

adjacency

This approach is explicit about initialising the array with null values before mapping.

1let N = 10
2let adj = new Array(N).fill(null).map(() => []);
3let prerequisites = [[1,0],[0,1]];
4
5prerequisites.forEach(([a, b]) => adj[a].push(b));

Sorting Arrays

Sort an array of numbers in ascending order:

1[11, 2, 22, 1].sort((a, b) => a - b);

Priority Queue (pq)

Min-Heap based on sum of pairs:

1const pq = new PriorityQueue({
2  compare: (a, b) => a.sum - b.sum
3});

Max-Heap based on sum of pairs:

1const pq = new PriorityQueue({
2  compare: (a, b) => b.sum - a.sum
3});

Can enqueue with data set like so:

1pq.enqueue({ sum: 100, i: 0, j: 0 });

MinPriorityQueue

  • This class allows you to manage elements in a min-heap fashion, where the smallest element is always at the top.
  • It’s particularly useful for problems where you need to efficiently access and remove the smallest element repeatedly.

MaxPriorityQueue

  • This class manages elements in a max-heap fashion, where the largest element is always at the top.
  • Use it when you need to access and remove the largest element efficiently.

Example Usage:

Here’s a quick example of using a MinPriorityQueue in a LeetCode problem:

1let heap = new MinPriorityQueue();
2heap.enqueue(10);
3heap.enqueue(5);
4heap.enqueue(20);
5console.log(heap.dequeue().element); // Outputs 5

Note: These classes are specific to the LeetCode environment and are not available in standard JavaScript. You will need to implement your own or use a third-party library if you are working outside LeetCode.

1npm install --save @datastructures-js/priority-queue
1const {
2  PriorityQueue,
3  MinPriorityQueue,
4  MaxPriorityQueue
5} = require('@datastructures-js/priority-queue');

PriorityQueue in this repo is implemented as 3 types:

  • PriorityQueue that accepts a custom comparator between elements.
  • MinPriorityQueue which considers an element with smaller priority number as higher in priority.
  • MaxPriorityQueue which cosiders an element with bigger priority number as higher in priority.

Build From Scratch

Min-Heap Implementation

A min-heap is a binary tree where the parent nodes are smaller than their children. Here’s a basic implementation:

 1class MinHeap {
 2    constructor() {
 3        this.heap = [];
 4    }
 5
 6    getParentIndex(index) {
 7        return Math.floor((index - 1) / 2);
 8    }
 9
10    getLeftChildIndex(index) {
11        return 2 * index + 1;
12    }
13
14    getRightChildIndex(index) {
15        return 2 * index + 2;
16    }
17
18    swap(index1, index2) {
19        [this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]];
20    }
21
22    enqueue(value) {
23        this.heap.push(value);
24        this.heapifyUp();
25    }
26
27    heapifyUp() {
28        let index = this.heap.length - 1;
29        while (index > 0) {
30            const parentIndex = this.getParentIndex(index);
31            if (this.heap[parentIndex] > this.heap[index]) {
32                this.swap(parentIndex, index);
33                index = parentIndex;
34            } else {
35                break;
36            }
37        }
38    }
39
40    dequeue() {
41        if (this.heap.length === 0) return null;
42        if (this.heap.length === 1) return this.heap.pop();
43        const minValue = this.heap[0];
44        this.heap[0] = this.heap.pop();
45        this.heapifyDown(0);
46        return minValue;
47    }
48
49    heapifyDown(index) {
50        let smallest = index;
51        const leftChild = this.getLeftChildIndex(index);
52        const rightChild = this.getRightChildIndex(index);
53
54        if (leftChild < this.heap.length && this.heap[leftChild] < this.heap[smallest]) {
55            smallest = leftChild;
56        }
57        if (rightChild < this.heap.length && this.heap[rightChild] < this.heap[smallest]) {
58            smallest = rightChild;
59        }
60        if (smallest !== index) {
61            this.swap(index, smallest);
62            this.heapifyDown(smallest);
63        }
64    }
65
66    size() {
67        return this.heap.length;
68    }
69}

Max-Heap Implementation

A max-heap is similar to a min-heap, but the parent nodes are larger than their children.

 1class MaxHeap {
 2    constructor() {
 3        this.heap = [];
 4    }
 5
 6    getParentIndex(index) {
 7        return Math.floor((index - 1) / 2);
 8    }
 9
10    getLeftChildIndex(index) {
11        return 2 * index + 1;
12    }
13
14    getRightChildIndex(index) {
15        return 2 * index + 2;
16    }
17
18    swap(index1, index2) {
19        [this.heap[index1], this.heap[index2]] = [this.heap[index2], this.heap[index1]];
20    }
21
22    enqueue(value) {
23        this.heap.push(value);
24        this.heapifyUp();
25    }
26
27    heapifyUp() {
28        let index = this.heap.length - 1;
29        while (index > 0) {
30            const parentIndex = this.getParentIndex(index);
31            if (this.heap[parentIndex] < this.heap[index]) { // Max-Heap condition
32                this.swap(parentIndex, index);
33                index = parentIndex;
34            } else {
35                break;
36            }
37        }
38    }
39
40    dequeue() {
41        if (this.heap.length === 0) return null;
42        if (this.heap.length === 1) return this.heap.pop();
43        const maxValue = this.heap[0];
44        this.heap[0] = this.heap.pop();
45        this.heapifyDown(0);
46        return maxValue;
47    }
48
49    heapifyDown(index) {
50        let largest = index;
51        const leftChild = this.getLeftChildIndex(index);
52        const rightChild = this.getRightChildIndex(index);
53
54        if (leftChild < this.heap.length && this.heap[leftChild] > this.heap[largest]) { // Max-Heap condition
55            largest = leftChild;
56        }
57        if (rightChild < this.heap.length && this.heap[rightChild] > this.heap[largest]) { // Max-Heap condition
58            largest = rightChild;
59        }
60        if (largest !== index) {
61            this.swap(index, largest);
62            this.heapifyDown(largest);
63        }
64    }
65
66    size() {
67        return this.heap.length;
68    }
69}

Key Points to Remember:

  • Min-Heap: Always keeps the smallest element at the top.
  • Max-Heap: Always keeps the largest element at the top.
  • Time Complexity: Insertion and deletion operations both take $O(\log n)$ time, where $n$ is the number of elements in the heap.
  • Use Cases: Min-heaps are useful for problems requiring efficient access to the minimum element (e.g., Dijkstra’s algorithm), while max-heaps are used when you need quick access to the maximum element (e.g., priority scheduling).

LeetCode Imported Tips

When working on LeetCode problems, you might come across built-in utility classes like MinPriorityQueue and MaxPriorityQueue. These classes are provided by the LeetCode environment to simplify the process of implementing priority queues, which are essentially min-heaps and max-heaps.

1JavaScript	node.js 20.10.0
2Your code is run with --harmony flag, enabling new ES6 features.
3
4lodash.js library is included by default.
5
6For Priority Queue / Queue data structures, you may use 5.4.0 version of datastructures-js/priority-queue and 4.2.3 version of datastructures-js/queue.

Working with JSON

Convert an object or array to a JSON string:

1JSON.stringify(obj);

map - set()

Initialise a map with key-value pairs:

1const myMap = new Map([
2  ['key1', 'value1'],
3  ['key2', 'value2'],
4  ['key3', 'value3']
5]);

Convert map values to an array:

1const valuesArray = Array.from(myMap.values());

set - add()


Math

Find the maximum value in a list of numbers:

1Math.max(...list);

Round a number to at most 2 decimal places:

1Math.round(num * 100) / 100;

To ensure numbers like 1.005 round correctly:

1Math.round((num + Number.EPSILON) * 100) / 100;

CSS: Adding Styles to :before and :after

Dynamically add CSS styles to :before and :after selectors:

1var styleElem = document.head.appendChild(document.createElement("style"));
2styleElem.innerHTML = ".add-fading-" + nth + ":before { z-index: 1 !important; }";

NPM Packages

md-file-tree by @michalbe

Generate a markdown tree of all files in a directory, recursively:

1## Display in console
2md-file-tree
3
4## Output to a file such as README.md
5md-file-tree > README.md

JavaScript Obfuscator

Obfuscate JavaScript code to make it harder to understand:

 1var fs = require("fs");
 2var jsObfuscator = require("js-obfuscator");
 3
 4fs.readFile("./000_unencrypted-js/main.js", "UTF-8", function (error, code) {
 5    if (error) throw error;
 6    var obfuscatedResult = jsObfuscator.obfuscate(code);
 7    fs.writeFile("./js/main.js", obfuscatedResult.getObfuscatedCode(), function (fsError) {
 8        if (fsError) console.log(fsError);
 9        console.log("Obfuscated.");
10    });
11});

TypeScript: Child Manager Configuration

Example configuration for managing multiple processes:

 1{
 2  "processes": [
 3    {
 4      "name": "Frontend",
 5      "command": {
 6        "executor": "yarn",
 7        "args": [
 8          "start"
 9        ],
10        "path": "../fe-project",
11        "isWindows": false
12      },
13      "maxLogs": 200
14    },
15    {
16      "name": "WebSocket",
17      "command": {
18        "executor": "yarn",
19        "args": [
20          "start"
21        ],
22        "path": "../server-project",
23        "isWindows": false
24      },
25      "maxLogs": 300
26    },
27    {
28      "name": "Docker",
29      "command": {
30        "executor": "docker-compose",
31        "args": [
32          "up"
33        ],
34        "path": "../be-project",
35        "isWindows": false
36      },
37      "maxLogs": 300
38    }
39  ],
40  "captureExit": true,
41  "longLive": false,
42  "debug": false
43}

HTML: onkeydown

To put on <form> to prevent user clicks on enter.

1onkeydown="return event.key != 'Enter';"

Spread operator

1const obj = { name: "Jing Hui", age: 24 }
2
3console.log(obj)
4
5const test = {...obj, age: 25, nationality: "malaysian"}

Destructuring assignments

You can use them in different contexts, such as swapping values, extracting properties from objects or arrays, and working with functions:

Swapping Two Variables

This is the most common use case: swapping two variables without using a temporary variable.

1let a = 5;
2let b = 10;
3
4// Swap a and b
5[a, b] = [b, a];
6
7console.log(a); // Output: 10
8console.log(b); // Output: 5

got this from Rotate Array (todo add link)

Extracting Values from Arrays

You can use destructuring to assign array elements to variables.

 1const numbers = [1, 2, 3, 4];
 2
 3// Destructure first two elements
 4const [first, second] = numbers;
 5
 6console.log(first);  // Output: 1
 7console.log(second); // Output: 2
 8
 9// Skip elements
10const [, , third] = numbers;
11
12console.log(third);  // Output: 3

Default Values in Array Destructuring

If the array is shorter than expected, you can provide default values.

1const colors = ['red'];
2
3// Destructure with a default value
4const [primaryColor, secondaryColor = 'blue'] = colors;
5
6console.log(primaryColor);  // Output: 'red'
7console.log(secondaryColor); // Output: 'blue' (default value)

Object Destructuring

You can destructure objects to extract specific properties into variables.

 1const person = {
 2  name: 'Alice',
 3  age: 30,
 4  job: 'Engineer'
 5};
 6
 7// Destructure object properties
 8const { name, age } = person;
 9
10console.log(name); // Output: 'Alice'
11console.log(age);  // Output: 30

Object Destructuring with Aliases

You can also rename the extracted properties by assigning them to new variable names.

 1const employee = {
 2  firstName: 'John',
 3  lastName: 'Doe'
 4};
 5
 6// Destructure with alias
 7const { firstName: fName, lastName: lName } = employee;
 8
 9console.log(fName); // Output: 'John'
10console.log(lName);  // Output: 'Doe'

Nested Object Destructuring

Destructuring can also be used for nested objects.

 1const student = {
 2  info: {
 3    name: 'Mark',
 4    grade: 'A'
 5  },
 6  courses: ['Math', 'Physics']
 7};
 8
 9// Destructure nested properties
10const { info: { name }, courses: [firstCourse] } = student;
11
12console.log(name);        // Output: 'Mark'
13console.log(firstCourse); // Output: 'Math'

Function Parameter Destructuring

You can destructure objects or arrays passed as function parameters.

 1// Destructure object in function parameters
 2function printStudent({ name, age }) {
 3  console.log(`${name} is ${age} years old.`);
 4}
 5
 6const student = { name: 'Emily', age: 25 };
 7printStudent(student); // Output: 'Emily is 25 years old.'
 8
 9// Destructure array in function parameters
10function sum([a, b]) {
11  return a + b;
12}
13
14console.log(sum([3, 7])); // Output: 10

Rest Operator in Destructuring (Spread Operator)

You can use the rest (...) operator to capture the remaining elements after destructuring.

1const fruits = ['apple', 'banana', 'cherry', 'date'];
2
3// Destructure first element and capture the rest
4const [firstFruit, ...otherFruits] = fruits;
5
6console.log(firstFruit);   // Output: 'apple'
7console.log(otherFruits);  // Output: ['banana', 'cherry', 'date']

Ignoring Values in Destructuring

You can ignore certain elements when destructuring arrays.

1const scores = [100, 90, 85, 80];
2
3// Ignore the second score
4const [firstScore, , thirdScore] = scores;
5
6console.log(firstScore); // Output: 100
7console.log(thirdScore); // Output: 85

Combining Arrays and Destructuring

Destructuring works seamlessly when combining multiple arrays.

 1const arr1 = [1, 2, 3];
 2const arr2 = [4, 5, 6];
 3
 4// Combine and destructure
 5const combined = [...arr1, ...arr2];
 6const [first, second, ...rest] = combined;
 7
 8console.log(first);  // Output: 1
 9console.log(second); // Output: 2
10console.log(rest);   // Output: [3, 4, 5, 6]

TODO: Rest Operator (...)

The rest operator is used to gather multiple elements into a single array or object. You usually see it in function parameters or destructuring.

Example 1: Rest in Function Parameters

In functions, the rest operator gathers all the extra arguments passed to the function into an array.

1function showNumbers(...numbers) {
2  console.log(numbers); // Gathers arguments into an array
3}
4
5showNumbers(1, 2, 3, 4, 5); // Output: [1, 2, 3, 4, 5]

Breakdown:

  • The ...numbers gathers all the arguments (1, 2, 3, 4, 5) into a single array named numbers.

Example 2: Rest in Array Destructuring

In destructuring, the rest operator gathers remaining elements of an array into a new array.

1const [first, second, ...rest] = [10, 20, 30, 40, 50];
2console.log(first);   // Output: 10
3console.log(second);  // Output: 20
4console.log(rest);    // Output: [30, 40, 50]

Breakdown:

  • first gets the value 10, second gets 20, and the rest of the values (30, 40, 50) are collected into the rest array.

Example 3: Rest in Object Destructuring

You can also use the rest operator to gather remaining properties of an object.

1const person = { name: 'Alice', age: 25, country: 'USA' };
2const { name, ...details } = person;
3console.log(name);     // Output: Alice
4console.log(details);  // Output: { age: 25, country: 'USA' }

Breakdown:

  • name gets Alice, and the remaining properties (age, country) are collected into the details object.

Spread Operator (...)

The spread operator is used to spread out the elements of an array, object, or iterable into individual elements.

Example 1: Spread in Arrays

Spread operator expands the elements of an array. This is often used to combine arrays.

1const arr1 = [1, 2];
2const arr2 = [3, 4];
3const combined = [...arr1, ...arr2];
4console.log(combined); // Output: [1, 2, 3, 4]

Breakdown:

  • ...arr1 spreads out [1, 2], and ...arr2 spreads out [3, 4], combining them into one array.

Example 2: Spread in Function Calls

You can pass the elements of an array as individual arguments to a function using the spread operator.

1const numbers = [5, 6, 7];
2console.log(Math.max(...numbers)); // Output: 7

Breakdown:

  • ...numbers spreads the array [5, 6, 7] into Math.max as individual arguments (5, 6, 7).

Example 3: Spread in Objects

The spread operator can copy properties from one object to another.

1const obj1 = { a: 1, b: 2 };
2const obj2 = { c: 3 };
3const merged = { ...obj1, ...obj2 };
4console.log(merged); // Output: { a: 1, b: 2, c: 3 }

Breakdown:

  • ...obj1 spreads the properties of obj1 into a new object, followed by ...obj2 spreading the properties of obj2.

Summary of Differences

FeatureRest Operator (...)Spread Operator (...)
PurposeGathers elements into an array/objectSpreads elements into individual items
UsageFunction parameters, destructuringFunction calls, array/object merging
Examplefunction(...args) {}Math.max(...[1, 2, 3])

Final Thoughts

  • Rest collects multiple elements into an array or object.

    • Example: Collect remaining arguments in a function.
  • Spread takes an array or object and breaks it into individual elements.

    • Example: Pass an array as arguments to a function or merge arrays/objects.

๐Ÿ› Common Errors

Array Referencing Issue

The method that causes all elements to reference the same array:

1let adj = new Array(N).fill([]);
  1. When using fill with objects (including arrays), it copies the reference to the object.
  2. For primitive types (like numbers or strings), it copies the value.
  3. Avoiding Common Pitfalls: Don’t use fill with mutable objects if you need distinct instances.

โœ… The Correct Way!

This approach is more concise and directly initialises the array elements:

1let adj = new Array(N).fill(null).map(() => []);
2// OR
3let adj = Array.from({ length: N }, () => []);

TODO

comments powered by Disqus