JavaScript

For non-JavaScript developers

Brief history of JavaScript

Netscape Navigator & Brendan Eich

In 1995 Netscape tasked Brendan Eich to create a new dynamic language for the Web.
10 days later LiveScript is born.

As a collaboration with Sun Microsystems, LiveScript is renamed JavaScript as a complement to Java for web development.

Netscape vs Microsoft

Microsoft reverse engineers JavaScript, creates JScript for Internet Explorer 4.

ECMA

In 1997 as Netscape quickly loses market shares, Brendan submits "JavaScript" to ECMA. Without copyright ownership of the name, JavaScript gets renamed as ECMAScript.

The first golden age

ECMAScript sees its first golden age as Ajax, a set of technologies for JavaScript and XML, gets released. JQuery, MooTools and many other libraries are released during this period.

Google arrives just in time

In 2008 Google releases Chrome, using a new engine named V8, adds JiT and forces the work on ECMAScript to resume after Microsoft blockade, ES5 finally sees the light of day.

Ryan Dahl announces Node

Enters JSConf 2009.

Announces Node.

Leaves.

Second golden age: React

In 2013 Facebook revolutionizes the web dev world by creating an endless amount of new jobs to fix React applications.

What is JiT?

Is JavaScript a compiled or interpreted language?
Is JavaScript a compiled or interpreted language?
Is JavaScript a compiled or interpreted language?
JavaScript uses Just in Time (JiT) compilation, meaning it is compiled during execution.

JiT in practice

Compiler (TypeScript lives here!)

Scope

Engine

Compiler (TypeScript lives here!)

Generates & hoist code

Scope

Engine

Compiler (TypeScript lives here!)

Generates & hoist code

Scope

Maintains look-up lists

Engine

Compiler (TypeScript lives here!)

Generates & hoist code

Scope

Maintains look-up lists

Engine

Executes code

JavaScript is a dynamic language because variables are assigned by the engine during runtime based on the value they hold at the time.
Compiler

              var a = 1;

              function myFunc(b) {
                console.log(a + b)
              }

              myFunc(2)
            
How many declarations are in this snippet?
Compiler

              var a = 1;

              function myFunc(b) {
                console.log(a + b)
              }

              myFunc(2)
            
a is declared in the global scope.
Compiler

              var a = 1;

              function myFunc(b) {
                console.log(a + b)
              }

              myFunc(2)
            
myFunc is declared in the global scope.
Compiler

              var a = 1;

              function myFunc(b) {
                console.log(a + b)
              }

              myFunc(2)
            
b is declared in myFunc scope.
The compiler's job is to define variables and function and hoist them at the beginning of the relative scope.
    The engine performs mainly 2 jobs:
  • LHS lookup
  • RHS lookup
    The engine performs mainly 2 jobs:
  • LHS lookup assigns a value
  • RHS lookup
    The engine performs mainly 2 jobs:
  • LHS lookup assigns a value
  • RHS lookup retrieves a value
Engine

              var a = 1;

              function myFunc(b) {
                console.log(a + b)
              }

              myFunc(2)
            
LHS lookups: a, b (implicit)
Engine

              var a = 1;

              function myFunc(b) {
                console.log(a + b)
              }

              myFunc(2)
            
RHS lookups: a, myFunc(2), a +, + b
    Errors:
  • Reference Error is a failure of LHS or RHS lookup, the variable does not exist at the time of lookup.
  • Type Error is operation failure on an value found in scope.

Scope

Scope is an author defined environment where code is evaluated and executed.

              var a = 1;

              function myFunc(b) {
                function myOtherFunc(c) {
                  console.log(a, b, c)
                }
                myOtherFunc(3)
              }

              myFunc(2) // 1, 2, 3
            
How many variables does each scope have?

              var a = 1;

              function myFunc(b) {
                function myOtherFunc(c) {
                  console.log(a, b, c)
                }
                myOtherFunc(3)
              }

              myFunc(2) // 1, 2, 3
            
