39

Con el tiempo que he trabajado con el lenguaje JavaScript, he empleado distintas formas para crear objetos por medio de lo que propiamente podría ser la emulación de una clase (u objeto literal).

Las formas que he trabajado son las siguientes:

  1. Clausuras:

    var NombreClase = (function () {
        function NombreClase () {}
    
        NombreClase.prototype.metodo = function () {
            // código
        }
    
        NombreClase.metodoEstático = function () {
            // código
        }
    
        function metodoPrivado () {
            // código
        }
    
        return NombreClase;
    })();
    
  2. Prototipo (prototype):

    function NombreClase (arg1, arg2) {
        this.arg1 = arg1;
        this.arg2 = arg2;
    }
    
    NombreClase.prototype.metodo = function () {
        // código
    }
    
    NombreClase.metodoEstático = function () {
        // código
    }
    
  3. Objeto literal:

    var NombreClase = {
        prop: 'foo',
        metodo: function () {
            // body...
        }
    };
    

Me gustaría conocer objetivamente ¿cuál es la conveniente usar?, ¿si existe otra forma? y ¿por qué es recomendable?.

Chofoteddy
  • 5,975
  • 5
  • 25
  • 65

9 Answers9

18

El sistema de herencia de javascript no funciona de la misma forma que la mayoría de los lenguajes que tienen clases propiamente dichas. En javascript no hay clases, hay prototipos y estos comparten semejanzas con las clases pero no se comportan exactamente igual que ellas.

La primera diferencia más obvia entre ambos tipos de herencia es que en las clases tradicionales estas generan la maqueta del nuevo objeto y se encargan se su inicialización, esto es, tienen un constructor y definen los métodos que tendrá el objeto creado. Este es un ejemplo en C#

// Declaración de la clase y herencia
public class NombreClase: ClasePadre
{
    // Constructor
    NombreClase() {
        // Código de inicialización
    }

    // Propiedades y métodos que tendrá el objeto
    // Métodos privados no son accesibles por las instancias creadas
    private string propiedadPrivada { get; set; }
    public void metodoPublico(string parametro) {

    }
}

Esta definición de atributos del objeto (léase propiedades, métodos, etc) esta condicionada tanto por los métodos que se declaran en la misma clase como en las clases padre y la visibilidad de dichos atributos puede ser controlada en la misma definición de la clase.

En javascript esto funciona similar pero la diferencia radical es que el código de inicialización o constructor(léase función) es el código que se encuentra dentro de la misma clase y el sistema de herencia es gestionado por el prototipo. Es aproximadamente el mismo resultado pero las responsabilidades se ejecutan de forma separada.

// Constructor
function NombreClase {
    // Código de inicialización
}

// Herencia
NombreClase.prototype.propiedad = 'valor';
NombreClase.prototype.metodo = function() {};

Todas las propiedades son públicas y hay que recurrir a trucos para lograr encapsulación. También es posible crear propiedades nuevas en la ejecución del mismo constructor

// Constructor
function NombreClase {
    // método o propiedad privada
    var privado = 'valor';
    // Esta propiedad se creará en todas las instancias
    this.metodo = function() {};
}

Esto tiene la desventaja de que ahora existe una copia diferente del método por cada objeto por lo que el prototipo es empleado para hacer más eficiente la creación de métodos ya que todas las instancias comparten el mismo método.

Como es evidente el lenguaje es muy permisivo y no tiene un diseño de clases como se especifica usualmente en la programación orientada a objetos por lo que simular una clase ha generado un sinnúmero de implementaciones diferentes, todas ellas enfocadas en compensar las carencias del lenguaje.

Básicamente existen dos formas de crear una clase y todas las librerías que pueden utilizarse usarán internamente uno u otro método.

Closure

// Constructor
function ClasePadre(nombre) {
  var ref = this;

  // Métodos de la clase
  ref.nombre = nombre;
  ref.imprime = function() {
    console.log(ref.toString());
  };

  // Métodos a sobreescribir
  ref.toString = function() {
    return 'Mi nombre es ' + ref.nombre;
  };
}

// Constructor
function Clase(nombre, apellido) {
  var ref = this;
  var base = {};
  ref.apellido = apellido;

  // Herencia
  // Ejecutando la funcion padre pero cambiando su valor this
  // y usando el de la función actual
  ClasePadre.call(ref, nombre);

  // Guardando referencia a los métodos que se sobreescriben
  // Aquí ref.toString contiene el método heredado de la clase padre
  base.toString = ref.toString;
  // Sobreescribiendo el método con la implementación particular
  // Accediendo a los métodos de la clase base
  ref.toString = function() {
    return base.toString() + ' y mi apellido es ' + ref.apellido;
  };
};

