- Compiler and Execution Phase
- Hoisting
- Scope
- Prototype
- This
- Closure
- Map, Filter and Reduce
- Call, Apply and Bind
When compiled, the compiler will recursively go through every function checking for variable declarations. A Scope manager will also keep track of which Scope these variables are declared in.
var foo = 'bar'
function bar() {
var foo = 'baz'
}
function baz(foo) {
foo = 'bam'
bam = 'yay'
}
Compile Phase
- The 1st
foo
as a variable declaration inside theglobal
Scope - The 2nd
foo
as a variable declaration inside thebar()
Scope - The 3rd
foo
as a variable declaration inside thebaz()
Scope. Because it's a parameter of a function, it will still be declared as a variable.
Execution Phase (Lexical Scope)
- Line 1 will become simply
foo = 'bar'
during the execution phase as it’s been declared- It will see
foo
and ask the Scope managerHas the global Scope has ever heard of a variable called 'foo'?
- The Scope manager will say
Yes
and we get a reference to thatfoo
- It will see
- Functions will be ignored unless they were invoked elsewhere
- Same occurs again where we check the Scope of
baz()
for a variable calledfoo
and same result again
- Same occurs again where we check the Scope of
- 3rd function
- Since
foo
is a parameter ofbaz()
, it will seefoo
as a variable in thebaz()
Scope bam
doesn’t exist in this Scope, so it will move out to theglobal
Scope and see if it exists in theglobal
Scope- We come to the
global
Scope, and theglobal
Scope will sayYes, I just created 'bam' for you
(in strict mode, it will say it doesn’t exist and throw an error) - Note: This is how global leakage occurs in JavaScript, because
bam
is now a reference to a global variable or a variable declared somewhere outside of the Scope ofbaz()
- Since
Hoisting is the conceptual model for how JavaScript works. Using the literal meaning of the word, hoisting is used to explain how Variables and Function declarations are hoisted to the top of a function or a global scope. They are hoisted to the top of our code and declared during the compile phase to make sure they are available for reference at runtime. Note that the function expression itself is not hoisted, but the declaration of that function is.
var a = b();
var c = d();
a;
c;
function b() {
return c
}
var d = function() {
return b();
}
When compiled, the declarations are hoisted and effectively re-ordered like this:
// Function declarations are moved to the top
function b() {
return c;
}
// Variables are declared, but no value is assigned
var a;
var c;
var d;
// Declared variables are assigned values
a = b();
c = d();
a;
c;
d = function() {
return b();
}
Scope in JavaScript is where the compiler looks for variables and functions when it needs them. We know already that during the Compile Phase, JavaScript will looked at how variables are declared, but how about Scope?
Above, we said that there are two phases that the JavaScript interpreter will go through - Compile and Execution. During the Execution Phase is when variable assignments are made and functions are executed. Lexical Scope means compile-time scope, and so the Lexical Scope is the scope that was determined after the Compile Phase. Let's put this together in an example:
Initial code
'use strict'
var foo = 'foo'
var wow = 'wow'
function bar(wow) {
var pow = 'pow'
console.log(foo) // 'foo'
console.log(wow) // 'zoom'
}
bar('zoom')
console.log(pow) // ReferenceError: pow is not defined
After Compile Phase
'use strict'
// Variables are hoisted to the top of the current scope
var foo
var wow
// Function declarations are hoisted as-is at the top of the current scope
function bar(wow) {
// wow = 'zoom'
var pow
pow = 'pow'
console.log(foo) // 'foo'
console.log(wow) // 'zoom'
}
foo = 'foo'
wow = 'wow'
bar('zoom')
console.log(pow) // ReferenceError: pow is not defined
- Declarations are hoisted to the top of their current scope
wow
is also declared within the scope ofbar()
as it's a function parameter
Now for the Execution Phase. If we look at console.log(foo)
, the JS interpreter needs to find the declaration of foo
before it executes this line. Is foo
in the scope of bar()
? No, move to its parent scope. Is foo
in the global
scope? Yes. The interpreter will now execute this line. When executing console.log(pow)
, it can't find pow
anywhere, which is why we see a ReferenceError
.
Variables declared in a Function Scope cannot be accessed from outside the function. This is a very powerful pattern to create private properties and only have access to them from within a Function Scope.
A try/catch
statement will create a new Block Scope, and more specifically, only the catch
clause will make this scope.
'use strict'
try {
var foo = 'foo'
console.log(bar)
}
catch (err) {
console.log(err)
}
console.log(foo)
console.log(err)
In this example, foo
will work as expected, but err
will throw a ReferenceError
.
In ES6, let
and const
are also bound to the Block Scope they were declared in. This can be any block, whether it's an if
, for
or function
block. Before let
and const
, it would be extremely inconvenient to create "private" variables, but now we can enforce this quite easily. This does 2 things:
- Reduce the possibility of bugs, or difficult-to-understand bugs
- Allows the garbage collector to clean these variables once we're out of the Block Scope
IIFE
An IIFE (Immediately Invoked Function Expression) is a pattern that allows you to create a new Block Scope. They are function expressions that we invoke as soon as the interpreter runs through the function.
var foo = 'foo';
(function bar() {
console.log('in function bar');
})()
console.log(foo);
Above, the IIFE will be the first thing that we see logged. We just saw that function declarations are hoisted to the top by the interpreter, so how does this happen?
- Wrapping the function in
(function() { ... })
turns this into a function expression - Adding
()
to the end will immediately invoke that function expression
for (var i = 0; i < 5; i++) {
(function logIndex(index) {
setTimeout(function () {
console.log('index: ' + index)
}, 1000)
})(i)
}
IIFEs are effective at creating private scopes. If we didn't use an IIFE above, the output would be 5, 5, 5, 5, 5
. By the time we've waited 1000ms, the value of i
would be 5
, so each log would be 5
. With an IIFE, it will create a private scope with the correct value of i
with each iteration of the for
loop, and we get the desired output.
For people that come to JavaScript from other languages, seeing new
, class
or constructor
makes it clear to them how inheritance works as it's very similar to other languages. The problem with JavaScript is that these operators are just that, they are words which are there to help make you feel more comfortable. Under the hood, there is nothing familiar about how inheritance is happening. In JavaScript, Objects can be strange things.
Class-based languages
In class-based languages (Java, C++, C#, Python etc.), a class
and an instance
of that class are two distinctly different things. An instance will inherit all the properties and methods of the class, but generally cannot modify them or add others. By knowing where an instance comes from, you'll know exactly how it will behave.
Prototype-based languages
JavaScript doesn't have real classes, so inheritance is possible due to the prototype
Object. It does the following:
- An Object template from which we get the initial properties for a new Object
- Any Object can be used as a prototype
- The child Object can override the inherited methods/properties, which will not affect the prototype
- The prototype can change its attributes, or add new ones, which will affect the Object
So, the Object is just a copy of the prototype Object and the prototype doesn't really care about this copy, but the Object definitely cares about the prototype.
Prototype chain
const Vehicle = {
used_for_transport: true,
}
const Car = {
__proto__: Vehicle,
wheels: 4,
engine: 'diesel',
}
const Tesla = {
__proto__: Car,
engine: 'electric',
}
Using ES6, it's clear to see how the prototype chain is working. When we try to use our Tesla
Object, it will have all of the properties of both Car
and Vehicle
, and it will also have overwritten the engine
property inherited from Car
. What is the prototype of Vehicle
you might ask? Well, if it's not explicitly declared, the prototype will always be the global Object()
.
Any time you try to access an Object's attribute, JavaScript is looking through the prototype chain to find the value. If it eventually gets to the global Object()
and can't find the property, it will return undefined
.
We saw that for an Object, __proto__
defines the Object's prototype. Another property, prototype
, belongs only to functions. It's used to build the __proto__
when the function is used as a constructor with the new
operator.
function Car(name) {
this.name = name
}
const tesla = new Car('Tesla')
What's happening?
- An Object is created
- The
__proto__
of that Object is set toCar.prototype
- The function
Car
is called withthis
as the newly created Object
What does tesla
look like?
name
will be'Tesla'
__proto__
will return:constructor
which is the functionCar
__proto__
which is theprototype
ofCar
containing things such astoString
etc.
The can create as many Car
Objects as we want, and if we want to add a property to all of those Car
s we can do so. Car.prototype.wheels = 4
will set apply to all Car
Objects. This is all done using the prototype chain. Car
and tesla
have no direct reference to each other.
The JavaScript class
appeals to developers from OOP backgrounds, but it's essentially doing the same thing.
class Rectangle {
constructor(height, width) {
this.height = height
this.width = width
}
get area() {
return this.calcArea()
}
calcArea() {
return this.height * this.width
}
}
const square = new Rectangle(10, 10)
console.log(square.area) // 100
This is basically the same as:
function Rectangle(height, width) {
this.height = height
this.width = width
}
Rectangle.prototype.calcArea = function calcArea() {
return this.height * this.width
}
The getter
and setter
methods in classes bind an Object property to a function that will be called when that property is looked up. It's just syntactic sugar to help make it easier to look up or set properties.
Context
We can’t alter how lexical scoping in JavaScript works, but we can control the context in which we call our functions. Context is decided at runtime when the function is called, and it’s always bound to the object the function was called within. By saying, change the context, I mean, we’re changing what this actually is.
Every function, while executing, has a reference to its current execution context, called this
. (Execution context is when and how the function was called)
- Implicit binding
- Default binding
- Explicit binding
- New binding
To workout what this
is, we check the call-site and see which of these 4 rules applies, with new
taking the highest precedence.
Implicit Binding
The simplest way to think about implicit binding is, what's to the left of the dot? Doing this will more often than not tell you what this
is. See below, we say obj.foo()
. What's to the left of the dot? obj
. What is this.bar
? obj.bar
.
Does the call-site have a context Object? Also referred to as an owning or containing Object. this
will refer to the Object's properties.
var obj = {
bar: 1,
foo: function() {
console.log(this.bar)
}
}
obj.foo()
When obj.foo()
is called, the call-site will be the foo
function inside obj
. Does the call site have a context Object? Yes, so this.bar
will be 1
.
Default Binding
This is a plain function call, the most common scenario. More often than not it will refer to the global
Scope or some other outer Scope.
function foo() {
console.log(this.a)
}
var a = 2
foo() // 2
Explicit Binding
This is where .call()
, .apply()
or .bind()
is used at the call site to explicitly set the this
reference.
e.g.
.call()
=>fn.call(thisObj, fnParam1, fnParam2)
.apply()
=>fn.apply(thisObj, [fnParam1, fnParam2])
.bind()
=>const newFn = fn.bind(thisObj, fnParam1, fnParam2)
var foo = function() {
console.log(this.bar)
}
foo.call({ bar: 1 }) // 1
New Binding
Consider var a = new Foo()
. This will do the following:
- Create a new empty Object called
a
- Link the
a
Object to theFoo
Object, which contains aprototype
andconstructor
- This new
a
Object gets bound asthis
in thenew Foo()
call
function Foo(a) {
this.a = a
}
var bar = new Foo(2)
console.log(bar.a) // 2
By calling Foo(..)
with new
in front of it, we've constructed a new Object and set that new Object as this
for the call of Foo(..)
.
Nested functions
The only exception to these rules are nested functions like this:
const person = {
personName: "MyPerson",
personAge: 50,
printInfo: function() {
console.log('Name:', this.personName)
console.log('Age:', this.personAge)
nestedFunction = function() {
console.log('this', this);
console.log('Name:', this.personName)
console.log('Age:', this.personAge)
}
nestedFunction()
}
}
person.printInfo()
Using the logic above, let's look at printInfo
. First, we need to go back to the call-site of printInfo
and check what's happening. We can see that person.printInfo()
is invoking the function, and person
is an Object, so printInfo()
is going to use person
as this
through implicit binding. Great!
To understand what's happening when nestedFunction
is invoked, we need to recap the different ways of setting this
.
const obj = { value: 5 }
obj.fn(10)
fn.call(obj, 10)
fn.apply(obj, [10])
const newFn = fn.bind(obj)
newFn(10)
In all of the above examples, we are setting this
as obj
. Calling a function without a leading parent object will generally use the global
object (window
in browsers) as this
. This means that when we call nestedFunction()
, there is nothing to tell this function what this
is, and it will use global
as this
.
An easy way to solve this is to make sure we call nestedFunction
with the correct this
. We can do this by changing the line to nestedFunction.call(this)
, and this will work because when the function is invoked, this
will be taken from printInfo
, which in turn was invoked by person
thus setting this
as the person
Object.
Click to test yourself!
Copy the code below into your browser or any other workspace and try to guess the correct outputs.
const Factory = {
value: 0,
add: function(v) {
this.value += v
},
subtract: (v) => this.value -= v
}
// What is Factory.value?
console.log('Expected: ')
console.log(`Actual: ${Factory.value}\n`)
Factory.add(1)
// What is Factory.value?
console.log('Expected: ')
console.log(`Actual: ${Factory.value}\n`)
Factory.subtract(1)
// What is Factory.value?
console.log('Expected: ')
console.log(`Actual: ${Factory.value}\n`)
const myAdd = Factory.add
myAdd(2)
// What is Factory.value?
console.log('Expected: ')
console.log(`Actual: ${Factory.value}\n`)
// Can you explain the result?
const myOtherAdd = {
value: 10,
add: Factory.add,
}
myOtherAdd.add(5)
// What is Factory.value?
console.log('\nExpected: ')
console.log(`Actual: ${Factory.value}`)
// What is myOtherAdd.value?
console.log('Expected: ')
console.log(`Actual: ${myOtherAdd.value}`)
2ality
proposed a different take on this
now that we have arrow functions. In his take, he refers to () => {}
as a real function and function () {}
as an ordinary function.
Ordinary Functions
Here's an ordinary function:
function add(x, y) {
return x + y;
}
Every ordinary function has an implicit parameter this
, that is always pre-defined as undefined
. This makes these two lines pretty much the same:
add(3, 5)
add.call(undefined, 3, 5)
If you nest ordinary functions, this
from outer
is shadowed.
function outer() {
function inner() {
console.log(this) // undefined
}
console.log(this) // 'outer this'
}
outer.call('outer this')
Since inner
is also an ordinary function, it has its own this
and any this
outside of it has been hidden away.
Real Functions (arrow)
Here's an arrow function:
const add = (x, y) => {
return x + y
}
If you nest an arrow function inside an ordinary function, this
is not shadowed:
function outer() {
const inner = () => {
console.log(this) // outer this
}
console.log(this) // outer this
inner()
}
outer.call('outer this')
The scope of an arrow function is determined by when it was created. Above, it is created inside outer
, thus it will inherit this
from outer
.
function outer() {
const inner = () => this
console.log(inner.call('inner this')) // outer this
}
outer.call('outer this')
The this
of an arrow function cannot be influenced. Above, our inner
function is still behaving exactly the same as in the previous example, despite us trying to explicitly define its this
using inner.call()
. The arrow function was still created inside outer
, and will still inherit this
from outer
.
Ordinary Functions as Methods & the Dot Operator
Ordinary functions are typically used to define "methods" on an Object:
const obj = {
prop: function () {}
// Can also be written as:
// prop() {}
}
We can access properties of an Object using the dot operator (.
). The dot operator has 2 uses:
- Getting and setting properties using
obj.prop
- Calling methods using
obj.prop(x, y)
The latter is effectively the same as calling obj.prop.call(obj, x, y)
. Again, when using an ordinary function this`` is always explicitly defined. If we are just calling a regular function
fn(x, y), we can see there is no dot operator and thus we are basically calling
fn(undefined, x, y)as
this`.
Pitfalls of Using Ordinary Functions
callApi() {
getUsers()
.then(function () {
this.logStatus('Done')
})
}
In a callback like this, this.logStatus
will fail because this
is undefined
as we saw in the inner
& outer
examples.
callApi() {
getUsers()
.then(() => {
this.logStatus('Done')
})
}
As soon as we change it to an arrow function, it works. The arrow function is defined on-the-fly, which translates to it being defined inside callApi
and having a reference to its this
.
prefixUserNames(names) {
return names.map(function(name) {
return `${this.company}: ${this.name}`
})
}
Again, this will fail as this.company
will be undefined
. And again, with an arrow function it would be resolved.
Pitfall: Using Methods as Callbacks
Here's a component:
class Component {
constructor(name) {
this.name = name
const btn = document.getElementById('myButton')
btn.addEventListener('click', this.handleClick)
}
handleClick() {
console.log(`Clicked ${this.name}`)
}
}
This component should log this.name
when clicked, but what will actually happen is an error of this
being undefined in the method. Why? Well, when we call this.handleClick
we are effectively doing this:
const handler = this.handleClick
handler()
As we know already this is calling handler.call(undefined)
so of course it will fail. Here's the fix:
btn.addEventListener('click', this.handleClick.bind(this))
Alternatively, we could define handler
in the constructor
as this.handler = this.handleClick.bind(this)
, or write the handleClick
method as an arrow function handleClick = () {}
.
Closure is when a function "remembers" its lexical scope (compile time scope), even when the function is executed outside that lexical scope. Here's a simple example:
const a = 123
function foo() {
console.log(this.a)
}
During the compile phase, this function will see that it needs to refer to a variable called a
, and make a reference to the global a
we created. Now, it doesn't matter where the call site of this function is, it will always refer to this global a
variable.
Another example:
function foo() {
const bar = 'bar'
function baz() {
console.log(bar)
}
outerFn(baz)
}
function outerFn(baz) {
baz()
}
foo()
At the end of this long winded function, outerFn()
is invoking baz()
from completely outside of foo()
. If we tried to log bar
from inside outerFn
, we would not get 'bar'
.
baz()
is passed into outerFn()
, which "remembers" the lexical scope in which it was invoked. It "remembers" the lexical scope of foo()
, which gives it a reference to bar
.
Private Variables
Closures are also good for keeping some data private (creating Privileged Methods).
const secretFunction = (secret) =>
getSecret: () => secret
This getSecret
function has access to its parent's Scope, giving it access to secret
and any other variables created within secretFunction
. If we try to access secret
from outside of this secretFunction
, it will throw an error.
These three Array methods are various ways of looping through an Array and performing actions. These methods are far more readable than standard for
loops as there are less pieces of code to read. Although not always faster than for
loop, these functions personify Functional Programming as they don't mutate the original Array and have no side-effects.
Use when: You want to map all the elements of an Array to another value.
Example: Convert Farenheit temperatures to Celsius
const farenheight = [10, 55, 82, 43];
const celsius = farenheight.map(elem => Math.round((elem - 32) * 5 / 9);
What it does:
- Runs your function as a callback on every element of the Array from left to right
- Returns a new Array with the results
Parameters:
arr.map((elem, index, arr) => {
...
}, thisArg);
Parameter | Meaning |
---|---|
elem | Element value |
index | Index of the element, moving from left to right |
arr | The original Array used to invoke .map() |
thisArg | Optional Object that will be referred to as this in the callback function |
NOTE: .map()
is exactly the same as .forEach()
, except .forEach()
will directly mutate the Array. A .map()
function can also have other functions chained onto it, whereas a .forEach()
cannot.
NOTE: Also, a .forEach()
is generally used when you don't care about receiving a result from the looping and instead want to directly mutate the Array.
Use when: You want to remove unwanted elements from an Array.
Example: Remove numbers smaller than 10 from an Array.
const numbers = [1, 2, 50, 3, 100, 9];
const bigNumbers = numbers.filter((elem, index, arr) => elem > 10);
What it does:
- Runs your function as a callback on every element of the Array from left to right
- The return value must be a
Boolean
, identifying if the element should be kept or removed - Returns a new Array with the results
Parameters:
arr.map((elem, index, arr) => {
...
}, thisArg);
Parameter | Meaning |
---|---|
elem | Element value |
index | Index of the element, moving from left to right |
arr | The original Array used to invoke .filter() |
thisArg | Optional Object that will be referred to as this in the callback function |
Use when: You want to iterate over an array accumulating a single result. Also used when you want to perform multiple actions on each item as it is more efficient than chaining multiple actions (e.g. .filter(...).map(...)
).
Example: Get the sum of the elements in an Array
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, elem) => acc + elem, 0);
Example 2: First, get all the even numbers from an Array. Second, double these numbers. We could perform .filter()
and then .map()
, but it's more efficient to use .reduce()
. In this is example, it is basically performing as a more readable and functional for
loop.
const numbers = [1, 2, 3, 4];
const result = numbers.filter((elem, index, arr) => {
return elem % 2 === 0;
}).map((elem, index, arr) => {
return elem * 2;
});
// 228.181ms in the browser for 1,000,000 items
const result = numbers.reduce((acc, elem) => {
if (elem % 2 === 0) acc.push(elem * 2);
return acc;
}, []);
// 59.956ms in the browser for 1,000,000 items
What it does:
- Runs your function as a callback on every element of the Array from left to right
- The value returned from the callback is passed on to the next callback, until finally it is returned from the
.reduce()
Parameters:
reduce((acc, elem, index, arr) => {
...
}, initialAcc);
Parameter | Meaning |
---|---|
acc | (Accumulator) Stores the cumulative value returned from each callback |
elem | Element value |
index | Index of the element, moving from left to right |
arr | The original Array used to invoke .filter() |
initialAcc | Used as the initial value of the accumulator in the first callback |
This is me:
const person = {
firstName: 'Declan',
lastName: 'Elcocks'
};
This is a function to say hello to me:
function say(greeting) {
console.log(`${greeting} ${this.firstName} ${this.lastName}`);
}
Now we need a way to tell our say()
function that this
should be the person
Object we created previously. In comes .call()
, .apply()
and .bind()
:
.call()
invokes the function, and allows you to pass in arguments
one by one.
Example:
say.call(person, 'Hello');
person
will be used asthis
in the function'Hello'
will be used as thegreeting
parameter in the function. If the function accepts more than one parameter, it will also be separated by a comma.
.apply()
invokes the function, and allows you to pass in an Array of arguments
.
Example:
say.apply(person, ['Hello']);
person
will be used asthis
in the function'Hello'
will be used as thegreeting
parameter in the function. If the function accepts more than one parameter, it will go through the Array to assign the values of the parameters.
.bind()
allows you to create a completely new function with the this
of your choosing bound to it. Any arguments passed to .bind()
will also be set as the values for those parameters.
Example:
const sayHelloToDeclan = say.bind(person, 'Hello');
- Creates a new function
sayHelloToDeclan()
with theperson
Object bound to it asthis
'Hello'
will be set as the value for thegreeting
parameter
Example 2:
const multiply = (a, b) => a * b;
const multiplyByTwo = multiply.bind(this, 2);
- Creates a base function to multiply two numbers together
- Uses
.bind()
to create a new function withnum1
intialized as2
- Note:
this
still needs to be passed as the first argument when using.bind()
. In this case,this
will be set as theglobal
Object as it is the Scope we are in.
- Use
.call()
to pass in comma separated arguments withthis
- Use
.apply()
to pass in an Array of arguments withthis
.bind()
is useful for currying functions and re-using functions as templates as with themultiply()
example
Note: Currying is when you chain functions. With the multiplyByTwo()
example, it is basically a function which calls another function straight away, thus chaining the two together.