DEV Community

notHanii
notHanii

Posted on

Javascript Working with Primitive Types, Prototypal Inheritance also var, let, const, and block scopes

Within JavaScript, there's a concept of a primitive value or a primitive data type. You may have heard or might hear that everything in JavaScript is an object. As we look at these primitive values, we can see that this is not true. There are in fact seven current types within JavaScript that are definitely not objects.

typeof operator

// output of log shown as comment
console.log(typeof 'hello world') // string
console.log(typeof 1) // number
console.log(typeof false) // boolean
console.log(typeof 42n) // bigint
console.log(typeof Symbol()) // symbol
console.log(typeof null) // object
console.log(typeof undefined) // undefined
Enter fullscreen mode Exit fullscreen mode

The difference between primitive types and Arrays or Objects is that they can't be mutated. The are immutable.

Primitive types don't have methods or properties on them either, they are not objects. They are the lowest level implementation in JavaScript.

So why can we use methods on primitive types like string.toUpperCase?

The typeof operator in JavaScript evaluates a statement to it's right and tells you what the type of that statement is. It will be a primitive or an object.

"Hello World" is a string
1 is a number
false is a boolean
42n is how we define bigint in JavaScript.

There's also another literal form for creating a symbol in JavaScript so you use Symbol() which is it's constructor.

Because of a bug that hasn't been fixed in JavaScript early on, null will show as an object when passed in to the typeof operator.

undefined is the seventh and final primitive type.

Primitive types are treated differently than objects. First we'll see how an object can be successfully mutated.

Mutate an Object

let obj = { a : 1}

function addTwo(obj) {
  obj.a = 2
}
Enter fullscreen mode Exit fullscreen mode

"Everything in JavaScript is an Object" is something that you've probably been told sometime in your career. As was discussed in the previous lesson, , this is clearly not true. So what is really happening?

Method used on Primitive Type

const str = 'foo'

console.log(typeof str) // string
console.log(str.length) // 3
Enter fullscreen mode Exit fullscreen mode

We should see an error when we try to use a 'dot' method on a string right? By rule, Primitive Types don't have any properties or methods on them. But as seen above, str.length returns a number, which means we just used a method on a string. 🧐

JavaScript uses a process called Autoboxing. Autoboxing wraps Primitive Types in an object so that we have the convenience of objects when dealing with Primitive Types in JavaScript.

When a Primitive Type is wrapped, it will connect that Type with a built-in object prototype that corresponds with the Primitive Type. This is where you get string.length, string.includes, or string.toUpperCase. (Methods documented on MDN web docs).

But because of autoboxing we treat primitive types like objects due to JavaScript wrapping those primitives into objects, now you can see why this misconception has started and is here today.

ProtoTypes are the way that JavaScript powers inheritance. This mimicks some behavior that you would expect out of a class in another language like C# or Java, but JavaScript does not have classes.

JavaScript adds a __proto__ property on your object that links other methods and properties (which themselves can be objects).

Open up your console and create an empty object let a = {}. If you log that variable, you'll see that __proto__ was automatically added even though the object is 'empty.'

This **proto key is also called Dunder proto.**

That Dunder proto property is what prototypes and inheritance is in JavaScript.

Every time you work with an object within JavaScript, as long as you don't mutate it later, will automatically be linked through this Dunder proto property to the global object prototype.

toString an Empty Object

const a = {}

a.toString()

console.log(a) // "[object Object]"
Enter fullscreen mode Exit fullscreen mode

Even though this object is empty, when I call toString on a, JavaScript will first look on the object for the toString method. When it doesn't find that method there, it will step into __proto__ and look for toString inside that object. In this case, it finds the toString method and calls it.

You could think of this method call as being called like this: a.__proto__.toString().

Nested Dunder Properties in Objects

const a = {}

const b = Object.create(a)

console.log(b)
/*
{}
  __proto__:
    __proto__: Object
*/
Enter fullscreen mode Exit fullscreen mode

Object.create will create an object and set the first argument that you pass it as the __proto__ for that variable, in this case, the object b will have the empty object a set as its prototype.

You can see from above that prototypes can be nested. In this case, we have an empty object (b), it's prototype is also empty because it was set to a, and then a's prototype is set to the Global Object ProtoType that all objects are given when no prototype is specified or mutated otherwise.

b prototype returns true

const a = {}

a.toString = function() { return true }

const b = Object.create(a)

console.log(b.toString()) // true
Enter fullscreen mode Exit fullscreen mode

The nearest prototype property wins when JavaScript is looking for a function. In this case, b is looking for a toString property. It doesn't find it in it's own object so it starts looking up the chain. The object a is set as the next prototype and it has a toString function. That function gets called. It doesn't matter that the Global Object Prototype has a toString property because JavaScript quits looking up the chain as soon as it finds something that matches.

b prototype in the console

console.log(b)
/*
{}
  __proto__:
    toString: f()
    __proto__:
      constructor: f Object()
      toString: f toString()
*/
Enter fullscreen mode Exit fullscreen mode

