Skip to content

jisung-lee7/play-javascript

Repository files navigation

play-javascript

My personal playground for javascript coding and learning.

📌 Summary

Table of contents

🏷️ Variable

  • A variable is a named reference to a value. That way an unpredictable value can be accessed through a predetermined name.

  • 'var' - don't ever use this
    • 'var' has no block scope, it has function-scope
      function test() {
          if (true) {
              var x = 10 
          }
          console.log(x) // Expected output: 10
      }
      
      test()

(added in ES6)

  • 'let' - rw(read/write)
    • Only use 'let' if variable needs to change.

  • 'constant' - r(read only)
    • Use const whenever possible.

  • Favor immutable data type always for a few reasons:
    • Security
    • Thread safety
    • Reduce human mistakes

Hoisting

  • Move declaration from bottom to top anywhere within the scope(Only declaration, NOT assignments)
  • 'let' and 'const' are also hoisted, but accessing them before initialization will cause a ReferenceError due to the TDZ (Temporal Dead Zone).
  • They cannot be accessed before initialization.

The process of variable creation

  • var(function-scoped)
    1. Declaration and initialization(=assigning 'undefined')
    2. Assignment

  • let(block-scoped)
    1. Declaration(hoisted)
    2. Initialization(when the code is reached)
    3. Assignment

  • constant(block-scoped)
    1. Declaration and Initialization and Assignment.

Data types

  • Immutable(Single item)
    • primitive types(number, string, boolean, null, undefined, symbol)
    • The memory value cannot be modified.

    • number
      • integer, decimal number
      • special numeric values(infinity, -infinity, NaN)

    • string
      • In any computer programming language, a string is a sequence of characters used to represent text.

    • boolean
      • false: 0, null, undefined, NaN, ''
      • true: any other value

    • null
      • If you can check null using typeof, it returns object.
      • This is a bug that type is object in the early version of JavaScript. It is not fixed to maintain backward compatibility.
        console.log(typeof null) // expected output: object

    • undefined
      • Automatically assigned to variables that have just been declared or to formal arguments for which there are no actual arguments.

    • symbol
      • create unique identifiers for objects.
      • Do not use 'new'.

  • Mutable(Box container by building single items)
    • all objects(Object, Array, Function, RegexExp) by default are mutable in JS
    • The memory value can be modified.

Type conversion(Type Casting)

  • Implicit type conversion
    • Automatic type conversion by the JavaScript engine.

  • Explicit type conversion
    • manual type conversion using internal function by programmer

🏷️ Operator

Assignment operator

  • The assignment (=) operator is used to assign a value to a variable or property.
let x = 1

Numeric operators

console.log(1 + 1)  // add
console.log(1 - 1)  // subtract
console.log(1 / 1)  // divide
console.log(1 * 1)  // multiply
console.log(1 % 1)  // remainder
console.log(1 ** 1) // exponentiation, power

Compound assignment operators

+=, -=, *=, /=

Increment and decrement operators

++, --

Logical operators: || (or), && (and), ! (not)

|| (or)

  • leftExpr || rightExpr
    • That means finds the first truthy value

&& (and)

  • leftExpr && rightExpr
    • That means finds the first falsy value

Optional chaining - new(ES11)

  • The optional chaining (?.) operator accesses an object's property or calls a function.
  • If the object accessed or function called using this operator is undefined or null, the expression short circuits and evaluates to undefined instead of throwing an error.
const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};

const dogName = adventurer.dog?.name;
console.log(dogName); // Expected output: undefined

console.log(adventurer.someNonExistentMethod?.()); // Expected output: undefined

Nullish coalescing operator - new(ES11)

  • leftExpr ?? rightExpr
    • When leftExpr === null or undefined, it returns rightExpr
    • When leftExpr !== null or undefined, it returns leftExpr
    • When leftExpr and rightExpr !== null or undefined, it returns leftExpr

Short-circuit evaluation

  • We can use logical operators for short-circuit evaluation.
    • As logical expressions are evaluated left to right, they are tested for possible "short-circuit" evaluation using the following rules:
      • truthy || anything is short-circuit evaluated to the truthy value.
      • falsy && anything is short-circuit evaluated to the falsy value.
      • nonNullish ?? anything is short-circuit evaluated to the non-nullish value.

Ternary operator: ?

  • condition ? value1 : value2
const isCat = true
const component = isCat ? 'cat' : 'dog'

console.log(component)

Comparison operators

console.log('10 <  7', 10 <  7) // less than
console.log('10 <= 7', 10 <= 7) // less than or equal
console.log('10 >  7', 10 >  7) // greater than
console.log('10 >= 7', 10 >= 7) // greater than or equal

Equality

==  // loose equality, does not check type
=== // strict equality, checks type

🏷️ Function

  • Javascript supports first-class function

  • First-class function
    • Functions are treated like any other variable
    • Can be assigned as a value to variable
    • Can be passed as an argument to other functions
    • Can be returned by another function

Parameters

  • Primitive parameters : passed by value
  • Object parameters : passed by reference

Function

  • Fundamental building block in the program
  • Subprogram can be used multiple times
  • Performs a task or calculates a value

