There are several ways to create or initialize an object.
const myObj = {
name: 'Peter'
};
console.log(myObj);
// { name: 'Peter' }
We can use literal notation
to initialize an object and the dot notation
to add properties.
// literal notation
const myObj = {};
// dot notation
myObj.name = 'Peter';
console.log(myObj);
// { name: 'Peter' }
const myObj = new Object();
myObj.name = 'Peter';
console.log(myObj);
// { name: 'Peter' }
We pass an existing object as the prototype of the new object or null
const myObj = {};
myObj.name = 'Peter';
const mySubObj = Object.create(myObj);
console.log(mySubObj);
// {}
console.log(mySubObj.name);
// Peter
... and we can check if myObj
is the prototype of mySubObj
console.log(Object.getPrototypeOf(mySubObj) === myObj)
// true
Note: Later we will see what prototype means.
Literal notation is faster and less verbose and it should be your first option.
Now, let's say that you need to create several objects with "the same shape" or properties.
You could do...
const peter = {
name: 'Peter',
greeting() {
console.log(`Hello, I'm ${this.name}`)
}
}
const wendy = {
name: 'Wendy',
greeting() {
console.log(`Hello, I'm ${this.name}`)
}
}
const tinkerbell = {
name: 'Tinkerbell',
greeting() {
console.log(`Hello, I'm ${this.name}`)
}
}
console.log(peter);
// { name: 'Peter', greeting: [Function: greeting] }
peter.greeting();
// Hello, I'm Peter
... or, use a regular function
to construct your objects.
function createCharacter(name) {
const newCharacter = {};
newCharacter.name = name;
newCharacter.greeting = function greeting() {
console.log(`Hello, I'm ${this.name}`);
}
return newCharacter;
}
const peter = createCharacter('Peter');
const wendy = createCharacter('Wendy');
const tinkerbell = createCharacter('Tinkerbell');
console.log(peter);
// { name: 'Peter', greeting: [Function: greeting] }
peter.greeting();
// Hello, I'm Peter
Note: In pre es2015
implementations you will see greeting: function greeting() {}
instead of greeting() {}
. Check the transpiled code into pre es2015
in babel
This is better (aka, cleaner/DRYer) than the previous snippet, but... We are still creating the same function and adding it as a method on the object.
And here is where the "OOP" or JS Prototypal Inheritance shines.
We are going to abstract the common logic into a new object
and keep our function
to initialize the characters objects. It sounds fairly simple, no?
But, how are we going to let each character object know where to find the abstracted functionality and link it at runtime to the "caller object"? Voila, we will create the characters objects with the functionality object as their prototypes using Object.create(objectPrototype)
instead of an object literal
.
const characterFunctionality = {
greeting: function greeting() {
console.log(`Hello, I'm ${this.name}`);
}
};
function createCharacter(name) {
const newCharacter = Object.create(characterFunctionality)
newCharacter.name = name;
return newCharacter;
}
const peter = createCharacter('Peter');
const wendy = createCharacter('Wendy');
const tinkerbell = createCharacter('Tinkerbell');
console.log(peter);
// { name: 'Peter', greeting: [Function: greeting] }
peter.greeting();
// Hello, I'm Peter
What is going on...?
At a high level (we will see this in more detail) when we call the method greeting()
on the object peter
, the JS interpreter tries to find it on the own object (peter), but, since that method doesn't exist, it will go UP on the prototype chain trying to find that method in the next prototype.
When we created our objects, we linked them to characterFunctionality
, and, that link resides in a hidden property: __proto__
.
Until now, we have being using regular functions
to create or initialize our objects. However, we can add the usage of the new keyword
to simplify some of the initialization process deferring to it (the new keyword
, allow me the alliteration) the workload.
Before seeing an example, let's state what new
is going to do for us:
- Creates a blank object
- Sets the prototype bind
- Sets this as the newly object: {}
- Returns
function CreateCharacter(name) {
this.name = name;
}
const peter = new CreateCharacter('Peter');
console.log(peter);
// CreateCharacter { name: 'Peter' }
Sample object
const character = {
name: 'Peter',
lastName: 'Pan',
greeting: (yourName) => `Hello, ${yourName}`
}
console.log(
character.greeting('Wendy')
)
Quick note about THIS:
We are going to address this
keyword in other section. But for the moment, and to keep the sight in OOP
, just remember... Arrow functions close over this
of the lexically enclosing context.
So, if you need to access to a property of your object, use regular functions.
Examples:
The context of our arrow function is the Global/Window object; the context of our "regular" function is the base object.
const name = 'Global-Peter'
const character = {
name: 'Scoped-Peter',
arrowGreeting: () => {
return `My name is ${name}`
},
funcGreeting() {
return `My name is ${this.name}`
}
}
console.log(
character.arrowGreeting(),
character.funcGreeting()
)
const myObj = {}
myObj.someProperty = 123
myObj['property with special chars'] = 'abc'
myObj.someMethod = function() {
return 'Hi!'
}
console.log(myObj.someProperty)
// 123
console.log(myObj['property with special chars'])
// abc
console.log(myObj.someMethod())
// Hi!
A few notes before jumping to the next topic:
- You can take advantage of named function to improve your debugging work (instead of anonymous functions). Here will not have much sense, but when you are going through stack traces it could make thing clearer and easier to understand or debug.
myObj.someMethod = function theFunctionName() {
return 'Hi!'
}
- Feel free to use ES2015
myObj.someMethod = () => {
return 'Hi!'
}
- Since we are just returning, if you want to have a "cleaner" code you can do:
myObj.someMethod = () => 'Hi!'
const myObj = {}
myObj.someProperty = 123
delete myObj.someProperty
console.log(myObj)
Both previous methods to ADD
and DELETE
properties mutate the object
.
In JavaScript we have primitive
(string, number, boolean, etc) and reference types
(objects and its special object type, array)
Primitives are immutable while reference types are mutable.
Let's see some examples to illustrate the previous statements. They will also help us to start taking scopes
into consideration.
var
holding aprimitive
value and afunction updating
(or mutating) the value of that variable
var primitive = 123
function updt(n) {
primitive = n
}
updt(1)
console.log(primitive)
// 1
console.log(window.primitive)
// 1
Some considerations...
When we use var
, the scope is going to be global/window or the function within its declared.
If we add the var statement
inside the function we will have a totally different result, since now, we have 2 primitive variables, one scoped to the global object (in the browser, window
) and the other to the function updt
var primitive = 123
function updt(n) {
var primitive = n
return n
}
updt(1)
console.log(window.primitive)
// 123
console.log(updt(1))
// 1
Remember, since our variable is scoped to the updt function
, every other function
within this one will have access to it.
var primitive = 123
function updt(n) {
var primitive = n
return function insideUpdt() {
return primitive
}()
}
updt(1)
console.log(console.log(updt(1)))
Note: Here we are returning what the invocation of insideUpdt()
returns, that's why we have the extra ()
. And yes, we could totally avoid naming our function (aka, use an anonymous function) but, for debugging purposes, named function always help.
var
holding aprimitive
value and anif statement
re-declaring that variable and assigning a new value
var primitive = 123
// same as if(true)
if(1) {
var primitive = 456
}
console.log(primitive)
// 456
Remember var
is NOT scoped at the block-level
.
We are re-declaring our primitive
variable (doable with var
) and assigning a new value.
BTW, remember that JS is going to process variable declarations before execution (aka, hoisting
). So, the engine will do the following:
var primitive = 123
primitive = 456
Now... What happens if we replace the var
key with let
...?
let primitive = 123
// same as if(true)
if(1) {
let primitive = 456
}
console.log(primitive)
// 123
As you probably guessed, for everything inside the "if
scope" the value of primitive
is going to be 456
. For the rest, it will be 123
. We are going to obtain "similar" results with const
, with the difference that once declared and assigned the value, we won't be able to mutate it.
if(1) {
const primitive = 456
primitive = 789
console.log(primitive)
}
Result: TypeError: Assignment to constant variable.
One more thing to remember... Global constants (let
and const
) do not become properties of the window object, unlike var variables.
var global = 1
let notGlobal0 = 2
const notGlobal1 = 3
console.log(
`
${window.global}
${window.notGlobal0}
${window.notGlobal1}
`
)
Result:
1
undefined
undefined
const
holding areference type
value and afunction updating
(or mutating) the data source (original variable)
const referenceValue = [1,2,3]
function addNumberToArray(arr, num) {
arr.push(num)
return arr
}
console.log(addNumberToArray(referenceValue, 4))
// [ 1, 2, 3, 4 ]
console.log(referenceValue)
// [ 1, 2, 3, 4 ]
We have seen mutations
(and immutability patterns) in several sections. But, it never hurts to refresh the memory.
Look at the snippet above...? Is the return of addNumberToArray()
what you were expecting...?
If not, let's make a quick go-through.
We declared a constant
holding an array
(of numbers) as value.
We declared a function
which receives as arguments an array
and a number
. This function pushes the number as an item into the passed array (aka, it adds the item to the end of the array); then, it returns its value.
However... When we are logging both, the return of our function and our original constant, we can see that both are returning the "same value".
First feeling: Didn't we say we cannot reassign the value of a constant
?
Yes... Technically, we are not changing its value if not altering one of its properties (same for item's arrays)
This reassigns the values and produces the expected ERROR
const numbers = [1,2,3]
numbers = [1,2,3,4]
const names = {
dad: 'Peter',
son: 'Pan'
}
names = {
dad: 'Peter',
son: 'Pan',
sister: 'Wendy'
}
Result: TypeError: Assignment to constant variable.
Now, this doesn't reassign. It just update the property/list.
const numbers = [1,2,3]
numbers.push(4)
const names = {
dad: 'Peter',
son: 'Pan'
}
names.sister = 'Wendy'
console.log(numbers)
// [ 1, 2, 3, 4 ]
console.log(names)
// { dad: 'Peter', son: 'Pan', sister: 'Wendy' }
From Mozilla's docs...
The const declaration creates a read-only reference to a value. It does not mean the value it holds is immutable—just that the variable identifier cannot be reassigned. For instance, in the case where the content is an object, this means the object's contents (e.g., its properties) can be altered.
Second feeling: Why did the original constant change...?
Maybe you tried to declare a variable within the function, assigned as its value the array
we are passing and then, performed the operation...
function addNumberToArray(arr, num) {
const newReferenceValue = arr
newReferenceValue.push(num)
return newReferenceValue
}
Yet, the output is still the same.
And this is because we are dealing with reference types
.
Objects (and arrays) are reference types... This means that every time we perform an operation over the reference, we are actually affecting the original object itself.
Time to see some basic examples:
Here we are declaring a constant with an object as value. That object is created using literal notation
Then, we declare a new constant which points to the previous one.
That why when we compare both, the result is true: they refer to the same object.
const character = {
name: 'Peter',
latName: 'Pan'
}
const refToObj = character
console.log(refToObj)
console.log(character)
// { name: 'Peter', latName: 'Pan' }
// { name: 'Peter', latName: 'Pan' }
console.log(character === refToObj)
// true
Here we have 2 objects with the same keys and values. When we compare both, the result is false
const peter1 = {
name: 'Peter',
latName: 'Pan'
}
const peter2 = {
name: 'Peter',
latName: 'Pan'
}
console.log(peter1)
console.log(peter2)
// { name: 'Peter', latName: 'Pan' }
// { name: 'Peter', latName: 'Pan' }
console.log(peter1 === peter2)
// false
This is because comparisons between objects is by reference
(not value), and, as you can see, each constant (object) refers to itself.
DO NOT do the following. Take it just as an illustration
What happens if we convert into strings both object before comparing them...
console.log(JSON.stringify(peter1) === JSON.stringify(peter2))
// true
... since string comparisons are by value
, both strings are equal.