var padre = new ClasePadre('José');
padre.imprime();

var hijo = new Clase('José', 'Perez');
hijo.imprime();

Como ves no se usa el prototipo en ningúna parte y esto tiene la desventaja inmediata que el operador instanceof deja de funcionar. Además crear métodos en el constructor es menos eficiente como mencioné anteriormente. La ventaja es que usualmente en javascript debes hacer cosas como esta setTimeout(hijo.imprime.bind(hijo), 1000) por la forma como funciona la palabra clave this y en este caso puedes ejecutar directamente setTimeout(hijo.imprime, 1000).

// Constructor
function ClasePadre(nombre) {
  var ref = this;

  ref.nombre = nombre;
  ref.imprime = function() {
    console.log(ref.toString());
  };

  ref.toString = function() {
    return 'Mi nombre es ' + ref.nombre;
  };
}

// Constructor
function Clase(nombre, apellido) {
  var ref = this;
  var base = {};
  ref.apellido = apellido;

  // Herencia
  // Ejecutando la funcion padre pero cambiando su valor this
  // y usando el de la función actual
  ClasePadre.call(ref, nombre);

  // Guardando referencia a los métodos que se sobreescriben
  // Aquí ref.toString contiene el método heredado de la clase padre
  base.toString = ref.toString;
  // Sobreescribiendo el método con la implementación particular
  ref.toString = function() {
    return 'Mi nombre y apellidos son ' + ref.nombre + ' ' + ref.apellido;
  };
};

var hijo = new Clase('José', 'Perez');
setTimeout(hijo.imprime, 1000);
console.log('Instancia de Clase', hijo instanceof Clase);
console.log('Instancia de ClasePadre', hijo instanceof ClasePadre);

Prototipo

Esta es la variante preferida y te encontrarás muchas implementaciones que se comportan ligeramente diferente pero básicamente hacen algo como esto

// Constructor
function ClasePadre(nombre) {
  this.nombre = nombre;
}

// Métodos heredables de la clase padre
ClasePadre.prototype.imprime = function() {
  console.log(this.toString());
};

ClasePadre.prototype.toString = function() {
  return 'Mi nombre es ' + this.nombre;
};

// Constructor
function Clase(nombre, apellido) {
  var ref = this;
  ref.apellido = apellido;

  // Herencia
  // Ejecutando el constructor de la clase padre
  ClasePadre.call(ref, nombre);
}

Clase.prototype = new ClasePadre();

Clase.prototype.toString = function() {
  // Accediendo a los métodos de la clase base
  return ClasePadre.prototype.toString.call(this) + ' y mi apellido es ' + this.apellido;
};

var padre = new ClasePadre('José');
padre.imprime();

var hijo = new Clase('José', 'Perez');
hijo.imprime();

Como puedes ver toda la definición es radicalmente diferente a la definición estandard de una clase ya que se encuentra dispersa y no es muy fácil de razonar. El truco de este método siempre consiste en reemplazar o modificar la propiedad prototype de la función(clase) para establecer una cadena de herencia ya que en javascript cuando una propiedad es referenciada el interprete busca en su prototipo y en el prototipo de su padre y así sucesivamente hasta que llega a Object.prototype.

cadena de herencia

Las soluciones giran siempre en torno a este mecanismo. Aquí te dejo la implementación de util.inherits de node que usa el método Object.create para establecer el prototipo. Este tiene la peculiar característica de usar una propiedad super_ para almacenar una referencia a la clase padre para su posterior uso.

function inherits(ctor, superCtor) {
  if (ctor === undefined || ctor === null)
    throw new TypeError('The constructor to `inherits` must not be ' +
      'null or undefined.');

  if (superCtor === undefined || superCtor === null)
    throw new TypeError('The super constructor to `inherits` must not ' +
      'be null or undefined.');

  if (superCtor.prototype === undefined)
    throw new TypeError('The super constructor to `inherits` must ' +
      'have a prototype.');

  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
}

Clases ES6

Estas clases son azúcar sintáctico sobre la herencia prototípica tradicional de javascript.

Las clases de javascript están introducidas en ECMAScript 6 y son azúcar sintáctica sobre la existente herencia basada en prototipos de JavaScript. La sintaxis de las clases no introduce un nuevo modelo de herencia orientada a objetos a JavaScript. Las clases de JavaScript proveen una sintaxis mucho más clara y simple para crear objetos y lidiar con la herencia.