Function Declaration

  • Function declarations can be called from anywhere. The reason is due to hoisting
  • Function declaration
    • function name(param1, param2) { body... return; }
    • one function === one thing e.g., createCardAndPoint -> createCard, createPoint
    • function is object in JS

Function expression

  • Anonymous function, Arrow function

What is diff Function declaration vs Function expression ?
  • A function declaration can be called earlier than it is defined. (hoisted)
  • A function expression is created when the execution reaches it.

🏷️ Scope

  • The scope is the current context of execution in which values and expressions are "visible" or can be referenced.
  • If a variable or expression is not in the current scope, it will not be available for use.
  • Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.
  • JavaScript has the following kinds of scopes:
    • Global scope
      • The default scope for all code running in script mode.
      • Variables and functions declared in the global scope are accessible throughout the entire script.
    • Module scope
      • The scope for code running in module mode (ES6 modules).
      • Variables and functions declared in a module are only accessible within that module unless explicitly exported.
    • Function scope
      • The scope created with a function.
      • Variables declared within a function are accessible only within that function.

  • In addition, identifiers declared with certain syntaxes, including let, const, class, or (in strict mode) function, can belong to an additional scope:
    • Block scope
      • The scope created with a pair of curly braces ({}).
      • Variables declared with let and const are limited to the block in which they are defined.

🏷️ Object

  • One of the JavaScript's data types.
  • A collection of related data and/or functionality.
  • Nearly all objects in JavaScript are instances of Object
const obj = { key: value }

  • How to declare object?
const objectLiteral = {} // 'object literal' syntax
const objectConstructor = new Object() // 'object constructor' syntax

Object property

  • A JavaScript property is a member of an object that associates a key with a value.
const obj = {
  a: 1,
  b() {}
}

  • JavaScript, as a dynamically typed language, allows you to add properties later and also delete it later.
const objTest = { name: 'jisung', age: 20 }

objTest.hasJob = true
console.log(objTest.hasJob) // Expected output: true

delete objTest.hasJob
console.log(objTest.hasJob) // Expected output: undefined

Computed property

const objTest = { name: 'jisung', age: 20 }

console.log(objTest.name)    // Expected output: jisung
console.log(objTest['name']) // Expected output: jisung
console.log(objTest['age'])  // Expected output: 20

objTest['hasJob'] = true
console.log(objTest.hasJob) // Expected output: true

function printValue(objParam, keyParam) {
  console.log(objParam) // Expected output: { name: 'jisung', age: 20, hasJob: true }
  console.log(objParam.keyParam)  // Expected output: undefined, the reason is objParam doesn't have literally key: keyParam
  console.log(objParam[keyParam]) // Expected output: 'jisung', it is computed properties
  }

printValue(objTest, 'name')

In operator: property existence check (key in obj)

const obj = { name: 'jisung', age: 20 }

console.log('name' in obj)   // Expected output: true
console.log('random' in obj) // Expected output: false

🏷️ Array

  • An array is an ordered collection of data (either primitive or object depending upon the language).
  • Arrays are used to store multiple values under a single variable name.
  • A regular variable, on the other hand, can store only one value.
  • Each item in an array has a number attached to it, called a numeric index, that allows you to access it.
  • In JavaScript, arrays start at index zero and can be manipulated with various methods.
  • How to declare array?
const arr1 = [1, 2] // 'array literal' syntax
const arr2 = new Array() // 'array constructor' syntax
const arr3 = [ 
  // An array can include string, number, object, function
  'Jisung',
  7,
  true,
  {
    name: 'Lee',
    age: '30'
  },
  function () {
    console.log(`TEST`)
  }
]

Array.prototype

forEach()

  • The forEach() method of Array instances executes a provided function once for each array element.

Syntax
forEach(callbackFn)
forEach(callbackFn, thisArg)

Parameters
  • callbackFn
    • A function to execute for each element in the array.
    • Its return value is discarded.
    • The function is called with the following arguments:
    • element
      • The current element being processed in the array.
    • index
      • The index of the current element being processed in the array.
    • array
      • The array forEach() was called upon.

  • thisArg (Optional)
    • A value to use as this when executing callbackFn.

Return value
  • None (undefined).

map()

  • The map() method of Array instances creates a new array populated with the results of calling a provided function on every element in the calling array.

Syntax
map(callbackFn)
map(callbackFn, thisArg)

Parameters
  • callbackFn
    • A function to execute for each element in the array.
    • Its return value is added as a single element in the new array.
    • The function is called with the following arguments:
    • element
      • The current element being processed in the array.
    • index
      • The index of the current element being processed in the array.
    • array
      • The array map() was called upon.

  • thisArg (Optional)
    • A value to use as this when executing callbackFn.

Return value
  • A new array with each element being the result of the callback function.

reduce()

  • The reduce() method of Array instances executes a user-supplied "reducer" callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element.
  • The final result of running the reducer across all elements of the array is a single value.

Syntax
reduce(callbackFn)
reduce(callbackFn, initialValue)

