loading...

What is Javascript's `new` keyword doing under the hood?

vincecampanale profile image Vince Campanale Originally published at vincecampanale.com ・3 min read

Good morning, afternoon, evening, night. I have some things to share with you about the new keyword in Javascript. Important things.

I'll start with some context and background about Constructor functions and the class keyword. Then, I will explain exactly what the new keyword is doing under the hood. Next, I will show how it does what it does by implementing it in code. Finally, I will explain why it does these things and give a couple arguments for avoiding this approach to Javascript object creation altogether in most situations. The information presented here comes from these resources and several others, processed by my brain.

Constructor functions ðŸ›

A Constructor function is a function that builds and returns a new instance of object. It looks like this:

/** Car: {
*    doors: number,
*    color: string,
*    drive: Function
*   }
*
* Car(doors: number, color: string) => Car
*/

function Car(doors=4, color='red') {
    this.doors = doors;
    this.color = color;
    this.drive = () => console.log('Vroom!');
}

The capital letter at the beginning of the Constructor name is simply a convention adopted by Javascript programmers to separate Constructor functions from regular functions.

The way Constructor functions work under the hood might make for an interesting article, but I'll leave that for another day. Today is about new.

The most important thing to take from this section is that the Constructor function, when invoked with the new keyword, will return an object with a doors property, a color property, and a drive method.

class

The class keyword was introduced to Javascript with the ES2015 specification, commonly known as ES6, soon to be known as "just Javascript."

The class keyword introduces nothing new (ha) -- it just provides some syntactic sugar for folks who like Java and semantic keywords. Nothing wrong with that.

Here's how you use it:

class Car {
    constructor(doors=4, color='red') {
        this.doors = doors;
        this.color = color;
    }

    drive() { console.log('Vroom!'); }
    // or drive = () => console.log('Vroom!');
}

Notice anything familiar?

I'll give you a hint:

console.log(typeof Car) // Function 

Under the Hood 🚗

Whether you are using a vanilla Constructor function or a special keyword to instantiate your object constructing mechanism, you will be using new to create new instances of the defined object. (There is another not-so-secret and powerful way to generate objects in Javascript called a factory function which will have to be covered in a future post).

So what is the new keyword doing under the hood (in human words)?

Three letters, four actions. When you say var myCar = new Car(), it...

1) Creates a new (empty) object 
2) Gets the prototype of the constructor function (Car) and sets it as the empty object's prototype
3) Calls the constructor function with the new empty object as `this` 
4) Returns the new object

What does this process look like in computer words?

Note: In order to reimplement new we will have to pass in the constructor and it's arguments separately.

First, let's do it in ES5 because you only live once.

// new(constructor: Function, constructorArgs: Array<any>) => Object
function new2(constructor, constructorArgs) {

    // Step 1: Create an empty object
    var newObject = {};

    // Step 2a: Get the prototype of the constructor function
    var constructorPrototype = constructor.prototype;
    // Step 2b: Set the empty object's prototype 
    Object.setPrototypeOf(newObject, constructorPrototype);

    // Retro technique to turn arguments into an actual array 
    var argsArray = Array.prototype.slice.apply(arguments); 
    // Slice off first argument b/c that's the constructor function itself. 
    var realConstructorArgs = argsArray.slice(1);

    // Step 3: Invoke constructor with newObject as 'this'
    constructor.apply(newObject, realConstructorArgs);

    // Step 4: Return the new object :)
    return newObject;
}

Now that we have a working implementation, we can clean it up and make use of some new tools from ES6.

// new(constructor: Function, constructorArgs: Array<any>) => Object
function new2(constructor, ...constructorArgs) {
    const newObject = {};
    Object.setPrototypeOf(newObject, constructor.prototype);    
    constructor.apply(newObject, constructorArgs);
    return newObject;
}

And...

const myCar = new2(Car, 4, 'blue');
console.log(myCar) // { doors: 4, color: 'blue', drive: [Function] }
myCar.drive() // Vroom!

But wait, there is an edge case. If the constructor function itself returns a new object, like this...

function Car(doors, color) {
    this.doors = doors;
    this.color = color;
    this.drive = () => console.log('Vroom!');
    return {
      doors,
      color
    }
}

we should just return that object directly:

// new(constructor: Function, constructorArgs: Array<any>) => Object
function new2(constructor, ...constructorArgs) {
    const newObject = {};
    Object.setPrototypeOf(newObject, constructor.prototype);
    return constructor.apply(newObject, constructorArgs) || newObject;
}

And we're done.

Hope this helped!

Tweet me with feedback @_vincecampanale if it did or didn't.

Til next time 👋.

Discussion

markdown guide
 

In the edge case you should return this

 

Okay, changed it. Is it correct now?

 

Well, now I think you do not completely understand it yourself.

In the new2 function you apply the constructor to newObject, which makes 'this' inside the constructor to reference newObject, therefore after modifying 'this' in the constructor it should return 'this'.
After that new2 can just return the result of 'constructor.apply' which is the reference to newObject.

P.S. Cannot really edit with markdown from a mobile.

You are correct -- I'm certainly having difficulty with understanding how to handle the edge case(s). Always learning! Thanks for taking the time to discuss it with me.

So, it sounds like my initial implementation of new2 was correct to return constructor.apply(newObject, constructorArgs) || newObject, but you are saying the Constructor function itself (in the edge case) should return this rather than the object it is currently returning? I believe that would actually be a new edge case (in addition to the one presented above).

I tested my implementation in a REPL and it is handling the edge case of a Constructor returning an object correctly.

Would you mind providing a code snippet or copying and correcting the code in the article to convey your insight?

Thanks again for contributing!

If constructor returns

    return {
      doors,
      color
    }

it is a completely different from newObject object reference and after that if new2 returns the result of constructor the newObject would be just lost in the new2 scope.

So it have to be like

function Car(doors, color) {
    this.doors = doors;
    this.color = color;
    this.drive = () => console.log('Vroom!');
    return this // equals to `newObject` ref
}

function new2(constructor, ...constructorArgs) {
    const newObject = {};
    Object.setPrototypeOf(newObject, constructor.prototype);
    return constructor.apply(newObject, constructorArgs); // bind newObject to constructor's `this` reference and call with args
}

Ah okay, I see what you're saying now. I guess part of what I was trying to convey is that the constructor function can in fact return any object. In which case, the new function should just return that object as is.

What you've presented here is a more realistic edge case. I will think about a way to cover it in the article.

Great insights -- thank you.

 

Nice catch Elias -- thank you for bringing it up. I'll fix it tonight!

 

Fantastic article. I was just wondering about this today.

 

Great article. I put this link in our news letter so other can find it as well: betterdev.link/issues/6

 

Nice article overall but the most interesting bit..."give a couple arguments for avoiding this approach(...)"

... has never happened! :-)

 

You're right! The article ended up getting quite long, so I decided to leave that part for a follow-up. I guess it's a cliffhanger for now ;)