This page looks best with JavaScript enabled

¿Qué hace ‘new’ en JavaScript?

 ·  ☕ 6 min  ·  ✍️ eiximenis

Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí

Si preguntas a mucha gente cual es el objetivo de new en JavaScript te dirán que el de crear objetos. Y estarán en lo cierto, aunque esa definición no es del todo precisa. Y es que mucha gente no entiende exactamente que hace new así que vamos a dedicarle este post.

Creación de objetos sin new

La verdad es que new no es necesario en JavaScript. Se pueden crear objetos perfectamente sin necesidad de usar new:

  1. var foo = {
  2.     name: 'foo',
  3.     getUpperName: function () {
  4.         return this.name.toUpperCase();
  5.     }
  6. }

Esta manera de crear objetos la conocemos como object notation y nos permite definir objetos con campos y métodos. Por supuesto, en este caso solo creamos un objeto pero basta con declarar una función que tenga ese código para crear tantos objetos como queramos:

  1. var Foo = function (name) {
  2.     var foo = {
  3.         name: name || '',
  4.         getUpperName: function () {
  5.             return this.name.toUpperCase();
  6.         }
  7.     }
  8.  
  9.     return foo;
  10. }
  11.  
  12. var f2 = Foo("f2");
  13. f2.getUpperName();  // devuelve F2

Nos podemos preguntar cual es el prototipo de cada objeto creado mediante una llamada a Foo. Pues, como es de esperar, el prototipo de f2 será Object.prototype.

Podríamos indicar que los objetos creados por Foo heredan de Foo.prototype, con un código similar al:

  1. var Foo = function (name) {
  2.     var foo = {
  3.         name: name || '',
  4.         getUpperName: function () {
  5.             return this.name.toUpperCase();
  6.         }
  7.     }
  8.     Object.setPrototypeOf(foo, Foo.prototype);
  9.     return foo;
  10. }
  11.  
  12. var f2 = Foo("f2");
  13. f2.getUpperName();  // devuelve F2
  14. f2 instanceof Foo; // devuelve true

Lo interesante es que establecer como prototipo del objeto creado el objeto Foo.prototype, automáticamente hace que instanceof Foo devuelva true sobre todos los objetos creados por la función Foo. Vale, estoy usando Object.setPrototypeOf que es una función nueva de ES2015, pero en ES5 clásico tenemos Object.create que nos permite hacer lo mismo:

  1. var Foo = function (name) {
  2.     var foo = Object.create(Foo.prototype);
  3.     foo.name = name || '';
  4.     foo.getUpperName = function () {
  5.         return this.name.toUpperCase();
  6.     }
  7.     return foo;
  8. }
  9.  
  10. var f2 = Foo("f2");
  11. f2.getUpperName();  // devuelve F2
  12. f2 instanceof Foo; // devuelve true

¿Entonces… qué hace new?

Bueno, new se introdujo para dar una “sintaxis familiar” a los desarrolladores que venían de Java, pero básicamente hace lo mismo que hemos hecho nosotros. En JavaScript hablamos de funciones constructoras, pero no hay sintaxis para declarar una función constructora. Una función constructora es aquella pensada para ser llamada con new, pero que quede claro: es usar o no new al invocar una función lo que convierte a esa en constructora.

Cuando usamos new en una función, llamémosla Bar, ocurre lo siguiente:

  1. Se crea automáticamente un objeto vacío que hereda de Bar.prototype
  2. Se llama a la función Bar, con el valor de this enlazado al objeto creado en el punto anterior
  3. Si (y solo sí) la función Bar devuelve undefined, new devuelve el valor de this dentro de Bar (es decir, el objeto creado en el punto 1).

Como, gracias al punto 2, dentro de una función llamada con new el valor de this es el objeto creado en el punto 1, decimos que en una función constructora el valor de this es “el objeto que se está creando”. Y es por ello que las codificamos de esa manera:

  1. var Foo = function (name) {    
  2.     this.name = name || '';
  3.     this.getUpperName = function () {
  4.         return this.name.toUpperCase();
  5.     }
  6. }