Parameters
  • callbackFn
    • A function to execute for each element in the array.
    • Its return value becomes the value of the accumulator parameter on the next invocation of callbackFn.
    • For the last invocation, the return value becomes the return value of reduce().
    • The function is called with the following arguments:
    • accumulator
      • The value resulting from the previous call to callbackFn.
      • On the first call, its value is initialValue if the latter is specified; otherwise its value is array[0].
    • currentValue
      • The value of the current element. On the first call, its value is array[0] if initialValue is specified; otherwise its value is array[1].
    • currentIndex
      • The index position of currentValue in the array. On the first call, its value is 0 if initialValue is specified, otherwise 1.
    • array
      • The array reduce() was called upon.



  • initialValue (Optional)
    • A value to which accumulator is initialized the first time the callback is called.
    • If initialValue is specified, callbackFn starts executing with the first value in the array as currentValue.
    • If initialValue is not specified, accumulator is initialized to the first value in the array, and callbackFn starts executing with the second value in the array as currentValue.
    • In this case, if the array is empty (so that there's no first value to return as accumulator), an error is thrown.

Return value
  • The value that results from running the "reducer" callback function to completion over the entire array.

🏷️ Truthy and Falsy

Truthy

  • In JavaScript, a truthy value is a value that is considered true when encountered in a Boolean context.
  • All values are truthy unless they are defined as falsy.

Falsy

  • A falsy (sometimes written falsey) value is a value that is considered false when encountered in a Boolean context.
if (null || undefined || false || NaN || 0 || -0 || 0n || '') {
  console.log('Falsy')
}

When should we use it?

function testFunc(obj) {
  // if (obj === undefined || obj === null) {
  if (!obj) { // falsy
    console.log('There is no obejct')
    return
  }
  console.log(person.name)
}

const person = null
testFunc(person)

🏷️ Destructuring assignment

  • The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables.

Destructuring Array

{
  // basic destructuring
  const array = [1, 2]
  const [a, b, c] = array
  console.log(a) // Expected output: 1
  console.log(b) // Expected output: 2
  console.log(c) // Expected output: undefined
}

{
  // skipping elements
  const array = [1, 2, 3]
  const [a, , b, c] = array
  console.log(a) // Expected output: 1
  console.log(b) // Expected output: 3
  console.log(c) // Expected output: undefined
}

{
  // default values
  const array = [undefined, 2]
  const [a = 10, b, c = 20] = array
  console.log(a) // Expected output: 10 (use default)
  console.log(b) // Expected output: 2
  console.log(c) // Expected output: 20
}

{
  // rest syntax
  const array = [1, 2, 3, 4, 5]
  const [a, b, ...rest] = array
  console.log(a) // Expected output: 1
  console.log(b) // Expected output: 2
  console.log(rest) // Expected output: [3, 4, 5]
}

{
  // rest syntax with specific positions
  const array = [1, 2, 3, 4, 5]
  const [a, , b, ...rest] = array
  console.log(a) // Expected output: 1
  console.log(b) // Expected output: 3
  console.log(rest) // Expected output: [4, 5]
}

Destructuring Object

{
  // basic destructuring
  const obj = { a: 1, b: 2 }
  const { a, b, c } = obj
  console.log(a) // Expected output: 1
  console.log(b) // Expected output: 2
  console.log(c) // Expected output: undefined
}

{
  // renaming variables
  const obj = { a: 1, b: 2 }
  const { a: a1, b: b1, c: c1 } = obj
  console.log(a1) // Expected output: 1
  console.log(b1) // Expected output: 2
  console.log(c1) // Expected output: undefined
}

{
  // default values
  const obj = { a: undefined, b: 2 }
  const aDefault = 10
  const bDefault = 20
  const { a: a1 = aDefault, b = bDefault, c = 30 } = obj
  console.log(a1) // Expected output: 10 (use default value)
  console.log(b) // Expected output: 2
  console.log(c) // Expected output: 30
}

{
  // rest syntax
  const obj = { a: 1, b: 2, c: 3, d: 4 }
  const { a, b, ...rest } = obj
  console.log(a) // Expected output: 1
  console.log(b) // Expected output: 2
  console.log(rest) // Expected output: { c: 3, d: 4 }
}

{
  // rest syntax with renaming
  const obj = { a: 1, b: 2, c: 3, d: 4 }
  const { a: a1, b: b1, ...rest } = obj
  console.log(a1) // Expected output: 1
  console.log(b1) // Expected output: 2
  console.log(rest) // Expected output: { c: 3, d: 4 }
}

{
  // computed property names
  const obj = { a: 1, b: 2, c: 3 }
  const key = 'a'
  const { [key]: a } = obj
  console.log(a) // Expected output: 1
}

🏷️ Spread syntax and Rest argument

Spread syntax

  • The spread (...) syntax allows an iterable, such as an array or string, to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected.
  • In an object literal, the spread syntax enumerates the properties of an object and adds the key-value pairs to the object being created.

{
  // using spread syntax in array
  const arr1 = [1, 2, 3]
  const arr2 = [...arr1, 4, 5, 6]

  console.log(arr2) // Expected output: [1, 2, 3, 4, 5, 6]
}

{
  // using spread syntax in string
  const str = 'Hello'
  const characters = [...str]

  console.log(characters) // Expected output: ['H', 'e', 'l', 'l', 'o']
}

{
  // using spread syntax in object
  const obj1 = { a: 1, b: 2 }
  const obj2 = { ...obj1, c: 3 }

  console.log(obj2) // Expected output: { a: 1, b: 2, c: 3 }
}

{
  // using spread syntax in object merging
  // if keys are the same, they will be overwritten.
  const obj1 = { a: 1, b: 2 }
  const obj2 = { d: 3, e: 4 }
  const obj3 = { ...obj1, ...obj2 }

  console.log(obj3) // Expected output: { a: 1, b: 2, d: 3, e: 4 }
}

{
  // using spread syntax in function calls
  const arr = [1, 2]

  function test(one, two, three) {
    console.log(one, two, three)
  }

  test(...arr) // Expected output: 1, 2, undefined
}

Rest parameters

  • The rest parameter syntax allows a function to accept an indefinite number of arguments as an array, providing a way to represent variadic functions in JavaScript.

{
  const arr1 = [1, 2, 3]
  const arr2 = [4, 5, 6]

  function test(ONE, TWO, ...rest) { // using rest parameters in function
    console.log(ONE) // Expected output: one
    console.log(TWO) // Expected output: two
    console.log(rest) // Expected output: [1, 2, 3, 4, 5, 6]
  }

  test('one', 'two', ...arr1, ...arr2) // spread syntax
}

🏷️ Shallow copy and Deep copy

Shallow copy

  • A shallow copy of an object is a copy whose properties share the same references (point to the same underlying values) as those of the source object from which the copy was made.
  • As a result, when you change either the source or the copy, you may also cause the other object to change too.
  • That behavior contrasts with the behavior of a deep copy, in which the source and copy are completely independent.
  • The copy of an object whose properties all have primitive values fits the definition of both a deep copy and a shallow copy.
  • For shallow copies, only the top-level properties are copied, not the values of nested objects.
    • Therefore:
      • Re-assigning top-level properties of the copy does not affect the source object.
      • Re-assigning nested object properties of the copy does affect the source object.
  • In JavaScript, standard built-in object-copy operations (spread syntax, Array.prototype.concat(), Array.prototype.slice(), Array.from(), and Object.assign()) do not create deep copies (instead, they create shallow copies).

Deep copy

  • A deep copy of an object is a copy whose properties do not share the same references (point to the same underlying values) as those of the source object from which the copy was made.
  • As a result, when you change either the source or the copy, you can be assured you're not causing the other object to change too.
  • That behavior contrasts with the behavior of a shallow copy, in which changes to nested properties in the source or the copy may cause the other object to change too.

Shallow copy of Array

{
  // same memory - array
  const arr1 = [1, 2]
  const arr2 = arr1

  // arr1 ──► (0x0000) [1, 2]
  // arr2 ─┘

  console.log(arr1 === arr2) // Expected output: true

  console.log(arr1) // Expected output: [1, 2]
  console.log(arr2) // Expected output: [1, 2]

  arr1[0] = 7

  console.log(arr1) // Expected output: [7, 2]
  console.log(arr2) // Expected output: [7, 2]
}

{
  // shallow copy - using spread syntax in array
  const arr1 = [1, 2]
  const arr2 = [...arr1]

  // arr1 ──► (0x0000) [1, 2]
  // arr2 ──► (0x0004) [1, 2]

  console.log(arr1 === arr2) // Expected output: false

  console.log(arr1) // Expected output: [1, 2]
  console.log(arr2) // Expected output: [1, 2]

  arr1[0] = 7

  console.log(arr1) // Expected output: [7, 2]
  console.log(arr2) // Expected output: [1, 2]
}

{
  // shallow copy - using spread syntax with nested arrays
  const arr1 = [1, 2, [3, 4]]
  const arr2 = [...arr1]

  // arr1 ──► (0x0000) [1, 2, ┬  ]
  //                          |─────► (0x0008) [3, 4]
  // arr2 ──► (0x0004) [1, 2, ┴  ]

  console.log(arr1 === arr2) // Expected output: false

  console.log(arr1) // Expected output: [1, 2, [3, 4]]
  console.log(arr2) // Expected output: [1, 2, [3, 4]]

  arr1[0] = 7
  arr1[2][0] = 9

  console.log(arr1) // Expected output: [7, 2, [9, 4]]
  console.log(arr2) // Expected output: [1, 2, [9, 4]]
}

{
  // same memory - nested arrays
  const arr1 = [1, 2, [3, 4]]
  const arr2 = arr1

  // arr1 ──► (0x0000) [1, 2, ┬ ]
  // arr2 ─┘                  ┴────► (0x0004) [3, 4]

  console.log(arr1 === arr2) // Expected output: true

  console.log(arr1) // Expected output: [1, 2, [3, 4]]
  console.log(arr2) // Expected output: [1, 2, [3, 4]]

  arr1[0] = 7
  arr1[2][0] = 9

  console.log(arr1) // Expected output: [7, 2, [9, 4]]
  console.log(arr2) // Expected output: [7, 2, [9, 4]]
}

{
  // same memory - object in array
  const arr1 = [1, 2, { key1: 'value1' }]
  const arr2 = arr1

  // arr1 ──► (0x0000) [1, 2, ┬ ]
  // arr2 ─┘                  ┴────► (0x0004) { key1: 'value1' }

  console.log(arr1 === arr2) // Expected output: true

  console.log(arr1) // Expected output: [ 1, 2, { key1: 'value1' } ]
  console.log(arr2) // Expected output: [ 1, 2, { key1: 'value1' } ]

  arr1[0] = 7
  arr1[2].key1 = 'value7'

  console.log(arr1) // Expected output: [ 7, 2, { key1: 'value7' } ]
  console.log(arr2) // Expected output: [ 7, 2, { key1: 'value7' } ]
}

{
  // shallow copy - using spread syntax to object in array
  const arr1 = [1, 2, { key1: 'value1' }]
  const arr2 = [...arr1]

  // arr1 ──► (0x0000) [1, 2, ┬  ]
  //                          |─────► (0x0008) { key1: 'value1' }
  // arr2 ──► (0x0004) [1, 2, ┴  ]

  console.log(arr1 === arr2) // Expected output: false

  console.log(arr1) // Expected output: [ 1, 2, { key1: 'value1' } ]
  console.log(arr2) // Expected output: [ 1, 2, { key1: 'value1' } ]

  arr1[0] = 7
  arr1[2].key1 = 'value7'

  console.log(arr1) // Expected output: [ 7, 2, { key1: 'value7' } ]
  console.log(arr2) // Expected output: [ 1, 2, { key1: 'value7' } ]
}

Shallow copy of Object

{
  // same memory - object
  const obj1 = { key1: 'value1' }
  const obj2 = obj1

  // obj1 ──► (0x0000) { key1: 'value1' }
  // obj2 ─┘

  console.log(obj1 === obj2) // Expected output: true

  console.log(obj1) // Expected output: { key1: 'value1' }
  console.log(obj2) // Expected output: { key1: 'value1' }

  obj1.key1 = 'value7'

  console.log(obj1) // Expected output: { key1: 'value7' }
  console.log(obj2) // Expected output: { key1: 'value7' }
}

{
  // shallow copy - using spread syntax in object
  const obj1 = { key1: 'value1', key2: 'value2' }
  const obj2 = { ...obj1 }

  // obj1 ──► (0x0000) { key1: 'value1', key2: 'value2' }
  // obj2 ──► (0x0004) { key1: 'value1', key2: 'value2' }

  console.log(obj1 === obj2) // Expected output: false

  console.log(obj1) // Expected output: { key1: 'value1', key2: 'value2' }
  console.log(obj2) // Expected output: { key1: 'value1', key2: 'value2' }

  obj1.key1 = 'value7'

  console.log(obj1) // Expected output: { key1: 'value7', key2: 'value2' }
  console.log(obj2) // Expected output: { key1: 'value1', key2: 'value2' }
}

{
  // shallow copy - using spread syntax with nested objects
  const obj1 = { key1: 'value1', key2: { key3: 'value3', key4: 'value4' } }
  const obj2 = { ...obj1 }

  // obj1 ──► (0x0000) { key1: 'value1', key2: ┬ }
  //                                           |─────► (0x0008) { key3: 'value3', key4: 'value4' }
  // obj2 ──► (0x0004) { key1: 'value1', key2: ┴ }

  console.log(obj1 === obj2) // Expected output: false

  console.log(obj1) // Expected output: { key1: 'value1', key2: { key3: 'value3', key4: 'value4' } }
  console.log(obj2) // Expected output: { key1: 'value1', key2: { key3: 'value3', key4: 'value4' } }

  obj1.key1 = 'value7'
  obj1.key2.key3 = 'value9'

  console.log(obj1) // Expected output: { key1: 'value7', key2: { key3: 'value9', key4: 'value4' } }
  console.log(obj2) // Expected output: { key1: 'value1', key2: { key3: 'value9', key4: 'value4' } }
}

{
  // same memory - nested objects
  const obj1 = { key1: 'value1', key2: { key3: 'value3', key4: 'value4' } }
  const obj2 = obj1

  // obj1 ──► (0x0000) { key1: 'value1', key2:  ┬  }
  // obj2 ─┘                                    ┴────► (0x0004) { key3: 'value3', key4: 'value4' }

  console.log(obj1 === obj2) // Expected output: true

  console.log(obj1) // Expected output: { key1: 'value1', key2: { key3: 'value3', key4: 'value4' } }
  console.log(obj2) // Expected output: { key1: 'value1', key2: { key3: 'value3', key4: 'value4' } }

  obj1.key1 = 'value7'
  obj1.key2.key3 = 'value9'

  console.log(obj1) // Expected output: { key1: 'value7', key2: { key3: 'value9', key4: 'value4' } }
  console.log(obj2) // Expected output: { key1: 'value7', key2: { key3: 'value9', key4: 'value4' } }
}

{
  // same memory - array in object
  const obj1 = { key1: 'value1', key2: [1, 2, 3] }
  const obj2 = obj1

  // obj1 ──► (0x0000) { key1: 'value1', key2:  ┬  }
  // obj2 ─┘                                    ┴────► (0x0004) [1, 2, 3]

  console.log(obj1 === obj2) // Expected output: true

  console.log(obj1) // Expected output: { key1: 'value1', key2: [1, 2, 3] }
  console.log(obj2) // Expected output: { key1: 'value1', key2: [1, 2, 3] }

  obj1.key1 = 'value7'
  obj1.key2[1] = 7

  console.log(obj1) // Expected output: { key1: 'value7', key2: [1, 7, 3] }
  console.log(obj2) // Expected output: { key1: 'value7', key2: [1, 7, 3] }
}

{
  // shallow copy - using spread syntax to array in object
  const obj1 = { key1: 'value1', key2: [1, 2, 3] }
  const obj2 = { ...obj1 }

  // obj1 ──► (0x0000) { key1: 'value1', key2:  ┬  }
  //                                            |─────► (0x0008) [1, 2, 3]
  // obj2 ──► (0x0004) { key1: 'value1', key2:  ┴  }

  console.log(obj1 === obj2) // Expected output: false

  console.log(obj1) // Expected output: { key1: 'value1', key2: [1, 2, 3] }
  console.log(obj2) // Expected output: { key1: 'value1', key2: [1, 2, 3] }

  obj1.key1 = 'value7'
  obj1.key2[1] = 7

  console.log(obj1) // Expected output: { key1: 'value7', key2: [1, 7, 3] }
  console.log(obj2) // Expected output: { key1: 'value1', key2: [1, 7, 3] }
}

🏷️ Synchronous and Asynchronous

  • Synchronous
    • Execute code in order
  • Asynchronous
    • Unpredictable when code will run
  • JavaScript is synchronous. -Execute the code block in order after hoisting.

Callback function

  • A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action

{
  // named callback function
  function callbackFunc() {
    console.log('run callbackFunc()')
  }

  function printData(callback) {
    console.log('run printData()')
    callback()
    console.log('done printData()')
  }

  printData(callbackFunc)
}

{
  // named callback function with parameters
  function callbackFunc(data) {
    console.log('run callbackFunc()')
    console.log(data)
  }

  function printData(num1, num2, callback) {
    console.log('run printData()')
    const sum = num1 + num2
    callback(sum)
    console.log('done printData()')
  }

  printData(1, 2, callbackFunc)
}

{
  // arrow callback function
  const callbackFunc = () => {
    console.log('run callbackFunc()')
  }

  function printData(callback) {
    console.log('run printData()')
    callback()
    console.log('done printData()')
  }

  printData(callbackFunc)
}

{
  // arrow callback function with parameters
  const callbackFunc = (data) => {
    console.log('run callbackFunc()')
    console.log(data)
  }

  function printData(num1, num2, callback) {
    console.log('run printData()')
    const sum = num1 + num2
    callback(sum)
    console.log('done printData()')
  }

  printData(1, 2, callbackFunc)
}

{
  // anonymous callback function
  function printData(callback) {
    console.log('run printData()')
    callback()
    console.log('done printData()')
  }

  printData(function () {
    console.log('run callbackFunc()')
  })
}

{
  // anonymous callback function with parameters
  function printData(num1, num2, callback) {
    console.log('run printData()')
    const sum = num1 + num2
    callback(sum)
    console.log('done printData()')
  }

  printData(1, 2, function (data) {
    console.log('run callbackFunc()')
    console.log(data)
  })
}

{
  // anonymous arrow callback function
  function printData(callback) {
    console.log('run printData()')
    callback()
    console.log('done printData()')
  }

  printData(() => {
    console.log('run callbackFunc()')
  })
}

{
  // anonymous arrow callback function with parameters
  function printData(num1, num2, callback) {
    console.log('run printData()')
    const sum = num1 + num2
    callback(sum)
    console.log('done printData()')
  }

  printData(1, 2, (sum) => {
    console.log('run callbackFunc()')
    console.log(sum)
  })
}

Synchronous Callback

function callbackFunc(data) {
  console.log('run callbackFunc()')
  console.log(data)
  console.log('done callbackFunc()')
}

function doSynchronousTask(num1, num2, callback) {
  console.log('run doSynchronousTask()')
  const sum = num1 + num2
  callback(sum)
  console.log('done doSynchronousTask()')
}

doSynchronousTask(1, 2, callbackFunc)

/*
Expected output:
   run doSynchronousTask()
   run callbackFunc()
   3
   done callbackFunc()
   done doSynchronousTask()
*/

Asynchronous Callback

function doAsynchronousTask(num1, num2, callback) {
  console.log('start doAsynchronousTask() task')
  setTimeout(() => {
    const sum = num1 + num2
    callback(sum)
  }, 2000)
  console.log('end doAsynchronousTask() task')
}

function callbackFunc(data) {
  console.log('run callbackFunc()')
  console.log(data)
  console.log('done callbackFunc()')
}

doAsynchronousTask(1, 2, callbackFunc)

/*
Expected output:
   run doAsynchronousTask()
   done doAsynchronousTask()
   run callbackFunc()
   3
   done callbackFunc()
*/

When should we use it?

function doRepeat(count, callback) {
  for (let i = 0; i < count; i++) {
    callback(i)
  }
}

doRepeat(3, (i) => { // Expected output: 0, 1, 2
  console.log(i)
})

doRepeat(3, (i) => { // Expected output: 0, 2, 4
  console.log(i * 2)
})

Promise

  • A Promise is a JavaScript object for asynchronous operation.
  • A Promise is in one of these states:
    • pending: initial state, neither fulfilled nor rejected.
    • fulfilled: meaning that the operation was completed successfully.
    • rejected: meaning that the operation failed.

Constructor

  • Creates a new Promise object.
  • The constructor is primarily used to wrap functions that do not already support promises.
// When new Promise is created, the executor runs automatically.
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // doing some heavy work (network, read files)
    console.log('doing something...')
    resolve('success')
    // reject(new Error('no network'))
    console.log('after resolve or reject..')
  }, 1000)
})

