Converta uma função de construtor em uma função regular que pode ser chamada sem novos

Em javascript, como você pode converter uma “função construtora” em uma função regular que pode ser chamada sem a newpalavra chave?

A implementação

/**
Take a constructor and return a function that can be called without new

but have the same effect as calling the constructor with new.

Useful for passing constructors to _.map, or passing arbitrary

arguments to the constructor using fn.apply(null, arguments)

*/

function ctor_fn(ctor) {
return function() {
var object = Object.create(ctor.prototype);
ctor
.apply(object, arguments);
return object;
}
}

Motivação

Suponha que você tenha uma função construtora Buttonque recebe dois argumentos: texte callback. Você pode criar um novo objeto de botão como este: new Button("Clickme", callback).

Se você precisar criar uma função que tenha o mesmo efeito de chamar Button, mas sem usar a newpalavra chave, você pode fazer algo como:

function CreateButton(text, callback) {
return new Button(text, callback);
}

Fácil. Agora você pode passar CreateButtoncomo uma função que recebe argumentos e retorna um objeto. Você pode até chamá-lo desta forma:

CreateButton.apply(null, ["click me", callback]);

Mas agora, e se você quiser fazer o mesmo com outros 10 construtores? Como podemos generalizar essa abordagem?

As funções do Javascript têm e , mas não há nada que faça o equivalente a mas com o efeito de ter a palavra chave na frente dele..call.apply.applynew

Na verdade, a solução é simples: tudo o que você precisa fazer é chamar applye passar como primeiro argumento algum tipo de objeto “vazio”, por assim dizer. No entanto, para realmente emular a newpalavra chave corretamente, esse objeto vazio deve ter o protótipo da função construtora; caso contrário, não é o mesmo que chamar a função com a newpalavra chave.

Exemplo

Abra o console dev do seu navegador e tente o seguinte:

Primeiro, vamos criar alguma função construtora.

function Button(text, callback) {
var self = this;
self
.text = text;
var noop = function() {};
self
.callback = callback || noop;
self
.as_element = function() {
var button = document.createElement("button");
button
.innerText = self.text;
button
.addEventListener("click", self.callback);
return button;
}
}

Nada de especial aí. Recebe um rótulo e uma função de retorno de chamada e fornece um método para criar um elemento de botão DOM com base nisso.

Agora, para testar se funciona, crie alguma instância e veja se ela se comporta conforme o esperado:

> b1 = new Button("test1", function() { console.log("Test 1 has been clicked"); })
<- Button {text: "test1", callback: function, as_element: function}
> b1.as_element()
<- <button>​test1​</button>​
> b1.as_element().click()
Test 1 has been clicked
<- undefined

Observe que estou fazendo isso no console de desenvolvimento do Chrome. As linhas que começam com <-indicam o valor retornado da linha inserida acima dela.

Observe também como o console do Chrome exibe o objeto de botão (segunda linha): ele diz . Este é o Chrome informando qual função foi usada para construir este objeto.Button { .... }

OK, então funciona como esperado ao usar a nova palavra-chave. Agora vamos transformá-lo em uma função usando o nosso ctor_fnacima:

CreateButton = ctor_fn(Button);

Veja se funciona:

> b2 = CreateButton("test2", function() { console.log("test2 clicked!! it works!!"); })
<- Button {text: "test2", callback: function, as_element: function}
> b2.as_element()
<- <button>​test2​</button>​
> b2.as_element().click()
test2 clicked
!! it works!!
<- undefined

Funciona exatamente como se fosse chamado com new. O Chrome até reconhece que foi construído a partir dessa Buttonfunção! Por quê? Porque costumávamos Object.create(ctor.prototype)criar o objeto. Sim, não usamos Buttonpara criar o objeto, mas esse é o seu protótipo, então é a mesma coisa.

Se você solicitar o construtor do objeto, obterá o seguinte:

> b2.constructor.name
<- "Button"

Que tal apply? Podemos chamar CreateButton.applypara criar o objeto e esperar se comportar corretamente?

> b3 = CreateButton.apply(null, ["test3", function() { console.log("Test3 clicked! Apply also works!"); }])
<- Button {text: "test3", callback: function, as_element: function}
> b3.as_element()
<- <button>​test3​</button>​
> b3.as_element().click()
Test3 clicked! Apply also works!
<- undefined

Sim, funciona!

Então aí está.