Tuesday 25 January 2022

Invoke JavaScript Constructor

I've been revisiting how we've been able to create an object of an arbitrary type in JavaScript over the years. I mean, a function that receives the "Type" (that is, a function intended to work as a constructor) and the arguments and creates an instance of that "Type". In current JavaScript it's really simple (thanks to the rest-spread operators)


function createFromType1(type, ...args) {
        return new type(...args);
}

//or

function createFromType2(type, ...args) {
        return Reflect.construct(type, args);
}

class Person{
	constructor(name, age){
		this.name = name;
		this.age = age;
	}
}

let p1 = createFromType1(Person, "Francois", 4);
let p2 = createFromType2(Person, "Francois", 4);



But in the past it was quite a bit more complicated, so I've felt like doing a bit of Javascript Archeology and writing it down here (I still consider useful to have an understanding of these things).

The first thing to understand is what happens when we call a function with new (either an ES6 class constructor or a normal function that we invoke with new). Remember that arrow functions and ES6 class methods are not constructible (can not be invoked with new). From this StackOverflow answer:

The new operator takes a function F and arguments: new F(arguments...). It does three easy steps:

Create the instance of the class. It is an empty object with its __proto__ property set to F.prototype. Initialize the instance.

The function F is called with the arguments passed and this set to be the instance.

Return the instance

(regarding the "instance of the class" there's a slight difference between a ES6 constructor and a normal function invoked with new, I talked about it in this post, but is not particularly important

Before ES5, to replicate the 3 aforementioned steps we had to do something like this (from the same StackOverflow answer):


    function New (f) {
/*1*/  var n = { '__proto__': f.prototype };
       return function () {
/*2*/    f.apply(n, arguments);
/*3*/    return n;
       };
     }
     
let p1 = New(Person)("Francois", 4);

or like this:


function construct(type, args) {
    function F() {
        type.apply(this, args);
    }
    F.prototype = type.prototype;
    return new F();
}
let p1 = construct(Person, ["Francois", 4]);

With the advent of ES5 we could simplify it a bit using Object.create, like this:


function createFromType(type, args){
	return type.apply(Object.create(type.prototype), args);
}
let p1 = createFromType(Person, ["Francois", 4]);

The 3 techniques above will not work with ES6 classes, because we can not use "apply" (or "call") with an ES6 class constructor (it's only constructable, but not callable, so using call or apply will throw an "Uncaught TypeError: Class constructor xxx cannot be invoked without 'new'"). So we can just forget about them (safe for historical reasons) and use the 2 first approaches that I show in this post.

No comments:

Post a Comment