DEV Community

Cover image for Unpacking JavaScript 01: Prototypes(--emulating proto runtime--)
sk
sk

Posted on

Unpacking JavaScript 01: Prototypes(--emulating proto runtime--)

previous article

Prototype based Language

The prototype pattern is a creational design pattern in software development. It is used when the type of objects to create is determined by a prototypical instance, which is cloned to produce new objects - wikipedia

JavaScript like any programming language conforms to a specific design principle/pattern, as java is built or designed on top of or around the classes paradigm and subsequent patterns, so is JavaScript with prototypes. prototype?, simply an object to create other objects, think of a template with predefined values and functionality, used to create other templates, say augmented/more functional templates.

This inevitably means if you were to take a high level concept in JS, say arrays and drill down a level deeper/behind the scene, you will come across an object or objects, which in them have other object(s) and so on until you reach the end or null.

a chain of sort, depicting objects building on top of each other to form more sophisticated objects which by consequence inherit values and properties exposed by objects(templates) lower in the chain. You've probably come across a statement like this or similar "everything in JavaScript is an Object" || "almost everything in JavaScript is an Object" or better yet "Nearly everything in JavaScript is an object other than six things that are not objects which are — null , undefined , strings, numbers, boolean, and symbols - These are called primitive values or primitive types" - everything about JS objects, to carry such statement with confidence you have to know more than JavaScript the language but the inner workings of JS, the runtime etc. I am feeling a bit bold at the moment I want to come with my own argument and try to defend it: "everything in JS is an object", bold isn't it, let's see how quickly it crumbles(note: arguments are not ultimate truths but paths to said truth, you can fairly well come up with your own and are an excellent way to learn), the only counter argument to this statement are primitive values even "normal functions", at first glance they don't act or even look like objects I guess my job is to prove otherwise.




// primitives

const foo = "bar"

const number = 80

const bool = true

const arr = []

// etc






function hello(){




}





Enter fullscreen mode Exit fullscreen mode

To prove if "anything" is "something" we need to know what "something" is, it's essence, basic building blocks, identity etc. In this case anything is everything in the JS language and something is an object.

"metaphyics" of an object

anything "dot operable" is an object


let value = {

    name: "sk",
   print(){
      console.log(this.name)
   }
}

value.print() // value is said to be dot operable


Enter fullscreen mode Exit fullscreen mode

an object is a structure with key(s) and value(s) pairs, to access said value(s) you use the keys via a dot or similar approved methods




value.name // dot operation
value[name] // similar approved methods - same as dot operation access a value thru key

Enter fullscreen mode Exit fullscreen mode

therefore anything that is dot operable or uses similar approved methods is an object.
hence if we see a dot operation or similar appr. methods, the assumption is, the left hand side of that dot is an object

if the above is accepted, anything dot operable etc is an object, I put to you then that at runtime JS primitive values are dot operable, therefore even primitive values are objects


foo.split()   // dot operation

number.toString()

arr.forEach()

Enter fullscreen mode Exit fullscreen mode

Then it is safe to conclude that during runtime primitive values somehow turn to objects, if they didn't, to operate on them we would use something of sort:


split(foo)

toString(number)

forEach(arr)

Enter fullscreen mode Exit fullscreen mode

but the fact that we are able to access these functions directly from a primitive value means it must be an object, encapsulating the value(data) and functionality in itself as key and value pairs.




foo = {

 value: "bar",

 toString(){



 }

 //etc



}



Enter fullscreen mode Exit fullscreen mode

I did skip the bool primitive, yes good catch, and null also undefined, to be fair I've never use bools in form of objects before, but they can be dot operable therefore fit the definition:


let l = new Boolean()
l.valueOf() // false

let f = true
f.valueOf() // true


Enter fullscreen mode Exit fullscreen mode

as for null and undefined won't even try I have nothing, they are a total mystery to me, won't even consult MDN, which is probably a reason enough to reject the everything argument and go with almost everything is an object. Well was this detour useless? not really wanted to show that coding is not just about actual coding but thinking about concepts, that's how you get good at stuff, form opinions about concepts you know and interact with, these detours can teach a lot and proving stuff or at least trying to is fun, it's not about being right but learning, you will see in later articles we will make an assumption about how a fixed array or arrays in general works and build one, our assumption is based on our knowledge and interactions with arrays, that is how you level up.

Not dwelling on proving whether or not everything is an object, the focus is on prototypes and the inheritance they provide via the chain they form, called prototype chain.