A method call will return undefined if it never finds the method called after going all the way up the prototype chain.

All types of objects within JavaScript have their own global built-in prototype objects that get connected whenever a new instance is created.

Plain Objects, Arrays, Maps, Sets, and Functions have their own Global Prototype Object.

Add property to function

function foo() {}

foo.firstName = 'zac'

console.log(foo.firstName) // zac
Enter fullscreen mode Exit fullscreen mode

Functions are first class objects in JavaScript which means they can have their own properties and methods like any other plain object could. You wouldn't typically find people adding properties to a function though, this is for demonstration purposes.

Every time a function is created, JavaScript will add a property to that function called prototype. This prototype property has two things on it. a constructor property which points back to the function itself and the __proto__ property.

Why does the prototype property exist if we already have proto?
What purpose does prototype serve?

This prototype property is not used in the prototype chain look-up if we were to "dot" onto the foo function when looking up a property.

__proto__ is also automatically created when a function is created and that is what JavaScript will use to look up methods it doesn't find immediately.

function foo() {}

foo.prototype.test = 'hello world'

console.log(foo.prototype) // foo { test: 'hello world'}

const name = new foo()

console.log(name.test) // hello world
Enter fullscreen mode Exit fullscreen mode

name.test is found on the name object because it is a new instance of the foo() function. Whatever prototype that currently lives on a function when a new instance is created with the new keyword will also be present on that new variable (name) as well.

Is the prototype that's passed to the new instance passed by reference? In other words, can it be mutated?

Origininal prototype is immutable

$ name
  foo {}
    __proto__: 
      test: "hello world"
      constructor: ƒ foo()
      __proto__: Object
$ name.test = "hey"
  "hey"
$ name
  foo {test: "hey"}

$ foo.prototype.test
  "hello world"
Enter fullscreen mode Exit fullscreen mode

The keyword Object is actually a function in JavaScript. Array, Map, and Set also have global functions like Object.

These global functions are connected to the __proto__ property to every instance of the object it corresoponds to. These functions are where many methods you use actually live.

In other words, you're using prototype inheritance every time you use any kind of object within your code.

Global prototype function is equal to the instance method

Array.prototype.map === [].map // true
Enter fullscreen mode Exit fullscreen mode

Scope is what determines where/when you can access a variable or function in your code. Block scope is a scope area that is usually defined by if, switch, or loops (eg. for or while).
More on the different types of scope in Ming-Shiuan's JavaScript: Introduction to Scope (function scope, block scope).

Block scope can also be defined by declaring variables within open and closed curly braces, { }. This is an overload of the object literal syntax.

It's important to note, there will not be any block scope until an assignment is made within that block.

Block scope

var firstName = 'zac'
{
  var firstName = 'jones'
  console.log(firstName) // jones
}
console.log(firstName) // jones
Enter fullscreen mode Exit fullscreen mode

Objects are not scoped, so simply looking for curlies will not completely help you when determining block scope.

Up above you can see an assignment and curlies which makes it a block scope

Why doesn't the console logs respect the block scope then? We know it's valid.

var only cares about function execution scopes, not block scopes. The global namespace is the function execution scope for the example above so both assignments are mutating the same variable.

Function Execution Scope

var firstName = 'zac'
function foo() {
  var firstName = 'jones'
  console.log(firstName) // jones
}
console.log(firstName) // zac
foo()
Enter fullscreen mode Exit fullscreen mode

To make the code work as we would expect it to, you can make the block scope a function and see that the console.logs will log out different variable values.

var declarations are hoisted to the top of their execution context with an initial value of undefined.

The code will look like this under the hood during execution time

Function Execution Scope under the hood

var firstName = undefined
firstName = 'zac'
function foo() {
  var firstName = undefined
  firstName = 'jones'
  console.log(firstName) // jones
}
console.log(firstName) // zac
foo()
Enter fullscreen mode Exit fullscreen mode

Any vars declared in a global function context are placed on the window object.
So in the browser console, window.firstName will be zac for me.

Block Scope with let

let firstName = 'zac'
{
  let firstName = 'jones'
  console.log(firstName) // jones
}
console.log(firstName) // zac
Enter fullscreen mode Exit fullscreen mode

let scopes to block scope as well as function execution scope.

let is only initialized when the parser evaluates it. It doesn't get hoisted to the top like var does. Finally, if we tried to re-declare let within the same scope, it would throw an error while using a var would not.

const has the same rules as let does.

The difference is that it const cannot be redefined to a different value. The thing to watch out for here is that const only does a shallow compare so if you change a value of a property in an object or an item in an array, const will not complain.

Block Scope with const

const firstName = 'zac'
{
  const firstName = 'jones'
  console.log(firstName) // jones
  firstName = 'not jones' // error!!
}
console.log(firstName) // zac
Enter fullscreen mode Exit fullscreen mode

Top comments (0)