class ClasePadre {
  constructor(nombre) {
    this.nombre = nombre;
  }

  imprime() {
    console.log(this.toString());
  }

  toString() {
    return 'Mi nombre es ' + this.nombre;
  };
}

class Clase extends ClasePadre {
  constructor(nombre, apellido) {
    super(nombre);
    this.apellido = apellido;
  }

  toString() {
    return 'Mi nombre y apellidos son ' + this.nombre + ' ' + this.apellido;
  };
}

var padre = new ClasePadre('José');
padre.imprime();

var hijo = new Clase('José', 'Perez');
hijo.imprime();

Mucho más limpio y fácil de entender pero sigue siendo herencia de prototipos hasta el punto que es posible heredar de una simple función

function ClasePadre(nombre) {
  this.nombre = nombre;
}

ClasePadre.prototype.imprime = function() {
  console.log(this.toString());
}

ClasePadre.prototype.toString = function() {
  return 'Mi nombre es ' + this.nombre;
};

class Clase extends ClasePadre {
  constructor(nombre, apellido) {
    super(nombre);
    this.apellido = apellido;
  }

  toString() {
    return 'Mi nombre y apellidos son ' + this.nombre + ' ' + this.apellido;
  };
}

var padre = new ClasePadre('José');
padre.imprime();

var hijo = new Clase('José', 'Perez');
hijo.imprime();

Aquí hay una lista de las librerías más notables que implementan OOP directamente en javascript o que compilan a este

devconcept
  • 12,541
  • 3
  • 39
  • 56
14

Bueno considero que las 3 son útiles, el problema esta es con que estamos mas cómodo al momento del trabajar.

Clausuras

Yo me siento cómodo trabajar con estas, ya que si nos vamos a los patrones de diseno es facil montar un Patrón Modular. Permite una lectura fácil del codigo. Se separa los conceptos de funciones publicas y privadas. y es lo mas parecido a una clase.

Prototipo (prototype)

El prototype me parece mas básico,lo que lo hace mas poderoso es el tema de poder cambiar una función especifica sin necesidad que modifique el objeto principal, se hace mas complejo el manejo de funciones publicas y privada.

Objeto

Es un simple objeto, no estas creando una clase como tal.

Mi recomendación es trabajar con las Clausuras, ya que es lo mas parecido a una clase, si trabajas con programación orientada a objeto se hará fácil la lectura y creación de las mismas. Con temas de rendimiento considero que es depende de la complejidad que le des pero no creo que exista mucha diferencia.

Wilfredo
  • 2,455
  • 3
  • 20
  • 35
11

Crea una clase nativa de JavaScript.

Entre las muchas cosas que introduce, ECMAScript 6 incluye definición de clases mediante la palabra clave class:

class NombreClase {
    constructor (arg1, arg2) {
        this.arg1 = arg1;
        this.arg2 = arg2;
    }

    metodo () {
        // código
    }

    static metodoEstatico () {
        // código
    }
}

Si estás trabajando con Node.js, las clases están disponibles en las últimas versiones. En versiones anteriores están disponibles mediante la opción --harmony.

El problema suele venir al querer hacer uso de estas características en el navegador, en cuyo caso es recomendable usar Babel, un compilador de ES6 a ES5 que soporta clases, entre otras cosas. Babel también está disponible para Node.js si tus requisitos incluyen compatibilidad con ES5.

Darkhogg
  • 929
  • 5
  • 15
8

En el 3er caso no se está simulando una clase ya que simplemente se tiene un único objeto con propiedades y funciones.

En el 2do, al asignar las funciones al prototype se están definiendo los métodos en el prototipo de la función lo cual significa que existe una única copia de los métodos para todas las instancias de la "clase", de forma similar como se implementa en otros lenguajes.

En el 1er caso no solo se logra lo anterior sino que al haber una clausura se permite encapsular también métodos privados que no tienen alcance fuera de la función mas externa, con lo cual es lo más parecido a una clase.

Excepto por el método privado esta forma es como TypeScript, que es un superconjunto de JavaScript compila sus clases a JavaScript.

Typescript:

class NombreClase {
    metodo(){
        //código
    }

    static metodoEstático(){
        //código
    }
}

JavaScript:

var NombreClase = (function () {
    function NombreClase() {
    }
    NombreClase.prototype.metodo = function () {
        //código
    };
    NombreClase.metodoEstático = function () {
        //código
    };
    return NombreClase;
})();

