aka. Day 6 with the space heater
Howdy y'all, it's been a cold weak but at least my toes are warm. This here's a deep dive into the runtime speeds of javascript's different object creation methods.
While experimenting with coding styles while working on some Abstract Data Structures. lul basically none are implemented at the time of this post. I've been reading lots of JS docs on MDN. The docs I was reading had many big red warnings about the negative impacts of fussing around with Object prototypes. I also found there are so many ways to make an Object. I had previously been playing around with formatting, syntax, feel, and readability (arguably all subjective), when these MDN warnings provoked me to switch gears and write some code to test the speed of different Object instantiation techniques.
To test the speed of different Object instantiation techniques, I modeled the following little note, and then implemented it in a bunch of different ways. Some of the implementations varied due to what I've been calling "coding style", but they at least implemented the following data and behavior.
// NOTE
{
type: <string>
Value: <string>
}
// PROTOTYPE VARS
isNote = true
constructor = <function reference> (Only if an actual constructor was used)
// PROTOTYPE METHODS
updateValue = (value) -> <note referece>
describe = () -> <string "type: value">
// STATIC METHODS
isNote = (note) -> !!note.isNote
OOP should be pronounced OOP
SO, this is the classic constructor, Nothing to fancy. No deviation from the mocked up model above. But, Below you will find the testNoteConstructor
function. Notice this function is not a test, because it's not asserting anything about the Note. It's just a function that uses every feature of the note. This function will be used by timeTest
as the cb
arg so that we can time how long it takes to instantiate 1048576 NoteConstructor
notes and execute their methods.
function NoteConstructor(type, value){
this.type = type
this.value = value
}
NoteConstructor.isNote = function(note){
return !!note.isNote
}
NoteConstructor.prototype.isNote = true
NoteConstructor.prototype.describe = function(){
return this.type + ': ' + this.value
}
NoteConstructor.prototype.updateValue = function(value){
this.value = value
return this
}
let testNoteConstructor = () => {
let result = {}
let note = result.note = new NoteConstructor('todo', 'get eggs')
result.descriptionBeforeUpdate = note.describe()
note.updateValue('get ham')
result.descriptionAfterUpdate = note.describe()
result.check = NoteConstructor.isNote(note)
result.constructor = note.constructor
return result
}
Also Nothing fancy here, just an ES6 class. Notice here that is has its very one testNoteClass
that does the same things as testNoteConstructor
. Each implementation has its very own testImplementationName
function.
class NoteClass {
constructor(type, value){
this.type = type
this.value = value
}
describe(){
return this.type + ': ' + this.value
}
updateValue(value){
this.value = value
return this
}
static isNote(note){
return !!note.isNote
}
}
NoteClass.prototype.isNote = true
let testNoteClass = () => {
let result = {}
let note = result.note = new NoteClass('todo', 'get eggs')
result.descriptionBeforeUpdate = note.describe()
note.updateValue('get ham')
result.descriptionAfterUpdate = note.describe()
result.check = NoteClass.isNote(note)
result.constructor = note.constructor
return result
}
Nothin but a good ole' factory and a testNoteFactory
to keep it company.
function NoteFactory(type, value){
return new NoteConstructor(type, value)
}
NoteFactory.isNote = (note) => !!note.isNote
let testNoteFactory = () => {
let result = {}
let note = result.note = NoteFactory('todo', 'get eggs')
result.descriptionBeforeUpdate = note.describe()
note.updateValue('get ham')
result.descriptionAfterUpdate = note.describe()
result.check = NoteFactory.isNote(note)
result.constructor = note.constructor
return result
}
Note: I think functional programming (FP) is better for modeling behavior that works with data, not modeling data itself. Unless, like an abstract data type, the model is a behavior model. These notes are just data with dinky behavior, If I was building a notes app using FP. The data would only be data and not reference any of its behaviors(methods), like some of the following implementations do.
Notice that the following Note implementation is very different because the notes have no methods! The notes are just data. The updateValue
and describe
have become an external function that works with note-like objects. Also, the updateValue
equivalent does not mutate the original note, it returns a copy with an updated value. Although this implementation's "methods" do not mutate the note, they do not stop mutation from happening elsewhere.
let describeNoteFunctional = (note) => note.type + ': ' + note.value
let updateNoteValueFunctional = (value, note) => ({...note, value})
let NoteFunctional = ({type, value}) => ({type, value, isNote: true})
NoteFunctional.isNote = (note) => !!note.isNote
let testNoteFunctional = () => {
let result = {}
let note = result.note = NoteFunctional({type:'todo', value: 'get eggs'})
result.descriptionBeforeUpdate = describeNoteFunctional(note)
let updatedNote = result.updatedNote = updateNoteValueFunctional('get ham', note)
result.descriptionAfterUpdate = describeNoteFunctional(updatedNote)
result.check = NoteFunctional.isNote(note)
return result
}
This is very similar to the NoteFunctional
implementation, but it's using curried functions. Currying is amazing, and if you haven't given it a shot YOU SHOULD RIGHT NOW! Below are some great JS curry related resources.
let describeNoteFunctionalCurry = (note) => () => note.type + ': ' + note.value
let updateNoteValueFunctionalCurry = (note) => (value) => ({...note, value})
let NoteFunctionalCurry = ({type, value}) => ({type, value, isNote: true})
NoteFunctionalCurry.isNote = (note) => !!note.isNote
let testNoteFunctionalCurry = () => {
let result = {}
let note = result.note = NoteFunctionalCurry({type:'todo', value: 'get eggs'})
result.descriptionBeforeUpdate = describeNoteFunctionalCurry(note)()
let updatedNote = result.updatedNote = updateNoteValueFunctionalCurry(note)('get ham')
result.descriptionAfterUpdate = describeNoteFunctionalCurry(updatedNote)()
result.check = NoteFunctionalCurry.isNote(note)
return result
}
This is the same as NoteFunctional
but each time a note is returned it is first passed into Object.freeze()
witch will prevent the note from being mutated anywhere in the program.
let describeNoteFunctionalFreeze = (note) => note.type + ': ' + note.value
let updateNoteValueFunctionalFreeze = (value, note) => Object.freeze({...note, value})
let NoteFunctionalFreeze = ({type, value}) => Object.freeze({type, value, isNote: true})
NoteFunctionalFreeze.isNote = (note) => !!note.isNote
let testNoteFunctionalFreeze = () => {
let result = {}
let note = result.note = NoteFunctionalFreeze({type:'todo', value: 'get eggs'})
result.descriptionBeforeUpdate = describeNoteFunctionalFreeze(note)
let updatedNote = result.updatedNote = updateNoteValueFunctionalFreeze('get ham', note)
result.descriptionAfterUpdate = describeNoteFunctionalFreeze(updatedNote)
result.check = NoteFunctionalFreeze.isNote(note)
return result
}
Functional coding is my favz, but usually, I don't use it to model OOP, for the lulz I did it anyway.
JS often has OOP implementations of objects that sometimes have functional-esq methods for example array.map(cb)
. But this here... is the opposite, it's using the powers of closure to make a function return an OOP like object without the use of the new
keyword. AKA. THIS ONE IS WACK, and its got readability issues... for example I felt the need to write comments in the code to explain complexities. Basically, I feel like this one is not a good choice even if it's fast, but it might be fun for you to read through anyways.
let describeNoteFunctionalish = (note) => () => note.type + ': ' + note.value
let updateNoteValueFunctionalish = (note) => (value) => {
// this is not functional it is mutating the note and returning "self"
note.value = value
return value
}
let NoteFunctionalish = (type, value) => {
var state = {
type,
value,
isNote: true,
}
// methods have to be added after state is intialized inorder to have a closure
// wraped over state, as opposed to undefined
return {
...state,
describe: describeNoteFunctionalish(state),
updateValue: updateNoteValueFunctionalish(state),
}
}
NoteFunctionalish.isNote = (note) => {
return !!note.isNote
}
let testNoteFunctionalish = () => {
let result = {}
let note = result.note = NoteFunctionalish('todo', 'get eggs')
result.descriptionBeforeUpdate = note.describe()
note.updateValue('get ham')
result.descriptionAfterUpdate = note.describe()
result.check = NoteFunctionalish.isNote(note)
return result
}
This one is much more like the first three, in that it returns an object that has prototype methods with the same behaviors. However, I used Object.create()
to bring the note to life. On MDN when prototype warnings come up usually it refers you to Object.create()
as a good tool to use, BUT SPOILER ALERT ITS HELLLLLZA SLOW.
function NoteObjectCreate(type, value){
let prototype = {
isNote: true,
describe: function(){
return this.type + ': ' + this.value
},
updateValue: function(value){
this.value = value
return this
},
}
return Object.create(prototype, {
type: {
writable: true,
value: type,
},
value: {
writable: true,
value: value,
},
})
}
NoteObjectCreate.isNote = function(note){
return !!note.isNote
}
let testNoteObjectCreate = () => {
let result = {}
let note = result.note = NoteObjectCreate('todo', 'get eggs')
result.descriptionBeforeUpdate = note.describe()
note.updateValue('get ham')
result.descriptionAfterUpdate = note.describe()
result.check = NoteObjectCreate.isNote(note)
return result
}
THE LAST IMPLEMENTATION, PHIEAW!
This one is also similar to the first three, and MDN also points to Object.setPrototypeOf()
as a good way to set an object's prototype. ALSO, SPOILER ALERT ITS HELLLLZA SLOOOOOOWW!
function NoteSetPrototypeOf(type, value){
let result = { type, value}
let prototype = {
isNote: true,
describe: function(){
return this.type + ': ' + this.value
},
updateValue: function(value){
this.value = value
return this
},
}
return Object.setPrototypeOf(result, prototype)
}
NoteSetPrototypeOf.isNote = function(note){
return !!note.isNote
}
let testNoteSetPrototypeOf = () => {
let result = {}
let note = result.note = NoteSetPrototypeOf('todo', 'get eggs')
result.descriptionBeforeUpdate = note.describe()
note.updateValue('get ham')
result.descriptionAfterUpdate = note.describe()
result.check = NoteSetPrototypeOf.isNote(note)
return result
}
After hashing out all the Object creation implementations, I made a function testSpeed
for testing the speed of a synchronous javascript function. testSpeed
has the following args.
cb
- a function who's speed gunna' be testiterations
- the number of times cb
should get executed per run
1048576
-- just an arbitrarily large numberruns
- the number of times you want to collect how many milliseconds elapsed while executing cb
iterations
times.
10
let testSpeed = ({cb, runs=10, iterations=Math.pow(2, 20)}) => {
let state = {
runs,
iterations,
results: []
}
for (var t =0; t < runs; t++){
let startTime = performance.now()
for(var i = 0; i < iterations; i++){
cb()
}
state.results.push(performance.now() - startTime)
}
state.min = state.results.reduce((r, n) => Math.min(r, n))
state.max = state.results.reduce((r, n) => Math.max(r, n))
state.diference = state.max - state.min
state.totalTime = state.results.reduce((r, n) => r + n)
state.average = state.totalTime / 10
return state
}
This lil doodie runs each function through speedTest
and aggregates the results, then it returns an array of those results sorted by the average ms elapsed.
let runTest = () => {
return [
testNoteConstructor,
testNoteClass,
testNoteFactory,
testNoteFunctional,
testNoteFunctionalFreeze,
testNoteFunctionalCurry,
testNoteFunctionalish,
testNoteObjectCreate,
testNoteSetPrototypeOf,
].map(cb => {
console.log('testing', cb.name)
return {
name: cb.name,
testSpeedResults: testSpeed({cb, runs: RUNS_PER_TEST, iterations: ITTERATIONS_PER_RUN})
}
}).sort((a, b) => a.testSpeedResults.average - b.testSpeedResults.average)
}
From Chrome Version 71.0.3578.98 (64-bit) on a whizzbang fast box.
All of the following numbers are milliseconds per 1048576 invocations.
It takes me longer to do a write up than to write the code.
And ...
Object.freeze
with caution, It's an amazing tool and you should use it but If you're regularly doing something Millions of times it can have a substantial impact. So Use it with caution all day.Object.create
and Object.setPrototypeOf
are DEFINITELY not supposed to be used this way! They are also amazing tools, but they should be used with much consideration. For example, Object.create
can be used to create classic inheritance (I'm not a fan of inheritance but wat-evz), where Object.create
is called once per type of model and then never again. As opposed to Once per instantiation of a model.<3 Slug.