To understand any low level concept, especially concepts that are hidden behind abstractions, although prototypes are not that hidden and can be accessed and changed, by low level I mean most developers generally do not interact with them, or they are abstracted away, for example in JS classes using the "extends" keyword abstract prototypical inheritance away from the developer, to understand such concepts a simulation/emulation or interpretation of what happens during runtime helps to grasp the concept better. Since prototypes deal with or are objects, to understand them we have to simulate how the JS engine supposedly handles and deal with objects, in short: building the object runtime as an attempt to understand prototypes and prototypical inheritance.

Object Runtime

Part 1

"Key value pairs" is an intuitive or go to answer when asked for a simple explanation of objects, I propose a simpler explanation: an object is a structure that allows "setting" and "getting" values using a key. Therefore the job of the object runtime is to take an object and a key, look for the value associated with key, and perform an action, a set or get.


const obj = {

 name: "john",

 surname: "doe"



}



obj.name  = "jane"   // object -> obj, key -> name, action -> set (denoted by equal)

obj.surname          // object -> obj, key -> surname, action -> get(return)




Enter fullscreen mode Exit fullscreen mode

we can easily emulate the get action compared to the set, for set we need some lower level concepts(metaprogramming), good news emulating the get operation is enough to drive the point home,

custom get function:




// emulate the get operation on objects

function get(obj, key){

 let type = typeof obj[key] // this is a literal object access, the actual engine(object runtime) already knows the type, for us there's no better way to know the type, so ignore the access and focus on the type 



     switch (type) {

         case "function":

              obj[key]()  // if type is fn call it

              break;

         default:

             return obj[key] // else return the valuebeing accessed

             break;

         }



}

Enter fullscreen mode Exit fullscreen mode

first we get the type of the value being accessed, object's can hold various types including functions, if it's a function the assumption is: it's being called, in-fact that is the only case we can handle without resorting to unnecessary methods. to handle both function calls and getting a reference to a function, we could use a string "function()" and "function" as a key to denote a reference or a call in get, which is not that necessary in our case, but that is one way to solve the function problem(being callable and a value at the same time).

if the value being accessed is a function, get calls the function, else the value is returned(any other value other than functions)


  // usage example

let ob = {

     name : "John",

     surname: "Doe",

     print: function() {

         console.log(this.name, " ", this.surname)

     }



}




console.log(get(ob, "name")) // -> John


get(ob, "print") // calling the print function -> John Doe




Enter fullscreen mode Exit fullscreen mode

we have a working get runtime, all we need to know now to implement prototypes, is the role of prototypes(chain of objects) during a get operation, MDN says something along these lines, when you access an object's property(using a key) the JS engine, firstly looks for that property in the object's own properties, if it does not find it, goes down the chain and look for the value in the object's prototype(2nd level in the chain), if the same thing happens goes further down and look for the value in the object's prototype prototype( 3rd level : remember the chain we talked about earlier) until the property is found or null is reached, if null is reached(end of the chain) then a does not exist error is thrown meaning the value accessed does not exist, remember a prototype is just an object, which also has a prototype object it builds from or inherits from(according to our earlier explanation until a null is reached).

example:




let ob = {  // object ob

    name: "Jane",  // ob own property



 prototype: {  // ob prototype 

       surname: "Doe"



         prototype: { // ob prototype's prototype

             age: -99



             prototype: null  // ob prototype's prototype's prototype

         }



  }




}



// '->' below denotes next step the engine/runtime takes

 //(how the engine asks or handles objects)

// accessing name 
 ob.name

// locate ob in memory -> is name a property of ob? -> true -> return name

// accessing surname 

ob.surname

// locate ob in memory -> is surname a property of ob? false -> does ob have a proto? -> true -> is surname in ob's prototype? -> true -> return surname



// accesing a value that does not exist: status
ob.status

/* locate ob in memory -> is status a property of ob? -> false 
-> does ob have a proto? -> true -> is status in ob's proto? -> false -> does ob's proto have a proto? -> true -> 
does ob's proto proto have status? false -> does ob's proto proto have a proto? -> false -> return throw new error("status does not exist on object ob")*/


Enter fullscreen mode Exit fullscreen mode

A side note, may be an interview question, why accessing a value that does not exist in a JS object may be expensive, ob.status above answers that question very, the access operations until null is reached are quiet numerable, imagine a longer chain.

we can implement the prototype chain using a recursive method or better yet the infamous while loop, looking at the specification above, all we need to do in "get" is check whether the accessed property exists in the object itself, if not look in it's prototype and repeat the process until we find the property, if not(meaning we reached null) throw an error.

JavaScript handles creating the prototype object for you whenever you declare a new object or create one, but for our purpose we need to handle that our selves, since we are emulating, our object will be something of sort:


let hello = {

     h: "hello world",

     inheritFrom: null  // will act as our prototype

}




