Mastering the Arcane Art of JavaScript-Mancy for C# Developers: On Summoning Servants and Critters, Or The Basics of Objects in JavaScript
The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.
The Very Basics of Objects in JavaScript
Hello JavaScriptmancer! It is time to get an introduction to the basics of objects in JavaScript. In this article you’ll learn the beauty of the object initializer and the nice improvements ES6 brings to objects. If you think that you already know this stuff, think twice! There are more than one surprise in this article and I promise that you’ll learn something new by the end of it.
Let’s get started! We’ll start by concentrating our efforts in the humble object initializer to provide some foundation that we can use later when we come to object-oriented programming in JavaScript and prototypical inheritance.
Objects it is!
Object Initializers (a.k.a. Object Literals)
You can experiment with all examples in this article directly within this jsBin or downloading the source code from GitHub.
The simplest way to create an object in JavaScript is to use an object initializer:
var critter = {}
You can add properties and methods to your object initializer as desired:
critter = {
position: { x: 0, y: 0 },
movesTo: function(x, y) {
console.log(this + ' moves to (' + x + ',' + y + ')')
this.position.x = x
this.position.y = y
},
toString: function() {
return 'critter'
},
hp: 40,
}
And, of course, if you call a method within the critter
object it behaves as you would expect from any good self-respecting method:
critter.moveTo(10, 10)
// => critter moves to (10,10)
As you saw in the introduction of the series, you can augment anyany object at any time with new properties:
As long as it is not frozen via Object.freeze
, which makes an object immutable to all effects and purposes.
critter.damage = 1
critter.attacks = function(target) {
console.log(
this + ' rabidly attacks ' + target + ' with ' + this.damage + ' damage'
)
target.hp -= this.damage
}
And use these new abilities to great devastation:
var rabbit = {
hp: 10,
toString: function() {
return 'rabbit'
},
}
critter.attacks(rabbit)
// => critter rabidly attacks rabbit with 1 damage
You can also use special characters as names for your properties by taking advantage of the []
(indexing) notation:
critter['sounds used when communicating'] = ['beeeeeh', 'grrrrr', 'tjjiiiiii']
critter.saysSomething = function() {
var randomPick = Math.floor(
Math.random() * this['sounds used when communicating'].length
)
console.log(this['sounds used when communicating'][randomPick])
}
critter.saysSomething()
// => beeeeeeh (random pick)
critter.saysSomething()
// => tjjiiiii (random pick)
As you can see in many of the examples above, you can use the this
keyword to reference the object itself and thus access other properties within the same object.
JavaScript Arcana: This in JavaScript
From my experience, this
is the biggest source of problems for a C# developer that moves to JavaScript. We are so accustomed to work with classes and objects in C#, to be able to blindly rely in the value of this
, that when we move to JavaScript where the behavior of this
is so completely undependable we explode in frustration and anger.
Since this
is such a big part of the JavaScript Arcana, I devote a whole article to demystifying it for you. For now, just remember that when calling a method on a object using the dot notation, like in critter.moveTo
, the value of this
is mostlymostly trustworthy.
I say mostly because if you have a this
keyword within a method and within a callback function then you are screwed (which I dare say is pretty common). But worry not! You’ll learn everything there is to learn about this
in the next article.
Getters and Setters
Getters and setters are an often overlooked feature within object initializers since ES5. They work exactly like C# properties and look like this:
var mouse = {
strength: 1,
dexterity: 1,
get damage() {
return this.strength * die20() + this.dexterity * die8()
},
attacks: function(target) {
console.log(
this +
' ravenously attacks ' +
target +
' with ' +
this.damage +
' damage!'
)
target.hp -= this.damage
},
toString: function() {
return 'mouse'
},
}
Notice the strange get damage()
function? That’s a getter and, in this case, it represents the read-only property damage
that is calculated from other two properties strength
and dexterity
:
mouse.attacks(rabbit)
// => mouse ravenously attacks rabbit with 19 damage
mouse.attacks(rabbit)
// =>
We can use a backing field to perform additional steps or validation in the same way we are familiar to in C#:
var giantBat = {
_hp: 1,
get hp() {
return this._hp
},
set hp(value) {
if (value < 0) {
console.log(this + ' dies :(')
this._hp = 0
} else {
this._hp = value
}
},
toString: function() {
if (this.hp > 0) {
return 'giant bat'
} else {
return 'a dead giant bat'
}
},
}
In this example we ensure that the _hp
of the giant bat cannot go below 0
:
mouse.attacks(giantBat)
// => "mouse ravenously attacks giant bat with 23 damage!"
// => "giant bat dies :("
console.log(giantBat.toString())
// => a dead giant bat
You may have noticed that I have created a couple of new objects for these two examples instead of augmenting my beloved critter
. Well, there was a reason for that. You cannot augment objects with getters and setters in the same way that you add other properties. In this special case, you need to rely in the Object.defineProperty
or Object.defineProperties
both methods also included in ES5. We will take a look at this two low level methods later in the series when we examine the mysteries of object internals. Let’s go back to object initializers!
Method Overloading
Method overloading within object initializers works just like with functions, so as we saw in the previous article, if you try to overload a method following the same pattern that you are accustomed to in C#:
var venomousFrog = {
toString: function() {
return 'venomous frog'
},
jumps: function(meters) {
console.log(this + ' jumps ' + meters + ' meters in the air')
},
jumps: function(arbitrarily) {
console.log(this + ' jumps ' + arbitrarily)
},
}
You’ll just succeed in overwriting the former jump
method with the latter:
venomousFrog.jumps(10)
// => venomous frog jumps 10
// ups we have overwritten a the first jumps method
Instead use any of the patterns that you saw in the previous article to achieve method overloading, for instance:
venomousFrog.jumps = function(arg) {
if (typeof arg === 'number') {
console.log(this + ' jumps ' + arg + ' meters in the air')
} else {
console.log(this + ' jumps ' + arg)
}
}
Which provides a naive yet functioning implementation of method overloading:
venomousFrog.jumps(10)
// => venomous frog jumps 10 meters
venomousFrog.jumps('wildly in front of you')
// => venomous frong jumps wildly in front of you
Creating Objects With Factories
Since creating one-off objects through object initializers can be tedious, particularly whenever you need more than one object of the same type, we often use factoriesfactories to encapsulate object creation:
or the new operator that we’ll see when we get to the OOP section of the series
function monster(type, hp) {
return {
type: type,
hp: hp || 10,
toString: function() {
return this.type
},
position: { x: 0, y: 0 },
movesTo: function(x, y) {
console.log(this + ' moves to (' + x + ',' + y + ')')
this.position.x = x
this.position.y = y
},
}
}
Once defined, we can just use it to instantiate new objects as we wish:
var tinySpider = monster('tiny spider', /* hp */ 1)
tinySpider.movesTo(1, 1)
// => tiny spider moves to (1,1)
var giantSpider = monster('giant spider', /* hp */ 200)
giantSpider.movesTo(10, 10)
// => giant spider moves to (10,10);
There’s a lot of cool things that you can do with factories in JavaScript. Some of them you’ll discover when you get to the OOP section where we will see an alternative to classical inheritance in the shape of object composition via mixins, but now let’s take a look at how to achieve data privacy.
Data Privacy in JavaScript
You may have noticed by now that there’s no access modifiers in JavaScript, no private
, public
nor protected
keywords. That’s because every property is public, that is, there is no way to declare a private property by using a mere object initializer. You need to rely on additional patterns with closures to achieve data privacy, and that’s when factories come in handy.
Imagine that we have the previous example of our monster
but now we don’t want to reveal how we have implemented positioning. We would prefer to hide that fact from prying eyes and object consumers so that if we decide to change it in the future, for a three dimensional representation, polar coordinates or who knows what, it won’t break any clients of the object. This is part of what I call intentional programming, every decision that you make, the interface that you build, the parts that you choose to remain hidden or public, represent your intentions on how a particular object or API should be used. Be mindful and intentional when you write code. Back to the monster
:
function stealthyMonster(type, hp) {
var position = { x: 0, y: 0 }
return {
type: type,
hp: hp || 10,
toString: function() {
return 'stealthy ' + this.type
},
movesTo: function(x, y) {
console.log(this + ' moves stealthily to (' + x + ',' + y + ')')
// this function closes over (or encloses) the position variable
// position is NOT part of the object itself, it's a free variable
// that's why you cannot access it via this.position
position.x = x
position.y = y
},
}
}
Let’s take a closer look to that example. We have extracted the position
property outside of the object initializer and inside a variable within the stealthyMonster
scope (remember that functions create scopes in JavaScript). At the same time, we have updated the movesTo
function, which creates its own scope, to refer to the position
variable within the outer scope effectively creating a closure. Because position
is not part of the object being returned, it is not accessible to clients of the object through the dot notation. Because the movesTo
becomes a closure it can access the position
variable within the outside scope. In summary, we got ourselves some data privacy:
var darkSpider = stealthyMonster('dark spider')
console.log(darkSpider.position)
// now position is completely private
// => undefined
darkSpider.movesTo(10, 10)
// => stealthy dark spider moves stealthily to (10,10)
ES6 Improves Object Initializers
ES6 brings some improvements to object initializers that reduce the amount of code needed to create a new object. For instance, with ES6 you can declare methods within objects using shorthand syntax:
let sugaryCritter = {
position: { x: 0, y: 0 },
// from movesTo: function(x, y) to...
movesTo(x, y) {
console.log(`${this} moves to (${x},${y})`)
this.position.x = x
this.position.y = y
},
// from toString: function() to...
toString() {
return 'sugary ES6 critter'
},
hp: 40,
}
sugaryCritter.movesTo(10, 10)
// => sugary ES6 critter moves to (10, 10)
With shorthand notation you can skip the function
keyword and collapse the parameters of a function directly after its name. Additionally, when we create an object using existing variables we can take advantage of the same shorthand syntax for properties:
function sugaryStealthyMonster(type, hp = 10) {
let position = { x: 0, y: 0 }
return {
// with property shorthand we avoid the need to repeat
// the name of the variable twice (type: type)
type,
hp,
toString() {
return `stealthy ${this.type}`
},
movesTo(x, y) {
console.log(`${this} moves stealthily to (${x},${y})`)
position.x = x
position.y = y
},
}
}
let sugaryOoze = sugaryStealthyMonster('sugary Ooze', /*hp*/ 500)
sugaryOoze.movesTo(10, 10)
// => stealthy sugary Ooze moves stealthily to (10,10)
Finally, with the advent of ES6 you can use any expression as the name of an object property. That is, you are no longer limited to normal names or using the square brackets notation that handles special characters, from ES6 on you’ll be able to use any expression and the JavaScript engine will evaluate it as a string (with the exception of ES6 symbols which we’ll in the next section). Take a look at this:
let theArrow = () => 'I am an arrow';
let crazyMonkey = {
// ES5 valid
name: 'Kong',
['hates!']: ['mario', 'luigi'],
// ES6 computed properties
[(() => 'loves!')()]: ['bananas'],
[sugaryOoze.type]: sugaryOoze.type
// crazier yet
[theArrow]: `what's going on!?`,
}
In this example you can appreciate how any expression is valid. We’ve used the result of evaluating a function (() => 'loves!')()
, a property from another object sugaryOoze.type
and even an arrow function theArrow
. If you inspect the object itself, you can see how each property has been intrepreted as a string:
console.log(crazyMonkey)
// => [object Object] {
// function theArrow() {
// return 'I am an arrow';
// }: "what's going on!?",
// hates!: ["mario", "luigi"],
// loves!: ["bananas"],
// name: "Kong",
// sugary Ooze: "sugary Ooze"
// }
And you can retrieve them with the []
(indexing) syntax:
console.log(crazyMonkey[theArrow])
// => "what's going on!?"
Use cases for this particular feature? I can only think of some pretty far-fetched edge cases for dynamic creation of objects on-the-fly. That, and using symbols as property names wich brings us to ES6 symbols and how to simulate data privacy in JavaScript with them.
ES6 Symbols and Data Privacy
ES6 Symbols offer us a new approach to data privacy in addition to using closures. Symbols are a new type in JavaScript conceived to represent constants and be used as identifier for object properties or, as stated in the spec, the set of all non-string values that may be used as the key of an object property symbol-spec. They are immutable and can have a description associated to them.
that’s from the one and only JavaScript specification ECMA-262 (http://bit.ly/es6-spec-symbols)
Symbols can be created using the Symbol
function:
let anUndescriptiveSymbol = Symbol()
console.log(anUndescriptiveSymbol)
// => [object Symbol]
console.log(typeof anUndescriptiveSymbol)
// => symbol
console.log(anUndescriptiveSymbol.toString())
// => Symbol()
You can add a description to a Symbol for easier debugging since the toString
method displays the description:
// you can add a description to the Symbol
// so you can identify a symbol later on
let up = Symbol('up')
console.log(up.toString())
// => Symbol(up)
Each symbol is unique and immutable, so even if we create two symbols with the same description, they’ll be two completely different symbols:
// each symbol is unique and immutable
console.log(`Symbol('up') === Symbol('up')?? ${Symbol('up') === Symbol('up')}`)
// => Symbol('up') === Symbol('up')?? false
Because properties that use a Symbol as name (or key) can only be accessed by a reference to that Symbol (the very same Symbol used to identify the property), if you don’t expose that Symbol to the outer world you have provided yourself with data privacy. Let’s see how symbols and data privacy via symbols work:
function flyingMonster(type, hp = 10) {
let position = Symbol('position')
return {
[position]: { x: 0, y: 0 },
type,
hp,
toString() {
return `stealthy ${this.type}`
},
movesTo(x, y) {
console.log(
`${this} flies like the wind from (${this[position].x}, ${
this[position].y
}) to (${x},${y})`
)
this[position].x = x
this[position].y = y
},
}
}
let pterodactyl = flyingMonster('pterodactyl')
pterodactyl.movesTo(10, 10)
// => stealthy pterodactyl flies like the wind from (0,0) to (10,10)
Since we don’t have a reference to the symbol because it is scoped within the flyingMonster
function, we cannot access the position property:
console.log(pterodactyl.position)
// => undefined
And because each symbol is unique we cannot access the property using another symbol with the same description:
console.log(pterodactyl[Symbol('position')])
// => undefined
If everything ended here the world would be perfect, we could use symbols for data privacy and live happily ever after. However, there’s a drawback: The JavaScript Object
prototype provides the getOwnPropertySymbols
method that allows you to get the symbols used as properties within any given object. This means that after all this trouble we can access the position property by following this simple procedure:
var symbolsUsedInObject = Object.getOwnPropertySymbols(pterodactyl)
var position = symbolsUsedInObject[0]
console.log(position.toString())
// => Symbol(position)
// Got ya!
console.log(pterodactyl[position])
// => {x: 10, y: 10}
// ups!
So you can think of Symbols as a soft way to implement data privacy, where you probably give a stronger intent to your code, but where your data is not truly private. This limitation is why I still like using closures over Symbols.
Concluding
In this article you learned the most straightforward way to work with objects in JavaScript, the object initializer. You learned how to create objects with properties and methods, how to augment existing objects with new properties and how to use getters and setters. We also reviewed how to overload object methods and ease the repetitive creation of objects with factories. We wrapped factories with a pattern for achieving data privacy in JavaScript through the use of closures.
You learnt about the small improvements that ES6 brings to object initializers with the shorthand notation for both methods and properties. We wrapped the article with a review of the new ES6 Symbol type and its usage for attaining a soft version of data privacy.
Do Some Research!
- ES6 Symbols in the ECMA-262 Specification
- Nikolas Zakas book on principles of object oriented programming in JavaScript
Interested in Learning More JavaScript? Buy the Book!
Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.
Have a great week ahead!! :)
More Articles In This Series
- Chapter 0: The Basic Ingredients of JavaScriptMancy
- Chapter 1: The Many a One JavaScript Quirks
- Functions
- Object Oriented Programming
- Basics
- ES6
- OOP
- Introduction to OOP in JavaScript for C# Developers
- Summoning Fundamentals: Introduction to OOP In JavaScript – Encapsulation
- Summoning Fundamentals: Introduction to OOP In JavaScript – Inheritance
- Summoning Fundamentals: Introduction to OOP In JavaScript – Polymorphism
- White Tower Summoning: Mimicking C# Classical Inheritance in Javascript
- White Tower Summoning Enhanced: The Marvels of ES6 Classes
- Black Tower Summoning: Object Composition with Mixins
- Safer JavaScript Composition With Traits and Traits.js
- JavaScript Ultra Flexible Object Oriented Programming with Stamps
- Object Oriented JavaScript for C# Programmers
- Tooling
- Data Structures
- Functional Programming
- And more stuff to be shaped out:
- Functional Programming with JavaScript
- Modules
- Asynchronous Programming
- The JavaScript Ecosystem
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.