3 scopes, each has one variable
Scope lookup stops once it finds a matching variable

              var a = 1; // shadowed by the other a

              function myFunc() {
                var a = 2
                console.log(a)
              }

              myFunc() // 2
            

Principle of least privilege and scope

Function scope

The "default" JS scope works for function blocks only. This is bad.
Examples of scope pollution

              for (var i = 0; i < 10; i++) { // false scope
                console.log(i)
              }

              if (true) {
                var b = 20; // false scope
                function f() { console.log(1) } // false scope
              }

              console.log(i, b) // 10, 20
              f(); // 1
            
The solutions exist, and they called let and const
Block scoped

              for (let i = 0; i < 10; i++) {
                console.log(i)
              }

              if (true) {
                const b = 20;
                const f = () => { console.log(1) }
              }

              console.log(i, b) // Reference Error
              f(); // Reference Error
            
    Advantages of block scope:
  • Automatic garbage collection
  • Least privilege by default
  • Const introduces fixed values
  • Const prevents name reassignment

Interlude #1

Always name your functions, you don't want to be the dev debugging this. A browser call stack showing multiple anonymous function calls, making debugging difficult

              setTimeout(() => { // bad
                console.log(1)
              }, 1000)

              const a = () => { console.log(1) }
              setTimeout(a, 1000) // good
            

this Keyword

this is a special reference that is not defined by the compiler, but the engine. Its value depends on the place that invoked it.

              function greetings() {
                console.log("Hello I am", this.name);
              }

              var me = {
                name: "Francesco"
              }
              var bossman = {
                name: "Vlad"
              }

              greetings.call(me) // Hello I am Francesco
              greetings.call(bossman) // Hello I am Vlad
            

Primitives

    Primitives:
  • string
  • number (NaN is a number!)
  • boolean
  • undefined
  • null
  • symbol
  • bigint
Primitives are immutable, their values cannot be altered, every new value is individually stored in memory.

              let a = 1;
              let b = a;

              b = 2;

              console.log(a) // 1
              console.log(b) // 2
            
JavaScript automatically uses object constructors behind the curtains when performing operations on primitives.

              let realString = "a"
              typeof realString // "string"
              realString instanceof String // false

              let objectString = new String("b")
              typeof objectString // "object"
              objectString instanceof String // true
            
Never use object constructors to define a primitive.
undefined is a special primitive, it implicitly represents the absence of a value.

              let a; // undefined

              const b = () => {
                return; // undefined
              }

              let c = [1,2].find(n => n === 3) // undefined
            
  • undefined should not be used explicitly, but only detected implicitly.
  • null can be used in cases where we explicitly don't want a value to be returned.
  • null and undefined have no constructor.
In an average interaction, the difference between null and undefined should not matter.

More generic falsy checks are safer when the type difference does not matter.

              let obj = { a: 1, b: null };

              typeof a.b === "undefined" // false
              typeof a.c === "undefined" // true

              !a.b // true
              !a.c // true
            

Objects

    Everything that is not a primitive is an exotic object, some examples:
  • Array
  • Function
  • Error
  • Date
  • Set
  • Proxy
  • Reflect
  • Map
  • Math
  • ...
Objects are not stored as immutable values, but as reference values. Meaning all variables are connected to the same value and no copies are created.

              let a = [1,2];
              let b = a;

              b.push(3);

              console.log(a) // [1,2,3]
              console.log(b) // [1,2,3]
            
Class inheritance and polymorphism (OOP) as seen in other languages are not concepts that JavaScript inherits.

JavaScript uses prototypal inheritance, meaning each object has a prototype property, which point to another object (or null!) recursively. Objects inherit from other objects in a dynamic rather than static way.
Java inheritance

              class Animal {
                void bark() {
                  System.out.println("ham ham");
                }
              }

              class Dog extends Animal {
                void eat() {
                  System.out.println("gnam gnam");
                }
              }

              public class Main {
                public static void main(String[] args) {
                  Dog dog = new Dog();
                  dog.eat(); // Own method
                  dog.bark(); // Inherited method
                }
              }
            