console.log(promise) // Expected output: Promise {<pending>}

setTimeout(() => {
  console.log('promise done')
  console.log(promise) // Expected output: Promise {<fulfilled>: 'success'}
}, 2000)

Static methods

  • Promise.resolve()
    • Returns a Promise object that is resolved with the given value.
    • If the value is a thenable (i.e. has a then method), the returned promise will "follow" that thenable, adopting its eventual state; otherwise, the returned promise will be fulfilled with the value.
  • Promise.reject()
    • Returns a new Promise object that is rejected with the given reason.
  • Promise.try()
    • Takes a callback of any kind (returns or throws, synchronously or asynchronously) and wraps its result in a Promise.
  • Promise.withResolvers()
    • Returns an object containing a new Promise object and two functions to resolve or reject it, corresponding to the two parameters passed to the executor of the Promise() constructor.

Promise concurrency
  • The Promise class offers four static methods to facilitate async task concurrency:

    • Promise.all()
      • Fulfills when all of the promises fulfill; rejects when any of the promises rejects.
      • Promise.all() waits until all the given promises are fulfilled and then returns an array of all the promises results.
      • If any of the promises are rejected, Promise.all() fails immediately with the reason from the rejected promise.
      const promise1 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('First'), 1000)
      )
      const promise2 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('Second'), 3000)
      )
      const promise3 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('Third'), 2000)
      )
      
      Promise.all([promise1, promise2, promise3])
        .then((values) => {
          console.log('All promises fulfilled:', values) // Expected output: All promises fulfilled: ['First', 'Second', 'Third']
        })
        .catch((error) => {
          console.error('One or more promises rejected:', error)
        })

    • Promise.allSettled()
      • Fulfills when all promises settle.
      • Promise.allSettled() waits until all given promises are either fulfilled or rejected, and then returns an array of objects representing the results of each promise.
      • Each object includes the promise’s status (fulfilled or rejected) and the result (value or reason).
      const promise1 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('First'), 1000)
      )
      const promise2 = new Promise((resolve, reject) =>
        setTimeout(() => reject('Second failed'), 3000)
      )
      const promise3 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('Third'), 2000)
      )
      
      Promise.allSettled([promise1, promise2, promise3]).then((results) => {
        console.log('All promises settled:', results)
        /*
        Expected output:
        All promises settled: [
          { status: 'fulfilled', value: 'First' },
          { status: 'rejected', reason: 'Second failed' },
          { status: 'fulfilled', value: 'Third' }
        ]
      */
      })

    • Promise.any()
      • Fulfills when any of the promises fulfills; rejects when all of the promises reject.
      • Promise.any() returns the fulfillment result of the first promise that fulfills among the given promises.
      • If all promises are rejected, it throws an AggregateError.
      const promise1 = new Promise((resolve, reject) =>
        setTimeout(() => reject('First failed'), 1000)
      )
      const promise2 = new Promise((resolve, reject) =>
        setTimeout(() => reject('Second failed'), 3000)
      )
      const promise3 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('Third'), 2000)
      )
      
      Promise.any([promise1, promise2, promise3])
        .then((value) => {
          console.log('First fulfilled promise:', value) // Expected output: First fulfilled promise: Third
        })
        .catch((error) => {
          console.error('All promises were rejected:', error)
        })

    • Promise.race()
      • Settles when any of the promises settles. In other words, fulfills when any of the promises fulfills; rejects when any of the promises rejects.
      • Promise.race() returns the first promise that either fulfills or rejects among the given promises.
      • In other words, it returns the result of the first promise to settle.
      const promise1 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('First'), 3000)
      )
      const promise2 = new Promise((resolve, reject) =>
        setTimeout(() => reject('Second failed'), 1000)
      )
      const promise3 = new Promise((resolve, reject) =>
        setTimeout(() => resolve('Third'), 2000)
      )
      
      Promise.race([promise1, promise2, promise3])
        .then((value) => {
          console.log('Race winner:', value) // Expected output: Race winner: Second failed
        })
        .catch((error) => {
          console.error('Race failed with:', error)
        })