Enter fullscreen mode Exit fullscreen mode

part 2 implementing the prototype chain

let's change our function from get to protoRuntime and add functionality to handle prototypes




function protoRunTime(obj, property) {

 // changed key to property

         if(obj[property]){  // checking if the property exists in obj as own property if not look at obj proto

                 let type = typeof obj[property]


                 switch (type) {

                     case "function":

                          obj[property]()
                     break;

                     default:

                           return obj[property]

                     break;

                 }

        }



 // NEW CODE

 else if(obj.inheritFrom){ // if obj has our pseudo proto(inheritFrom)

             // MAGIC HAPPENES HERE

             let result = handleInherit(obj.inheritFrom,property) // recursive function that looks for the property in the chain(will implement)

             if(result !== null){ // if handleInherit found the value
                    // calle protoruntime on that object in the chain

                     return protoRunTime(result, property) 


             }

             else{ // handleInherit did not find the value

                   throw new Error(`${property} does not exist on ${obj_} prototype also`)

             }




 }

 else{ //if obj does not have inheritFrom by consequence property does not exist

          throw new Error(`${property} does not exist on ${obj_}`)

 }



}





Enter fullscreen mode Exit fullscreen mode

the "NEW CODE" may look daunting, but it is quite simple, follow the comments in the code


 let result = handleInherit(obj.inheritFrom,property)
Enter fullscreen mode Exit fullscreen mode

handleInherit returns a value or null, let's call handleInherit a chain crawler and will loop/crawl over all necessary inheritFrom's(prototype) objects until it finds a property and returns the object with that property, if it reaches the end of the chain null is returned, handled by the code below

     if(result !== null){ // if handleInherit found the value
                    // calle protoruntime on that object in the chain

                     return protoRunTime(result, property) 


             }

             else{ // handleInherit did not find the value

                   throw new Error(`${property} does not exist on ${obj_} prototype also`)

             }


Enter fullscreen mode Exit fullscreen mode

if a value is found handleInherit will return the object(inheritFrom) with that value, then we call protoRunTime over that returned object which will inevetible be trapped by  if(obj[property]) and the switch will execute accordingly:




 return protoRunTime(result, property)




Enter fullscreen mode Exit fullscreen mode

handleInherit function




function handleInherit(obj, property){

 let current = obj



 while(current){  // if current !== null

         if(current[property]){

               break   // if current obj has the property we are looking for, break and return the current object

         }

         else if(current.inheritFrom){

                 current = current.inheritFrom  // else make the current's inheritFrom obj  current(let current) and loop again
             // this is the crawling part, going down the chain

         }

         else{ // we no longer have inheritFrom

             current = null  // else if we reach the end, property does not exist on obj_ make current null

         }

}



     return current   // return whatever current is,

}



Enter fullscreen mode Exit fullscreen mode

if you understand the switch between current to it's inheritFrom object in the "else if" block, you now understand chain traversal, handleInherit is moving down the chain

Testing everything



let baseHello = {
     h :function(){
       console.log("hey there! ")
     },

    inheritFrom: null
}


let hello = {
    h: "hello world",

    inheritFrom: baseHello
}



let Obj ={

    hello: ()=>{
        console.log("hello world")
    },
    init: "init",
    inheritFrom: hello
}



 // chain   Obj -> inherits from 'hello' -> 'hello' inherits baseHello




protoRunTime(Obj, "hello")
console.log(protoRunTime(Obj, "init"))
console.log("traversal", protoRunTime(Obj, "h")) // -> hello world (can you see why? not hey there! remember the spec and chain)



Enter fullscreen mode Exit fullscreen mode

Conclusion

this was just scratching the surface much more cool stuff coming up. this is part of a series of articles "Unpacking JavaScript" a precursor to a project I am working on "24 projects in JavaScript", an eBook building "real" world projects(from engines, compilers, front end to mobile apps) in JS as explained in the intro article, along a table of content for upcoming articles.

Real vs Tutorial projects

There's one fundamental difference really, the target audience of a tut is you alone, you are mastering or at least absorbing concepts(learning), while a "real" project the end user is at the forefront you start thinking about user experience, application/module size, maintenance, patterns to follow, robustness, optimization etc, while thinking about users you think of yourself too, what to gain: monetization, status, a community or whatever, in short you are building something for people whether free or to trade, which is a very valuable skill and exactly what this eBook is about: building "real/usable/beneficial" apps/modules/programs however you view them.

if the above sounds like something you will be interested in, the eBook is progressing very well, you can sign up here to receive an update once complete, you can also sign up to be notified when future articles are published

Discussion (0)