Pero para que veas exactamente hasta que punto new no era “imprescindible” es porque incluso podríamos hacer una especie de polyfill para new:

  1. var functionToCtor = function (ctor) {
  2.     var o = Object.create(ctor.prototype);
  3.     var args = Array.prototype.slice.call(arguments)
  4.     args.splice(0, 1);
  5.     var v = ctor.apply(o, args);
  6.     return v === undefined ? o : v;
  7. }

La función functionToCtor hace lo mismo que new. Es decir, en este caso dada la función constructora Foo ambas expresiones son equivalentes:

  1. var foo = functionToCtor(Foo, "foo");
  2. var foo2 = new Foo("foo2");

Tanto foo como foo2:

  • Tienen como prototipo Foo.prototype
  • Tienen como constructor la función Foo
  • Devuelven true a la expresión instanceof Foo

¿Y si una función constructora devuelve un valor?

Esa es buena… Pues en este caso cuando se use new el valor obtenido será el devuelto por la función constructora (siempre que este valor sea distinto de undefined):

  1. var Foo = function (name) {    
  2.     this.name = name || '';
  3.     this.getUpperName = function () {
  4.         return this.name.toUpperCase();
  5.     }
  6.  
  7.     return { a: 10 };
  8. }
  9.  
  10. var f = new Foo();
  11. f.a;        //  10
  12. f instanceof Foo // false

Si… a pesar de que f ha sido creado usando new Foo, el prototipo de f ya no es Foo.prototype, ya que f vale el objeto devuelto por la función Foo. En este caso el objeto “original” cuyo prototipo es Foo.prototype y al que nos referíamos con this dentro de Foo se ha perdido. Lo mismo ocurre si usas el “polyfill” functionToCtor que hemos visto antes.

Funciones duales

Podemos hacer una función que sepa si se ha invocado con new o no? Pues sí:

  1. var Foo = function (name) {
  2.     if (this instanceof Foo) {
  3.         // Invocada con new
  4.     }
  5.     else {
  6.         // Invocada sin new
  7.     }
  8. }

Esto nos permite trabajar con this en el caso de que se haya usado new o bien no trabajar con this y devolver un valor en el caso de que no se haya invocado a la función con this. De todos modos si quieres que el usuario obtenga el mismo valor use new o no, lo más fácil es devolver el valor desde la función constructora. En este caso, eso sí, pierdes el hecho de que el prototipo del objeto sea la función constructora. Si quieres asegurarte que el usuario obtiene siempre un objeto con el prototipo asociado a la función constructora, es decir como si siempre usase new, puedes hacer:

  1. var Foo = function (name) {
  2.     var _create = function (name) {
  3.         this.name = name || '';
  4.         this.getUpperName = function () {
  5.             return this.name.toLocaleUpperCase();
  6.         }
  7.     }
  8.  
  9.     if (this instanceof Foo) {
  10.         _create.bind(this)(name);
  11.     }
  12.     else {
  13.         var o = Object.create(Foo.prototype);
  14.         _create.bind(o)(name);
  15.         return o;
  16.     }
  17. }

La función _create es la que realmente rellena el objeto (le llega ya creado en la variable this). La función Foo lo único que hace es mirar si ha sido llamada con new o no. En el primer caso llama a _create pero vinculando el valor de this dentro de _create al propio valor de this dentro de Foo (que en este caso es el objeto que se está creando) y no devolver nada. En el caso que el usuario no haya usado new la función Foo crea un objeto vacío, le asigna Foo.prototype como prototipo y luego llama a _create vinculando el valor de this dentro de _create al del objeto que acaba de crear. Y finalmente, en este caso, devuelve el objeto creado.

Así, tanto si el usuario hace var x = Foo() como var x2 = new Foo() obtiene lo mismo: un objeto cuyo prototipo es Foo.prototype y que por lo tanto devuelve true a instanceof Foo.

Espero que este post os haya ayudado a entender como funciona new en JavaScript.

Saludos!

Si quieres, puedes invitarme a un café xD

eiximenis
ESCRITO POR
eiximenis
Compulsive Developer