JavaScript Constructor Functions and Prototypes.
JavaScript is sometimes a bit confusing. It has a native type Object
. How does that relate to Object-Orientation? And how does the desire to avoid repeating ourselves relate to them both?
Constructor Functions
It’s sometime handy to represent data with objects, which gives us key/value pairs. For example, we may represent a user as the following:
const bobby = {name: ‘bobby’, age: 20, hometown: ‘Philadelphia’}
Now imagine that we had a couple of users. This code will grow out of proportion very quickly! We would be repeating ourselves!
function User(name, age, hometown) {return {name, age,hometown,}}let bob = User(“Mr. Bob”, 45, “Manhattan”)bob.age // => 45
Interestingly typeof
confirms that bob is an Object
:
> typeof bob‘object’
However, something’s not quite as clear as we might like it to be. If we ask bob
what made him, the answer is...
> bob.constructor[Function: Object]
bob is certainly an Object
but it's more specific than that: it's a User
. We'd really like for this special kind of object to be reflected when we ask it what it is. We'd like for a mystical process to come along and say you are not merely an Object
, you are a User
. We tell JavaScript to bless the thing created by the constructor function into being something more specific than Object
by using the keyword new
. Using new
requires that we evolve our factory function into a constructor function. It's the same idea, but with a few subtle additions.
What Is new
and How Does It Work With the Constructor Function
Lets create a constructor function. Constructor functions must be paired with the new
keyword.
function User(name, email) {this.name = name;this.email = email;}
this
here refers to the function's context. Since functions in JavaScript are also Object
s, a function can say “Hey, constructor function, when you run, create a new copy of yourself, leaving the original unchanged and on that particular copy, set the properties based on the arguments passed into the function.
The keyword new
tells the constructor function to do exactly that.
Put the two together like so:
function User(name, email) {this.name = name;this.email = email;}let lauren = new User(“Lauren”, “lauren@example.com”);lauren.name //=> “Lauren”
What’s happening here hinges on the new
keyword. When we invoke the function with new
before it, you can imagine an imaginary JavaScript Object
being copied for use in the User
function. The constructor function is not changed; the freshly-created, new context is changed.
This sorta makes sense, the function that constructed lauren
, or the (constructor )
User
. The instance lauren
is an object
. Given what we know about the types available in JavaScript, object
makes good sense (not a Number
or a String
, that's for sure!)
Since we know one OO pattern, we might be wondering how to add methods to the User
instances. It should be obvious that if we can set a property to point to a value like "Lauren"
or "lauren@example.com"
, we should be able to set an anonymous function to a property. That function would have access to the this
context created when the instance was new
'd into existence.
function User(name, email) {this.name = name;this.email = email;this.sayHello = function() {console.log(`Hello everybody, my name is ${this.name} whom you’ve beenmailing at ${this.email}!`);};}let lauren = new User(‘lauren’, ‘lauren@example.com’);lauren.sayHello(); //=> Hello everybody, my name is lauren whom you’ve been mailing at lauren@example.com!
Identify Inefficiency In Constructor Function Object Orientation
Let’s assume the following. Let’s assume that the name
property costs 32 bytes of space. Let's also assume that the email
property costs 32 bytes of space. Let's lastly assume that a function costs 64 bytes of space.
So to create our lauren
instance we pay a cost of 32 + 32 + 64 = 128
bytes. But now let's assume that we want to create many more User
s - Facebook numbers of users. Lets suppose a paltry 1 million users. That would be: 128 million bytes of space. While memory and disk are getting bigger and cheaper all the time, we'd like to be efficient whenever possible.
In our example the name
s vary, so we can't economize there. The email
s vary, so we can't economize there either. But the method, sayHello
is the same in every instance: "in my current context return a template string with this current context's values."
The key to gaining efficiency is the prototype.
Prototype
We access the prototype of a constructor function by typing the constructor function’s name, and adding the attribute .prototype
. So for User
it's User.prototype
. Attributes that point to functions in this JavaScript Object
will be shared to all instances made by that constructor function.
function User(name, email) {this.name = name;this.email = email;}User.prototype.sayHello = function() {console.log(`Hello everybody, my name is ${this.name}`);};let sarah = new User(‘sarah’, ‘sarah@example.com’);let lauren = new User(‘Lauren’, ‘lauren@example.com’);sarah.sayHello(); //=> // “Hello everybody, my name is sarah!”lauren.sayHello(); //=> // “Hello everybody, my name is Lauren!”
The prototype is just a JavaScript Object
.
User.prototype;// {sayHello: ƒ, constructor: ƒ}typeof User.prototype;// object
To prove the efficiency of sharing methods via prototype:
lauren.sayHello === sarah.sayHello; //=> true
Conclusion
We’ve seen the constructor function and the new
keyword work to create new instances with instance data and instance methods. We’ve also seen the constructor’s prototype and seen how it can be used to share functionality between instances.