JavaScript prototypal inheritance

              const animal = {
                bark: () => console.log('ham ham')
              };

              const dog = Object.create(animal);
              dog.eat = () => console.log('gnam gnam');

              dog.eat(); // Own method
              dog.bark(); // Inherited method

              animal.bark = () => console.log('bau bau')

              dog.bark(); // bau bau
            
JavaScript prototypal inheritance, class syntax

              class Animal {
                bark() { console.log('ham ham'); }
              };
              
              class Dog extends Animal {
                eat() { console.log('gnam gnam'); }
              };
            
              const myDog = new Dog();
              myDog.eat();  // Own method
              myDog.bark(); // Inherited method
            
              Animal.prototype.bark = () => console.log('bau bau');
              myDog.bark(); // bau bau
            
This works because 'class' is just syntactic sugar for the prototype system.
Trying to emulate classical inheritance in JavaScript tends to generate hard to understand code and hard to debug problems linked to the way reference works.
Our purpose when writing JavaScript code is not to copy behaviour from other languages, but use JavaScript's own features to solve the unique problems encountered in a frontend environment.

Interlude #2

This is called callback hell

              fetchItalianJoke((joke) => {
                // if (...)
                translateJokeToRomanian(joke, (translateJoke) => {
                  // if (...)
                  doubleCheckTranslation(translateJoke, (correctTranslatedJoke) => {
                    // if (...)
                    postJoke(correctTranslatedJoke, () => {
                      // if (...)
                      console.log("Done!")
                    })
                  })
                })
              })
            
I have omitted reject/error for readibility purposes.

Number of scopes increases drastically as nesting gets deeper. Promises return a similar result.
This is the same code, using async/await

              const getJoke = async () => {
                try {
                  const joke = await fetchItalianJoke()
                  const translatedJoke = await translateJokeToRomanian(joke)
                  const correctTranslatedJoke = await doubleCheckTranslation(translatedJoke)

                  await postJoke(correctTranslatedJoke)
                  console.log("Done!")
                } catch (err) { // err is block scoped!
                  console.error(err)
                }
              }
            
3 Block scopes, adding 10 more awaits will still max out at 3 block scopes.

Pure Functions

Pure functions are functions devoid of side effects, making them more reliable and easier to understand.

              const num1 = 1;
              const num2 = 2;

              const sum = (a,b) => a + b;

              const num3 = sum(num1, num2)

              console.log(num1) // 1
              console.log(num2) // 2
              console.log(num3) // 3
            
Idempotency (same input -> same output) create predictable and easily testable code regardless of how many times it is executed.

              for (let i = 0; i < 100; i++) {
                // run this test 100 times
                test('adding 2 numbers results in their sum', () => {
                  const r1 = Math.floor(Math.random() * 10) + 1 // 1-10
                  const r2 = Math.floor(Math.random() * 10) + 1 // 1-10

                  expect(sum(r1, r2)).toBe(r1 + r2);
                });
              }
            
Refactoring code to pure functions can lead to more modular code as well.

            const addNum = (arr) => { // impure
              arr.push(arr.length + 1)
            }

            let numbers = [1,2,3,4]
            addNum(numbers) // side effect on numbers
          

            const addNum = (arr) => { // pure
              return arr.length + 1
            }

            let numbers = [1,2,3,4]
            let number = addNum(numbers) // no side effects
          
Reducing side effects in an application state can reduce the occurrence of unexpected bugs, raising confidence and readibility in the code.

Immutability

While purity allows us to change our state only once, immutability allows us to not change it at all.
JavaScript primitives are immutable, but objects are not.

            const a = 1
            a = 2 // TypeError!

            const b = { a: 1 }
            b.a = 2 // { a: 2 }
          
Both JavaScript and TypeScript provide utilities to make objects readonly, but they are only shallow.

              const x = Object.freeze({ a: 1, b: { c: 2 }})

              x.a = 2 // error
              x.b.c = 1 // allowed
            

              
            
