NOTA: Este artículo está centrado en un tema que ya comenté en el post anterior, pero de una forma más organizada y centrada, algunos fragmentos están extraídos del post anterior para mayor claridad.
Constructores en Javascript
He hablado antes de la limitación de los constructores javascript y sobre todo de la complejidad de extenderlos
function Person(name) {
this.name = name;
}
Person.prototype.methodA = function() { ... };
function Employee(name, position) {
Person.call(this, name);
this.position = position;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.methodB = function() { ... };
Esto es un asunto que trae de cabeza a la mayoría de la gente que programa javascript, la dificiltad que conlleva crear una simple "clase" hasta el punto que en la siguiente versión del ECMAScript (el estándar en el que está basado Javascript) han incluído una forma más sencilla de hacer lo mismo: la palabra clave class
class Person {
constructor(name) {
this.name = name;
}
methodA() {}
}
class Employee extends Person {
constructor(name, position) {
super(name);
this.position = position;
}
methodB() {}
}
Aunque he visto a mucha gente emocionada pensando que ECMAScript 6 traerá clases reales tengo que decir que este código no hace ni más ni menos que lo que hace el primer código. Y es muy importante saberlo porque aunque prezcan classes como las de Java o C++, en este caso siguen siendo objetos usando herencia por prototipos y esconderlo solo servirá para no saber porqué el código no funciona como esperamos.
En cualquier caso vemos que definir tipos en javascript es complicado y la solución propuesta por el equipo de ECMA no es, en mi opinión, la más adecuada.
Orientado a objetos
Creo que el problema se aloja en la definición que dimos en un principio a "Programación Orientada a Objetos" (Object Oriented Programming, OOP) ya que los primeros lenguajes OOP creaban objetos usando clases y otras herramientas, y aunque los objetos son la base del sistema la estructura está dada por las clases. Lo que sería Programación Orientada a Objetos con Clases.
Después recibimos otros lenguajes que también se definian como "Programación Orientados a Objetos" pero enfocado de otra forma, entre ellos javascript. En este caso el lenguaje no tiene clases sino que todo son objetos y la estructura se crea mediante prototipos, todo objeto puede ser el prototipo de otro objeto y esto significa que si B prototipa a A todas las propiedades que A tenga también existirán en B. Esto es lo que llamo Programación Orientada a Objetos con Prototipos.
Me he cruzado con mucha gente que piensa que la Programación Orientada a Objetos no es posible sin clases y que si Javascript no tiene clases no puede denominarse orientado a objetos. Como en todo debate entre geeks acabamos en la wikipedia:
La programación orientada a objetos o POO (OOP según sus siglas en inglés) es un paradigma de programación que usa los objetos en sus interacciones, para diseñar aplicaciones y programas informáticos. Está basado en varias técnicas, incluyendo herencia, cohesión, abstracción, polimorfismo, acoplamiento y encapsulamiento.
En resumen, un lenguaje orientado a objetos es el que tiene objetos (brillante conclusión) y cumple una serie de técnicas (herencia, cohesión, abstracción, polimorfismo...) que en el caso de Java se hace mediante clases y en el caso de Javascript se hace mediante prototipos.
Los inicios de Javascript
Javascript en sus inicios se llamó LiveScript, cuenta la leyenda que por aquella época Java estaba teniendo mucho éxito y por marketing se decidió llamar al nuevo lenguaje JavaScript. También cuenta que por el mismo motivo a última hora se decidió modificar el lenguaje para parecerse más a Java añadiendo, entre otras funcionalidades, el operador new
para que pareciera tener clases.
Hay algo muy curioso en los constructores Javascript, que en el fondo son simples funciones, y es que todas las funciones javascript tienen la propiedad prototype
que por defecto trae un objeto que solo tiene una propiedad, la propiedad constructor
que es el propio constructor.
function Testing() {}
console.log(Testing.prototype.constructor === Testing);
var proto = Testing.prototype;
console.log(proto.constructor.prototype === proto);
Constructores vs objetos prototipo
Esto me hace pensar que quizás la intención original de los objetos en javascript no era tener constructores que contienen prototipos sino tener prototipos que contienen constructores. Es decir: en lugar de...
function MyType() {
this.id = 1;
}
MyType.prototype.methodA = function() { ... }
Hacer esto...
var MyType = {
constructor: function() {
this.id = 1;
},
methodA: function() { ... },
};
Vaya! No parece una forma mucho más sencilla de declarar tipos? Aquí podemos comparar el mismo tipo escrito con constructores y con este paradigma y juzguen ustedes mismos. Y que pasa cuando intentamos invocar al constructor? hay que usar .call()
o .apply()
para pasarle this?
var instancia = Object.create(MyType);
instancia.constructor();
BOOM! Constructor ya recibe this porque es invocado directamente en la instancia! No es exageradamente sencillo y lógico desde éste punto de vista?
Además por accidente hemos quitado de en medio la función constructora y lo que tenemos es un simple objeto, el elemento más básico de la programación orientada a objetos. Es decir, para declarar un tipo solo tenemos que crear un objeto, para prototipar un objeto solo necesitamos un paso
var SubType = Object.create(MyType);
No estamos obligados, a diferenia del primer caso, a crear un nuevo constructor para crear un subtipo, por la herencia por prototipos tenemos el mismo constructor que MyType
console.log(SubType.constructor === MyType.constructor);
// true
Y la mejor parte, que pasa si queremos crear un tipo sin constructor? No hay problema.
var MyType = {};
console.log(MyType.constructor); // Object
ECMAScript 6
Este paradigma se parece bastante a la forma de crear clases en ECMAScript 6
class MyType {
constructor() {
this.id = 1;
}
methodA() { ... }
}
Que alguno dirá, si, pero con las clases de ECMAScript 6 podemos extender clases, llamar al método padre con super
y nos ahorramos poner function
... Pero esas no son funcionalidades de las clases de ECMAScript 6, esas son funcionalidades de todos los objetos en ECMAScript 6.
// Clase ECMAScript 6
class Employee extends Person {
constructor(name, postition) {
super(name);
this.position = postition;
}
methodA() { ... }
}
// Objeto en ECMAScript 6 estándar
var Employee = {
__proto__: Person,
constructor() {
super(name)
this.id = 1;
},
methodA() { ... }
};
Las diferencias entre una clase ECMAScript 6 y un objeto en ECMAScript 6 son mínimas, pero mientras que una clase nos hace pensar que Employee
se comportará como una clase Java cuando no es así, un objeto es simplemente eso, un objeto y todos somos capaces de entender como se comporta un objeto, no? (si no que haces leyendo esto? o.o)
Pero dejemos ECMAScript 6 de lado por ahora, que aún tiene que transcurrir tiempo antes de que podamos usarlo en serio.
Instanciación
Hasta aquí era la definicion del tipo, pero como creamos una instancia? Primero tendríamos que plantearnos que es una instancia, si no tenemos clases podemos tener instancias? Según la wikipedia
En el paradigma de la orientación a objetos, una instancia (en inglés, instance) se refiere a una realización específica de una clase o prototipo determinados.
Pero, al menos a mi, no importa mucho la palabra; el tema es que nosotros creamos objetos para que hagan de prototipos y queremos crear "instancias" de estos prototipos. La forma de prototipar un objeto es usando Object.create()
var instance = Object.create(MyType);
Pero, un momento... Esto es exactamente lo mismo que hicimos para crear un subtipo, no? Si. Entonces en que se diferencia una instancia de un subtipo? En general, nada. Una instancia ES un subtipo. Pero en la mayoría de los casos las "instancias" tienen una necesidad que los subtipos no tienen: en una instancia se invoca al constructor, en un subtipo no.
var MyType = {
constructor: function () {
this.id = 1;
},
};
// Crear sub-tipo
var SubType = Object.create(MyType);
// Crear instancia
var instance = Object.create(MyType);
instance.constructor();
Esta similitud entre una instancia y un SubTipo nos ayuda a entender hasta que punto en el fondo Javascript es muy, muy sencillo: todo son objetos; no hay diferencia entre un tipo y una instancia porque la diferencia es conceptual.
Esto es muy útil para entender la sencillez y el corazón de Javascript, pero es un poco tedioso tener que hacer dos pasos para instanciar, podríamos simplificarlo?
Type.new()
es el nuevo new
Lo cierto es que podríamos, podemos hacer una función que haga este proceso:
function createInstance(Type) {
var instance = Object.create(Type);
instance.constructor();
return instance;
}
var Type = {
constructor: function () {
this.id = 1;
},
};
var instance1 = createInstance(Type);
var TypeWithoutConstructor = {};
var instance2 = createInstance(TypeWithoutConstructor);
Y que pasa si en lugar de llamarla createInstance
la llamamos $new
por ejemplo?
var instance = $new(MyType);
Empieza a parecer similar, solo nos faltaría cambiar la funcion $new
para pasarle parámetros al constructor
function $new(Type, params) {
var instance = Object.create(Type);
instance.constructor.apply(instance, params);
return instance;
}
var Type = {
constructor: function (name) {
this.name = name;
},
};
var instance = $new(Type, ['bob']);
Parece funcionar, pero solo para acabar de pulirlo, porqué no ponemos $new como método de Type? así podríamos pasarle los argumentos sin el array y como ECMAScript 5 nos permite usar palabras clave como propiedades de objeto podemos llamarlo simplemente new
.
var Type = {
new: function () {
var instance = Object.create(this);
instance.constructor.apply(instance, arguments);
return instance;
},
constructor: function (name) {
this.name = name;
},
};
var instance = Type.new('bob');
Y tenemos una forma que podemos usar con ECMAScript 5 para crear tipos e instancias de forma sencilla. Pero que diferencia hay entre esto y hacer un new
? A parte de la ya mencionada simplicidad para crear y extender tipos, tiene más ventajas, principalmente porque nos permite controlar más exactamente cómo se crea un objeto que en algunos casos es conveniente cambiarlo (en la mayoría no), pero excede el alcance de éste post.
Conclusión
Después de pasarme los últimos años probando mil y una formas de crear y extender "clases" para encontrar la forma más sencilla, rápida y elegante, muchas de ellas registradas en este blog; me quedo con ésta. La lección que me dio javascript es que no es conveniente luchar contra su naturaleza, si queremos usar javascript y no morir en el intento lo más razonable es usar javascript y no tratarlo en contra de su naturaleza.
Aún está por verse pero creo que este sistema incluso puede competir cara a cara con las "clases" de ECMAScript 6, pero en cualquier caso la conversión entre un tipo creado por constructor y uno creado con este sistema es muy sencilla
Por ejemplo, convertir un tipo creado con este sistema a constructor para usarlo con new
:
var MyType = {
myMethod: function() { ... },
};
function MyConstructor() {
MyType.constructor.apply(this, arguments);
}
MyConstructor.prototype = MyType;
O convertir un constructor a este paradigma:
function MyConstructor() {
this.value = 1;
}
MyConstructor.prototype.myMethod = function() { ... };
var MyType = MyConstructor.prototype;
// y si queres añadir new...
MyType.new = $new;
Inicializador
Para finalizar un bonus, después de toda esta travesía me he dado cuenta que el constructor, que para javascript parece tan importante, no lo es tanto. Si nos paramos a mirar el constructor vemos que es una simple función
function MyType() { ... }
No tiene nada de especial, incluso podemos invocarla como una función y no construye nada. Entonces quién construye? new
. Es el operador new
el que crea el nuevo objeto y luego invoca el método llamado "constructor", que no se diferencia en nada de cualquier otro método que podría tener el objeto.
Por como yo lo veo, la función del constructor es más inicializar que construir, debe encargarse de inicializar las propiedades del objeto, no construir. Visto así es evidente que el nombre "constructor" no es apropiado, en mi caso prefiero la denominación "initializer" o simplemente "init", como Backbone ya hace en sus objetos.
Por eso en mis proyectos cuando utilizo este paradigma, prefiero que mi función $new
invoque el método init
en lugar de llamar al método constructor
.
function $new() {
var obj = Object.create(this);
obj.init.apply(obj, arguments);
return obj;
}
var MyType = {
new: $new,
init: function () {
this.value = 1;
},
};
var instance = MyType.new();