Instance methods

  • Promise.prototype.then()

    • Appends fulfillment and rejection handlers to the promise.
      const promise = new Promise((resolve, reject) => {
        // doing some asynchronous operation
        setTimeout(() => resolve('Success'), 1000)
      })
      
      // Adding handlers with .then()
      promise.then(
        (value) => console.log('Fulfilled with:', value), // Handler for fulfillment - Expected output: Fulfilled with: Success
        (error) => console.error('Rejected with:', error) // Handler for rejection
      )

    • And returns a new promise resolving to the return value of the called handler.
      const promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve('Original Value'), 1000)
      })
      
      const newPromise = promise.then((value) => {
        console.log('Handled Value:', value) // Expected output: Handled Value: Original Value
        return 'New Value'
      })
      
      newPromise.then((value) => console.log('New Promise Value:', value)) // Expected output: New Promise Value: New Value

    • Or to its original settled value if the promise was not handled (i.e. if the relevant handler onFulfilled or onRejected is not a function).
      const promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("Original Value"), 1000)
      })
      
      const newPromise = promise.then()  // No handler functions provided
      
      newPromise.then((value) => console.log("Original Value Passed Through:", value)) // Expected output: Original Value Passed Through: Original Value

  • Promise.prototype.catch()

    • Appends a rejection handler callback to the promise.
      const promise = new Promise((resolve, reject) => {
        // Error occurs after 1 second
        setTimeout(() => reject(new Error('Something went wrong!')), 1000)
      })
      
      // Adding a rejection handler with catch()
      promise.catch((error) => {
        console.error('Caught Error:', error.message) // Expected output: Caught Error: Something went wrong!
      })

    • And returns a new promise resolving to the return value of the callback if it is called.
      const promise = new Promise((resolve, reject) => {
        // Error occurs after 1 second
        setTimeout(() => reject(new Error('Something went wrong!')), 1000)
      })
      
      const newPromise = promise.catch((error) => {
        console.error('Caught Error:', error.message) // Expected output: Caught Error: Something went wrong!
        return 'Recovered' // Returning a new value
      })
      
      newPromise.then((value) => {
        console.log('New Promise Value:', value) // Expected output: New Promise Value: Recovered
      })

    • Or to its original fulfillment value if the promise is instead fulfilled.
      const promise = new Promise((resolve, reject) => {
        // Successfully resolves after 1 second
        setTimeout(() => resolve('Success'), 1000)
      })
      
      const newPromise = promise.catch((error) => {
        console.error('Caught Error:', error.message)
        return 'Recovered'
      })
      
      newPromise.then((value) => {
        console.log('Original Fulfillment Value:', value) // Expected output: Original Fulfillment Value: Success
      })

  • Promise.prototype.finally()

    • Appends a handler to the promise.
      const promise = new Promise((resolve, reject) => {
        // Successfully resolves after 1 second
        setTimeout(() => resolve('Success'), 1000)
      })
      
      // Adding a handler with finally()
      promise.finally(() => {
        console.log('Promise settled!') // Expected output: Promise settled!
      })

    • And returns a new promise that is resolved when the original promise is resolved.
      const promise = new Promise((resolve, reject) => {
        // Successfully resolves after 1 second
        setTimeout(() => resolve('Success'), 1000)
      })
      
      const newPromise = promise.finally(() => {
        console.log('Cleaning up...') // Expected output: Cleaning up...
      })
      
      newPromise.then((value) => {
        console.log('Final Value:', value) // Expected output: Final Value: Success
      })

    • The handler is called when the promise is settled, whether fulfilled or rejected.
      const promise = new Promise((resolve, reject) => {
        // Error occurs after 1 second
        setTimeout(() => reject(new Error('Something went wrong!')), 1000)
      })
      
      // finally() will be called whether the promise is fulfilled or rejected
      promise
        .catch((error) => {
          console.error('Caught Error:', error.message) // Expected output: Caught Error: Something went wrong!
        })
        .finally(() => {
          console.log('Promise settled! Cleaning up...') // Expected output: Promise settled! Cleaning up...
        })

async / await

  • Promise-based behavior to be written in a cleaner style and avoiding the need to explicitly configure promise chains.
  • An async function declaration creates an AsyncFunction object.
  • Each time when an async function is called, it returns a new Promise which will be resolved with the value returned by the async function, or rejected with an exception uncaught within the async function.
    • An async function automatically returns a Promise.
  • Await expressions make promise-returning functions behave as though they're synchronous by suspending execution until the returned promise is fulfilled or rejected.
    • The await keyword can only be used inside async functions.
    • Async functions can contain zero or more await expressions.
  • You can use await to handle Promises more cleanly and use try/catch blocks to manage errors in asynchronous code.
function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('resolved')
    }, 2000)
  })
}

async function asyncCall() {
  console.log('calling')
  const result = await resolveAfter2Seconds()
  console.log(result)
  // Expected output: "resolved"
}

asyncCall()

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published