Immutability is achieved by discarding old values and creating new ones to keep our state up to date, never mutating existing values.

              const updateUserRole = (user, role) => {
                const newUser = { ...user, role }

                return newUser
              }
            
While there are tricks and libraries that allow deep immutability, the issue is in our way of thinking about our code, more than the tools we use.

You should treat all data as immutable by default.

Single source of truth

There's only one value, everything else is a reference to it.

By having a centralized application state, the UI needs only to listen to a single entity, and update only when that entity is updated.

This greatly simplifies how a UI is written and brings down maintenance time considerably.
An immutable, single source of truth store, using pure functions for updates.

              const reducer = (state, { type } = {}) => {
                switch(type) {
                  case 'increment':
                    return { value: state.value + 1 }
                  default:
                    return state
                }
              }

              const store = {
                state: { value: 1 },
                dispatch: function (action) {
                  this.state = reducer(this.state, action)
                }
              }

              store.state // { value: 1 }
              store.dispatch({ type: "increment" })
              store.state // { value: 2 }
            
Making it immutable by hiding the state and exposing a frozen object to views.

              const stateSymbol = Symbol("state");

              const store = {
                [stateSymbol]: { value: 1 },
                getState: function () {
                  return Object.freeze(this[stateSymbol])
                },
                dispatch: function (action) {
                  this[stateSymbol] = reducer(this[stateSymbol], action)
                }
              }
            

The node package manager

The JavaScript ecosystem is built around small, user created modular libraries. The ecosystem exploded during the last decades, with more than 2 million packages currently hosted on npm.
npm packages are just JavaScript project, and as any project, they can import other projects. This can lead to immense dependency trees usually multiple GBs in size. Meme comparing a node_modules folder to the density of a black hole.
The user-created nature and deeply nested structure of npm modules can lead to structural issues in even the best curated codebases. XKCD comic showing a giant tower of blocks labeled 'All Modern Digital Infrastructure' resting on one tiny, crucial support block labeled: 'A project some random person in Nebraska has been thanklessly maintaining since 2003.'
    The left-pad incident
  • In 2016 kik asks npm the rights over the kik package owned by Azer Koçulu.
  • npm agrees, removing the ownership from Koçulu.
  • Koçulu removes his 273 packages from npm, including left-pad.
  • Widespread installation failures for tools like Babel halt development for countless companies.
  • Major companies like Meta, Netflix, Spotify and Paypal are affected.
  • npm restores left-pad and tightens rules around unpublishing packages with dependents.
Unlike Maven for Java, npm relies on open source contributions.

This makes the ecosystem wider, but increases the attack surface for supply chain attacks.

Libraries and the evolution of JS

The contributions from the open source community cannot be understated for the growth of JavaScript.

Many features like Promises (bluebird), the fetch API (jQuery), .map and .filter, (lodash) and even the import/export syntax (node) were inspired by open source libraries.

Avoiding external libraries is not the point, knowing the features of the language is.

    Should I use a library?

  • Is the problem already solved by modern JavaScript?
  • Does the library solve a complex problem?
  • Is the library healthy?

    Should I use a library?

  • Is the problem already solved by modern JavaScript?
  • Check the MDN Docs.

  • Does the library solve a complex problem?
  • Is the library healthy?

    Should I use a library?

  • Is the problem already solved by modern JavaScript?
  • Check the MDN Docs.

  • Does the library solve a complex problem?
  • Complex animations or date manipulation are examples of complex problems.

  • Is the library healthy?

    Should I use a library?

  • Is the problem already solved by modern JavaScript?
  • Check the MDN Docs.

  • Does the library solve a complex problem?
  • Complex animations or date manipulation are examples of complex problems.

  • Is the library healthy?
  • Check GitHub and npm to assess the security risk and maintenence burden.

Code you don't understand is code you can't trust.

If you need to execute it to understand it, there's a problem with the code.