Nota: Cabe mencionar también que la sintaxis de clases de TypeScript (sin contar las anotaciones de tipos) coincide con la sintaxis de ECMAScript 2015 (anteriormente conocido como ECMAScript 6) por lo que si tu plataforma objetivo es antigua puedes utilizar TypeScript y si la plataforma es suficientemente moderna puedes utilizar JavaScript en ambos casos con la misma sintaxis.

Carlos Muñoz
  • 12,864
  • 2
  • 42
  • 62
6

No sabría decirte cual es la mejor o la peor forma de crear clases en Javascript, puesto que se volvería un tema de discusión dada las preferencias de un programador determinado.

En cuanto a tema de rendimiento, no se exactamente en que infiera una u otra forma de crear la clase, pensaría que es el mismo rendimiento para cualquiera de ellas, lo que tienes que pensar en realidad es en el rendimiento en el llamado a métodos y obtención de data que cambia muy poco, por ejemplo, una buena practica es implementando un cache en javascript

En cuanto a la creación de clases en javascript, sea como sea que emules la clase, te recomiendo que la crees dentro de contenedores/namespaces.

Namespace

Un espacio de nombres es un contenedor que permite asociar toda la funcionalidad de un determinado objeto con un nombre único. En JavaScript un espacio de nombres es un objeto que permite a métodos, propiedades y objetos asociarse. La idea de crear espacios de nombres en JavaScript es simple: Crear un único objeto global para las variables, métodos, funciones convirtiendolos en propiedades de ese objeto. El uso de los namespace permite minimizar el conflicto de nombres con otros objetos haciendolos únicos dentro de nuestra aplicación.

// namespace global
var MIAPLICACION = MIAPLICACION || {};

Te voy a dejar como referencia este articulo Programación orientada a objetos de JavaScript que me encontré en la web, quizás allí encuentres la mejor forma de crear una clase en javascripts.

5

Si tu código va a escalar tanto como para necesitar clases mi recomendación es que no le des más vueltas e inicies con

TypeScript

TypeScript es un superset del lenguaje javascript que incorpora características POO y strong typing. Al generar un proyecto TypeScript lo que resulta de la 'compilación' es simple javascript standard de alto rendimiento y funcional.

TypeScript ya incorpora full soporte para ECMA Script 6 y ha sido adoptado por los grandes actores de la industria como

  • Microsoft
  • Google / AngularJS
  • Ebay

y un sin fin más

Es muy fácil de aprender porque al ser un superset todo lo que ya sabes de javascript lo puedes seguirlo usando.

Tutorial TypeScript

Ejemplos

JuanK
  • 2,437
  • 12
  • 35
  • 1
    Apoyo TypeScript, y un plus: si no tienes intenciones de usar un transpiler puedes usar como IDE [Visual Studio Community 2015](https://www.visualstudio.com/) y cada ves que guardes cambios en tus archivos .ts se generará un .js equivalente con código javascript que cualquier navegador puede ejecutar. – Henry Rodriguez Dec 14 '15 at 17:23
4

En Ceylon usamos el segundo enfoque, el de prototipo, pero el constructor es una función privada. Un tipo lo definimos así:

function MiClase(inst) {
  $init$MiClase();
  if (inst===undefined)inst=new MiClase.$$;
  //pegarle propiedades
  return inst;
}
function $init$MiClase() {
  if (MiClase.$$===undefined) {
    MiClase.$$=function(){};
    (function(miclase) {
      //pegar cosas al prototipo
      miclase.metodo=function metodo(){};
    })(MiClase.$$.prototype);
  }
}

Para crear una instancia:

var miclase=MiClase();

Este diseño lo hicimos basado en el libro "JavaScript: The Good parts", de Douglas Crockford. Junto con algunos mecanismos adicionales, nos permite incluso implementar herencia múltiple.

César
  • 16,990
  • 6
  • 37
  • 76
Chochos
  • 376
  • 2
  • 5
0

Una buena opción es usar babeljs para poder usar características de es6/7 como son las clases.

https://babeljs.io/

esto viene siendo una forma de trabajar muy popular en el ultimo tiempo y puedes usar babeljs junto a webpack o browserify para compilar y optimizar todo tu codigo en un buddle unico.

-5

He llegado a la conclusión de que hay 3 formas buenas de hacerlo. y creé el siguiente post sobre el tema:

http://ricardogeek.com/3-formas-de-definir-clases-en-javascript/

Espero que te ayude :)

César
  • 16,990
  • 6
  • 37
  • 76
RicardoE
  • 111
  • 2
  • 5
    Estaría bien si en la respuesta incluyeras un resúmen o algunos fragmentos significativos del post que enlazas. – Konamiman Dec 14 '15 at 09:06