We've already discussed a big part of the things that JavaScript does behind the scenes, but there's one important detail left to discuss: the creation of the execution context.
let two = 2;
function double(num){
return num * x;
}
console.log(double(8)); // 16
JavaScript engines consist of two main components: a call stack and a memory heap.
engine
When a function is executed in JavaScript, a new frame is created in the call stack. Each frame represents an Execution Context.
When a script is first executed, the JavaScript engine creates a global Execution Context object and pushes it to the call stack.
All of the global code, i.e., code which is not inside any function or object, is executed inside the global execution context.
It's possible to conceptually represent an EC as an object with three properties:
When a function is called, the interpreter scans it for declarations and creates the activation object. This is when hoisting occurs.
The activation object is the object used to hold the properties that describe the environment and scope of an executing function.
function myFunction(num1, num2) {
var num3 = 30;
return num1 + num2 + num3;
}
myFunction(10, 20);
num1 | 10 |
num2 | 20 |
arguments | {0: 10, 1: 20} |
num3 | 30 |
Activation Object |
---|
The Scope Chain is a container of Activation Objects for the executing function lexical scope. Scope Chain has a reference to the activation objects of all the parents of the executing function.
In other words, it's what makes the lexical scope work.
const name = 'Dee';
const age = 25;
const city = 'Lisbon';
function getPersonInfo() {
const name = 'Sarah';
const age = 22;
return `${name} is ${age} and lives in ${city}`;
}
console.log(getPersonInfo());
In JavaScript, this is a reference to the object that owns the currently executing code. The implicit this argument is available inside functions.
The value of this is dynamically bound depending on how the function was called.
const myObj = {
number: 42,
action: function () {
let number = 22;
return this.number;
},
};
console.log(myObj.action()); // What will be the output?
In the global context, i.e., outside any function, this refers to the global object.
When the execution environment is the browser, global object refers to the window object.
// BROWSER
var a = 'Pikachu';
let b = 'Bulbasaur';
console.log(this.a)
console.log(a);
console.log(this.b);
console.log(b);
// What will happen?
// NODE
var a = 'Pikachu';
let b = 'Bulbasaur';
console.log(this.a)
console.log(a);
console.log(this.b);
console.log(b);
// What will happen?
Inside a function, the value of this depends on how the function is being called.
function myFunc() {
return this;
}
// BROWSER:
console.log(myFunc()); // window
// NODE:
console.log(myFunc()); // globalThis
let myObj = {
action: myFunc
};
// BROWSER:
console.log(myObj.action()); // myObj
// NODE:
console.log(myObj.action()); // myObj
Implicit this binding
Using the methods bind, call and apply, we can explicitly control the this binding.
let myObj = {
description: 'myObj',
action: whatsThis
};
let description = 'global';
function whatsThis() {
console.log(this.description);
}
whatsThis(); // undefined as this refers to the global object
myObj.action(); // 'myObj'
whatsThis.call(myObj); // 'myObj'
whatsThis.apply(myObj); // 'myObj'
let newFunc = whatsThis.bind(myObj);
newFunc(); // 'myObj'
function printThisAndArguments() {
console.log(this, arguments);
}
let myObj = {
prop: 23
}
// CALL
printThisAndArguments.call(myObj, 'Grace', 'John'); // { prop: 23 } { '0': 'Grace', '1': 'John' }
// APPLY
printThisAndArguments.apply(myObj, ['Grace', 'John']); // { prop: 23 } { '0': 'Grace', '1': 'John' }
//BIND
const printMyObjAndArguments = printThisAndArguments.bind(myObj);
printMyObjAndArguments('Grace', 'John'); // { prop: 23 } { '0': 'Grace', '1': 'John' }
In arrow functions, this retains the value of the enclosing lexical context.
let description = 'global';
const whatsThis = () => {
console.log(this.description);
}
let myObj = {
description: 'myObj',
action: whatsThis
};
whatsThis();
myObj.action();
whatsThis.call(myObj);
whatsThis.apply(myObj);
let newFunc = whatsThis.bind(myObj);
newFunc();
// What will happen?
let myObj = {
action: function () {
return () => {
console.log(this);
}
}
};
myObj.action()();
// What about now?
function upperCaseArguments() {
return arguments.forEach(function (arg) {
return arg.toUpperCase();
});
}
// What will happen?
function upperCaseArguments() {
var argsAsArray = Array.prototype.slice.call(arguments);
return argsAsArray.map(function (arg) {
return arg.toUpperCase();
});
}
A closure is the combination of a function and the lexical environment within which that function was declared.
In other words, a closure is created when a function "remembers" its enclosing lexical environment.
function init() {
const name = 'Anne';
return function () {
console.log(name);
};
}
const displayName = init();
displayName();
Closures are useful because they let you associate data (the lexical environment) with a function that operates on that data.
This concept has obvious ties to the OOP concept, allowing us to emulate the concept of classes with public and private members.
let secretMessageFactory = function (secretMessage, key) {
let tries = 3;
return {
get: function (secretKey) {
if (tries === 0) {
return 'No more tries. Secret has been destroyed.';
}
if (key === secretKey) {
return secretMessage;
}
tries--;
return 'Wrong.';
}
};
};
let secret = secretMessageFactory('secret message', 1234);
console.log(secret.get(1234)); // secret message
console.log(secret.get()); // Wrong.
console.log(secret.get(1509)); // Wrong.
console.log(secret.get(1090)); // Wrong.
console.log(secret.get(1090)); // 'No more tries. Secret has been destroyed.'
Another Module Example
The creation of closures will sometimes produce unexpected results.
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function () {
console.log(i);
});
}
funcs[0]();
funcs[1]();
funcs[2]();
// What will happen?
The code above works with an IIFE (Immediately Invoked Function Expression) that returns a new function with a different execution context where i has a distinct value.
var funcs = [];
// We need to create a new execution context, closed over each value of i
for (var i = 0; i < 3; i++) {
funcs.push((function (i) {
return function () {
console.log(i);
};
})(i));
funcs[0](); // outputs 0
funcs[1](); // outputs 1
funcs[2](); // outputs 2
Pass The Tests
JavaScript uses special functions called constructor functions to define and initialise objects and their features.
// MANUAL CREATION
function createNewPerson(name) {
const obj = {
name,
greeting: function () {
console.log('Hi! I\'m ' + this.name + '.');
}
};
return obj;
}
const person = createNewPerson('Paul');
// CONSTRUCTOR FUNCTION
function Person(name) {
this.name = name;
this.greeting = function () {
console.log('Hi! I\'m ' + this.name + '.');
}
}
const person = new Person('Paul');
A constructor function is JavaScript's version of a class. It returns nothing, simply defining common properties and methods to all objects of a given type.
Constructor functions usually start with a capital letter. This isn't a syntax requirement; it's a convention used to make constructor functions easier to recognise in code.
The new keyword is used to tell the interpreter that we want to create a new object instance.
In the previous example, every time we called the Person constructor, we created a new greeting function. This isn't ideal behaviour, and we'll learn how to solve it later.
Generic objects also have a constructor, which generates an empty object.
// CREATING AN EMPTY OBJECT AND ADDING PROPERTIES
const person = new Object(); // {}
person.name = 'Paul';
person.age = 54;
// PASSING AN OBJECT LITERAL AS ARGUMENT
let person1 = new Object({
name: 'Paul',
age: 54
});
JavaScript is a prototype-based language.
Prototypes are the mechanism by which JS objects inherit features from one another.
To provide inheritance, objects can have a prototype object, which acts as a template object that it inherits methods and properties from.
An object's prototype object may also have a prototype object, which it inherits methods and properties from, and so on. This is often referred to as a prototype chain.
An objects prototype is accessible via two ways:
Constructor functions also have access to a prototype property.
This property, despite its name, does NOT represent the function's prototype.
In a constructor function, the prototype property represents the blueprint for instances created from said function.
Prototypal Inheritance is based on a delegation mechanism in which non-existent properties are looked up in prototype objects.
function Person(first, last, age) {
this.name = {
first,
last
};
this.age = age;
this.breathe = function () {
console.log('*breathing noises*');
}
}
const arnold = new Person('Arnold', 'Schwarzenegger', 74);
arnold.breathe(); // available from the instance itself
console.log(arnold.valueOf()); // where does valueOf come from?
Prototype Chain
Remember this problem?
Every time we create a new Person object, we're creating a new breathe() function. Considering that every Person object breathes the same way, there mus be a better way to do it.
function Person(first, last, age) {
this.name = {
first,
last
};
this.age = age;
this.breathe = function () {
console.log('*breathing noises*');
}
}
function Person(first, last, age) {
this.name = {
first,
last
};
this.age = age;
}
Person.prototype.breathe = function () {
console.log('*breathing noises*');
};
const arnold = new Person('Arnold', 'Schwarzenegger', 74);
arnold.breathe(); // available from the instance's prototype
Note that methods and properties are not copied from one object to another in the prototype chain. They are accessed by walking up the chain.
function AwesomePerson(first, last, age, occupation) {
Person.call(this, first, last, age); // chain Person and AwesomePerson constructors
this.occupation = occupation;
}
// Make AwesomePerson inherit from Person by overriding Person prototype object
AwesomePerson.prototype = Object.create(Person.prototype);
// Recreate constructor property destroyed in previous line
Object.defineProperty(AwesomePerson.prototype, 'constructor', {
value: AwesomePerson,
enumerable: false, // so that it does not appear in 'for in' loop
writable: true
});
const arnold = new AwesomePerson('Arnold', 'Schwarzenegger', 74, 'be awesome');
arnold.breathe();
Subclassing can be achieved in pseudo-classical inheritance
by chaining and linking constructors together.
AwesomePerson.prototype.doTheAwesome = function () {
console.log('doing the awesome, in an awesome kind of way.');
}
const arnold = new Person('Arnold', 'Schwarzenegger', 74, 'be awesome');
arnold.doTheAwesome(); // ERROR
ECMAScript 2015 introduces class syntax to JavaScript as a way to write reusable classes using easier, cleaner syntax, which is more similar to Java classes.
It is important to notice that, under the hood, classes still work with prototypal inheritance. Classes are just syntactic sugar.
class Person {
constructor(first, last, age) {
this.name = {
first,
last
};
this.age = age;
}
breathe() {
console.log('*breathing noises*');
};
}
The constructor() method defines the constructor function that represents our Person class. There can ONLY be one.
breathe() is a class method.
class AwesomePerson extends Person {
constructor(first, last, age, occupation) {
super(first, last, age); // 'this' is initialized by calling the parent constructor.
this.occupation = occupation;
}
doTheAwesome() {
console.log('doing the awesome, in an awesome kind of way.');
}
}
class Person {
constructor(first, last, age) {
this.name = {
first,
last
};
this.age = age;
}
get fullName() {
return `${this.name.first} ${this.name.last}`; // Use of template literals, a way of creating a string with an embedded expression. They support multi-line.
}
}
const arnold = new AwesomePerson('Arnold', 'Schwarzenegger', 74, 'be awesome');
console.log(arnold.fullName);
Calling a getter or a setter in JavaScript looks just like accessing a normal property. The difference is you can have extra logic running on the background.
class Person {
(...)
set fullName(newName) {
if (newName.split(' ').length !== 2) {
console.log('First and last name are required. No less, no more.');
return;
}
this.name.first = newName.split(' ')[0];
this.name.last = newName.split(' ')[1];
}
}
const arnold = new AwesomePerson('Arnold', 'Schwarzenegger', 74, 'be awesome');
console.log(arnold.fullName); // Arnold Schwarzenegger
arnold.fullName = 'Sylvester'; // First and last name are required. No less, no more.
console.log(arnold.fullName); // Arnold Schwarzenegger
class Person {
constructor(first, last, age) {
this.name = {
first,
last
};
this.age = age;
}
static greet() {
console.log('Hi!');
}
}
console.log(Person.greet());
The static keyword defines a static method or property for a class.
Static members are called without instantiating their class and cannot be called through a class instance.
An important difference between function declarations and class declarations is that function declarations are hoisted and class declarations are not.
This means that, in order to access a class (wether to create an instance or subclass) we need to have declared the class first.
const arnold = new AwesomePerson('Arnold', 'Schwarzenegger', 74, 'be awesome'); // ERROR
class AwesomePerson {
}
class Person {
#name; // private fields can only be declared up-front in a field declaration
constructor(first, last, age) {
this.#name = {
first,
last
};
this.age = age;
}
get fullName() {
return `${this.#name.first} ${this.#name.last}`;
}
(...)
}
const person = new Person('Martha', 'Sully');
console.log(person.name); // undefined
class Person {
constructor(first, last, age) {
this.name = {
first,
last
};
this.age = age;
}
(...)
}
class Student extends Person {
constructor(...args) { // default constructor for subclasses, that will be created in case we don't declare it
super(...args);
}
};
The syntax above uses the spread operator.
Spread syntax can be used when all elements from an object or array need to be included in a list of some kind.
JavaScript programs started off as small scripting tasks, providing a bit of interactivity to web pages where needed.
However, nowadays we have complete applications being run in browsers with a lot of JavaScript, as well as JavaScript being used in other contexts.
With that came the need to think about mechanisms for splitting JavaScript programs into separate modules that could be imported when/where needed.
Node.js has supported modules for a long time.
There are a number of JavaScript libraries and frameworks that enable module usage:
Modern browsers have started to support module functionality natively.
As of ES6, JavaScript supports a native module format.
An ES6 module is a file containing JS code. There’s no special module keyword; a module mostly reads just like a script, except for two differences:
ES6 modules are automatically strict-mode code
We can use import and export in modules
Everything declared inside a module is local to the module, by default.
If we want something declared in a module to be public, so that other modules can use it, we must export that feature.
// EXPORT KEYWORD
export class Person {
(...)
}
// DEFAULT EXPORTS (one per module; when importing, name can be customized)
export default Person;
// EXPORT LISTS
export {
Person,
AwesomePerson
};
Import is used to pull items from a module into another script.
// IMPORT ALL FROM file.js
import * as PersonModule from 'file.js';
// IMPORT Person AND AwesomePerson FROM file.js
import { Person, AwesomePerson } from 'file.js';
// IMPORT DEFAULT FROM file.js
import bla from 'file.js';
Scripts that use modules must be loaded by setting a type="module" attribute in the <script> tag, if the execution environment is a browser.
If the execution environment is Node.js, then "type": "module" should be added to the package.json file.
<script type="module" src="./file.js"></script>
A dependency is a third-party bit of software that solves a problem for us.
A web project may have any number of dependencies, ranging from zero to many, and our dependencies might include sub-dependencies that we didn't explicitly install.
A package manager is a system that will manage our project's dependencies.
It will provide a method to install new dependencies, manage where packages are stored on our file system, and offer capabilities for us to publish our own packages.
npm is the default package manager for Node.js, although there are others (like Yarn, for example).
npm (short for Node Package Manager) is a popular package manager among JavaScript developers. It is automatically installed whenever we install Node.js on our system.
npm consists of three components:
Using npm
npm with dependencies: Jest