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(){
}
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
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
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()
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)
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
}
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
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)
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;
}
}
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
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")*/
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
}
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_}`)
}
}
the "NEW CODE" may look daunting, but it is quite simple, follow the comments in the code
let result = handleInherit(obj.inheritFrom,property)
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`)
}
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)
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,
}
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)
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
Top comments (0)