¿Qué técnicas se pueden utilizar para definir una clase en JavaScript y cuáles son sus compensaciones?

686

Prefiero usar OOP en proyectos a gran escala como el que estoy trabajando en este momento. Necesito crear varias clases en JavaScript pero, si no me equivoco, hay al menos un par de formas de hacerlo. ¿Cuál sería la sintaxis y por qué se haría de esa manera?

Me gustaría evitar el uso de bibliotecas de terceros, al menos al principio.
Buscando otras respuestas, encontré el artículo Programación orientada a objetos con JavaScript, Parte I: Herencia - Doc JavaScript que analiza la programación orientada a objetos en JavaScript. ¿Hay una mejor manera de hacer la herencia?

Karim
fuente
nota: este es un duplicado de stackoverflow.com/questions/355848
Jason S
3
Personalmente, me gusta declarar miembros de la clase dentro del cuerpo de la función. Utilizo la técnica de 'arreglar esto' para crear un cierre y hacer que se comporte más como una clase. Tengo un ejemplo detallado en mi blog: ncombo.wordpress.com/2012/12/30/…
Jon
Porté la mayoría de la funcionalidad de C ++ OOP a JavaScript con una sintaxis simple y natural. Vea mi respuesta aquí: stackoverflow.com/a/18239463/1115652
No hay clases en JavaScript. Pero si desea simular un comportamiento de clase en JS, puede hacerlo. Ver detalles en: symfony-world.blogspot.com/2013/10/…
ducin

Respuestas:

743

Aquí está la forma de hacerlo sin usar ninguna biblioteca externa:

// Define a class like this
function Person(name, gender){

   // Add object properties like this
   this.name = name;
   this.gender = gender;
}

// Add methods like this.  All Person objects will be able to invoke this
Person.prototype.speak = function(){
    alert("Howdy, my name is" + this.name);
};

// Instantiate new objects with 'new'
var person = new Person("Bob", "M");

// Invoke methods like this
person.speak(); // alerts "Howdy, my name is Bob"

Ahora la respuesta real es mucho más compleja que eso. Por ejemplo, no existen las clases en JavaScript. JavaScript utiliza un prototypeesquema de herencia basado en.

Además, existen numerosas bibliotecas populares de JavaScript que tienen su propio estilo de aproximación a la funcionalidad de clase en JavaScript. Querrás ver al menos Prototype y jQuery .

Decidir cuál de estos es el "mejor" es una excelente manera de comenzar una guerra santa en Stack Overflow. Si te embarcas en un proyecto más grande con mucho JavaScript, definitivamente vale la pena aprender una biblioteca popular y hacerlo a su manera. Soy un prototipo, pero Stack Overflow parece inclinarse hacia jQuery.

En la medida en que solo haya "una forma de hacerlo", sin ninguna dependencia de bibliotecas externas, la forma en que escribí es más o menos eso.

Tríptico
fuente
48
Pero no funciona como el lenguaje X, donde aprendí la única forma verdadera en que una cosa que se usa para crear instancias de objetos debería funcionar :(
Erik Reppen
2
De acuerdo con developer.mozilla.org/en-US/docs/Web/JavaScript/…, las propiedades también deben agregarse al prototipo ("Person.prototype.name = '';")
DaveD
1
@DaveD - tal vez lo hizo, pero ya no parece ...
Kieren Johnstone
66
¿jQuery ni siquiera proporciona ninguna forma de crear una funcionalidad de clase? (Todas las clases que tiene son clases CSS) Debe eliminarlo de esa parte de la respuesta.
Bergi
77
Desde la segunda mitad de 2015 se lanzó el nuevo estándar EcmaScript 6, por lo que propongo hacerlo de la nueva manera (mucho más limpia y fácil
DevWL
213

La mejor manera de definir una clase en JavaScript es no definir una clase.

Seriamente.

Hay varios sabores diferentes de orientación a objetos, algunos de ellos son:

  • OO basado en clases (introducido por primera vez por Smalltalk)
  • OO basado en prototipo (introducido por primera vez por Self)
  • OO basado en varios métodos (creo que introdujo por primera vez CommonLoops)
  • OO basado en predicados (ni idea)

Y probablemente otros que no conozco.

JavaScript implementa OO basado en prototipos. En OO basado en prototipos, los nuevos objetos se crean copiando otros objetos (en lugar de crear una instancia de una plantilla de clase) y los métodos viven directamente en objetos en lugar de en clases. La herencia se realiza por delegación: si un objeto no tiene un método o propiedad, se busca en sus prototipos (es decir, el objeto del que se clonó), luego los prototipos del prototipo y así sucesivamente.

En otras palabras: no hay clases.

JavaScript en realidad tiene un buen ajuste de ese modelo: constructores. No solo puede crear objetos copiando los existentes, sino que también puede construirlos "de la nada", por así decirlo. Si llama a una función con la newpalabra clave, esa función se convierte en un constructor y la thispalabra clave no apuntará al objeto actual sino a uno recién creado "vacío". Por lo tanto, puede configurar un objeto de la forma que desee. De esa manera, los constructores de JavaScript pueden asumir uno de los roles de las clases en OO tradicional basado en clases: servir como plantilla o modelo para nuevos objetos.

Ahora, JavaScript es un lenguaje muy poderoso, por lo que es bastante fácil implementar un sistema OO basado en clases dentro de JavaScript si lo desea. Sin embargo, solo debe hacer esto si realmente lo necesita y no solo porque así es como lo hace Java.

Jörg W Mittag
fuente
"Si llama a una función con la nueva palabra clave, esa función se convierte en un constructor y esta palabra clave no apuntará al objeto actual sino a un" vacío "recién creado. Si llama a una función sin la nueva palabra clave, esto se referirá al contexto de llamada, por defecto el objeto global (ventana). En modo estricto, indefinido es el valor predeterminado. llamar, aplicar y vincular toma el contexto de la llamada como primer parámetro. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Elias Hasle
83

Clases ES2015

En la especificación ES2015, puede usar la sintaxis de clase que es solo azúcar sobre el sistema prototipo.

class Person {
  constructor(name) {
    this.name = name;
  }
  toString() {
    return `My name is ${ this.name }.`;
  }
}

class Employee extends Person {
  constructor(name, hours) {
    super(name);
    this.hours = hours;
  }
  toString() {
    return `${ super.toString() } I work ${ this.hours } hours.`;
  }
}

Beneficios

El principal beneficio es que las herramientas de análisis estático encuentran más fácil orientar esta sintaxis. También es más fácil para otros que vienen de lenguajes basados ​​en clases usar el lenguaje como políglota.

Advertencias

Tenga cuidado con sus limitaciones actuales. Para lograr propiedades privadas, uno debe recurrir al uso de símbolos o mapas débiles . En futuras versiones, las clases probablemente se ampliarán para incluir estas características faltantes.

Apoyo

La compatibilidad con el navegador no es muy buena en este momento (es compatible con casi todos excepto IE), pero ahora puede usar estas funciones con un transpilador como Babel .

Recursos

Valle
fuente
56

Prefiero usar Daniel X. Moore's {SUPER: SYSTEM}. Esta es una disciplina que proporciona beneficios tales como variables de instancia verdaderas, herencia basada en rasgos, jerarquías de clases y opciones de configuración. El siguiente ejemplo ilustra el uso de variables de instancia verdaderas, que creo que es la mayor ventaja. Si no necesita variables de instancia y está satisfecho con solo variables públicas o privadas, entonces probablemente haya sistemas más simples.

function Person(I) {
  I = I || {};

  Object.reverseMerge(I, {
    name: "McLovin",
    age: 25,
    homeState: "Hawaii"
  });

  return {
    introduce: function() {
      return "Hi I'm " + I.name + " and I'm " + I.age;
    }
  };
}

var fogel = Person({
  age: "old enough"
});
fogel.introduce(); // "Hi I'm McLovin and I'm old enough"

Wow, eso no es realmente muy útil por sí solo, pero mira agregar una subclase:

function Ninja(I) {
  I = I || {};

  Object.reverseMerge(I, {
    belt: "black"
  });

  // Ninja is a subclass of person
  return Object.extend(Person(I), {
    greetChallenger: function() {
      return "In all my " + I.age + " years as a ninja, I've never met a challenger as worthy as you...";
    }
  });
}

var resig = Ninja({name: "John Resig"});

resig.introduce(); // "Hi I'm John Resig and I'm 25"

Otra ventaja es la capacidad de tener módulos y herencia basada en rasgos.

// The Bindable module
function Bindable() {

  var eventCallbacks = {};

  return {
    bind: function(event, callback) {
      eventCallbacks[event] = eventCallbacks[event] || [];

      eventCallbacks[event].push(callback);
    },

    trigger: function(event) {
      var callbacks = eventCallbacks[event];

      if(callbacks && callbacks.length) {
        var self = this;
        callbacks.forEach(function(callback) {
          callback(self);
        });
      }
    },
  };
}

Un ejemplo de tener la clase de persona incluye el módulo enlazable.

function Person(I) {
  I = I || {};

  Object.reverseMerge(I, {
    name: "McLovin",
    age: 25,
    homeState: "Hawaii"
  });

  var self = {
    introduce: function() {
      return "Hi I'm " + I.name + " and I'm " + I.age;
    }
  };

  // Including the Bindable module
  Object.extend(self, Bindable());

  return self;
}

var person = Person();
person.bind("eat", function() {
  alert(person.introduce() + " and I'm eating!");
});

person.trigger("eat"); // Blasts the alert!

Divulgación: Soy Daniel X. Moore y este es mi {SUPER: SYSTEM}. Es la mejor manera de definir una clase en JavaScript.

Daniel X Moore
fuente
@DanielXMoore "Las variables de instancia se comparten entre instancias individuales de una clase" Esas no son variables de instancia, son variables estáticas / de clase.
JAB
2
@JAB Eso es incorrecto, las variables estáticas / de clase se comparten entre todas las instancias de una clase. Cada instancia tiene sus propias variables de instancia.
Daniel X Moore
(Dicho de otra manera, usando el significado normal del término "variable de instancia", si una variable es una o no es ortogonal al nivel de accesibilidad de la variable.)
JAB
2
Casi parecías un superhéroe por reclamar el mejor xD
Dadan
Un enfoque sencillo para definir una clase de Javascript utilizando objetos de JavaScript: wapgee.com/story/i/203
Ilyas karim
41
var Animal = function(options) {
    var name = options.name;
    var animal = {};

    animal.getName = function() {
        return name;
    };

    var somePrivateMethod = function() {

    };

    return animal;
};

// usage
var cat = Animal({name: 'tiger'});
liammclennan
fuente
Esta es una manera muy elegante de construir una estructura de objeto utilizable sin tener que importar nada. Estaba usando el sistema de clases de Resig, pero puede que me guste más. Gracias.
Tim Scollick
29
El problema con este enfoque es que cada vez que cree una nueva instancia de Animal redefinirá las funciones en lugar de solo definirlas una vez con el prototipo.
Justin
33

Las siguientes son las formas de crear objetos en JavaScript, que he usado hasta ahora

Ejemplo 1:

obj = new Object();
obj.name = 'test';
obj.sayHello = function() {
    console.log('Hello '+ this.name);
}

Ejemplo 2

obj = {};
obj.name = 'test';
obj.sayHello = function() {
    console.log('Hello '+ this.name);
}
obj.sayHello();

Ejemplo 3

var obj = function(nameParam) {
    this.name = nameParam;
}
obj.prototype.sayHello = function() {
    console.log('Hello '+ this.name);
}

Ejemplo 4: Beneficios reales de Object.create (). por favor consulte [este enlace]

var Obj = {
    init: function(nameParam) {
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var usrObj = Object.create(Obj);  // <== one level of inheritance

usrObj.init('Bob');
usrObj.sayHello();

Ejemplo 5 (Crockford's Object.create personalizado):

Object.build = function(o) {
   var initArgs = Array.prototype.slice.call(arguments,1)
   function F() {
      if((typeof o.init === 'function') && initArgs.length) {
         o.init.apply(this,initArgs)
      }
   }
   F.prototype = o
   return new F()
}
MY_GLOBAL = {i: 1, nextId: function(){return this.i++}}  // For example

var userB = {
    init: function(nameParam) {
        this.id = MY_GLOBAL.nextId();
        this.name = nameParam;
    },
    sayHello: function() {
        console.log('Hello '+ this.name);
    }
};
var bob = Object.build(userB, 'Bob');  // Different from your code
bob.sayHello();


Para mantener la respuesta actualizada con ES6 / ES2015

Una clase se define así:

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

    toString() {
        return '((Class::Person) named ' + this.name + ' & of age ' + this.age + ')';
    }
}

let objPerson = new Person("Bob",33);
console.log(objPerson.toString());
Amol M Kulkarni
fuente
1
@Justin: Por favor, hágame saber lo que no es válido?
Amol M Kulkarni
Mientras estudiaba estas anotaciones, también me topé con this.set (). Por ejemplo: this.set ('puerto', 3000). Supongo que esto se usa para establecer la propiedad del puerto para el objeto. Si es así, ¿por qué no usamos directamente: {port: 3000}? ¿Hay alguna documentación donde pueda obtener más detalles?
adityah
24

Creo que deberías leer la herencia prototípica de Douglas Crockford en JavaScript y la herencia clásica en JavaScript .

Ejemplos de su página:

Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    return this;
};

¿Efecto? Te permitirá agregar métodos de una manera más elegante:

function Parenizor(value) {
    this.setValue(value);
}

Parenizor.method('setValue', function (value) {
    this.value = value;
    return this;
});

También recomiendo sus videos: JavaScript avanzado .

Puede encontrar más videos en su página: http://javascript.crockford.com/ En el libro de John Reisig puede encontrar muchos ejemplos del sitio web de Douglas Crockfor.

Jarek
fuente
25
¿Se trata sólo de mí? ¿Cómo diablos es esto más elegante? 'strings'
Llamaría a las
44
@JAB, pero la reflexión es la excepción, no la regla. Con el método anterior, debe declarar todos sus métodos con cadenas.
Kirk Woll
16

Debido a que no admitiré el plan de fábrica de YUI / Crockford y porque me gusta mantener las cosas autónomas y extensibles, esta es mi variación:

function Person(params)
{
  this.name = params.name || defaultnamevalue;
  this.role = params.role || defaultrolevalue;

  if(typeof(this.speak)=='undefined') //guarantees one time prototyping
  {
    Person.prototype.speak = function() {/* do whatever */};
  }
}

var Robert = new Person({name:'Bob'});

donde idealmente el tipo de prueba está en algo como el primer método prototipo

annakata
fuente
Me gusta. La mayoría de las veces uso la sintaxis estándar de JS porque no me gusta la idea de copiar funciones en cada instancia de objeto. Sin embargo, siempre extrañé la belleza de la solución autónoma, y ​​esto la resuelve bastante bien.
Lukasz Korzybski
1
No estoy seguro, pero entendí que definir la función prototipo dentro del alcance (algo así como un cierre) de una función da como resultado una pérdida de memoria ya que el recolector de basura no puede llegar allí en la instancia de esas clases.
Sanne
15

Si lo que busca es simple, puede evitar la palabra clave "nueva" por completo y simplemente usar métodos de fábrica. Prefiero esto, a veces, porque me gusta usar JSON para crear objetos.

function getSomeObj(var1, var2){
  var obj = {
     instancevar1: var1,
     instancevar2: var2,
     someMethod: function(param)
     {  
          //stuff; 
     }
  };
  return obj;
}

var myobj = getSomeObj("var1", "var2");
myobj.someMethod("bla");

Sin embargo, no estoy seguro de cuál es el rendimiento para los objetos grandes.

Sam
fuente
La línea obj.instancevar1 = var1 no es necesaria, ya que el objeto interno tendrá acceso a los parámetros de getSomeObj ().
Tríptico
Guau. Eso me duele el cerebro, pero tiene cierta elegancia. Entonces, la parte "obj.instancevar1 = var1" es el comienzo de una especie de constructor, supongo.
Karim
Acabo de ver el comentario de Triptych. Veo. Por lo tanto, podría hacer algo como "instancevar1: var1" donde se instancia el objeto interno.
Karim
Exactamente ... cuando usa {} para definir un objeto, tiene acceso a las variables que están actualmente en el alcance.
Sam
10
Con este enfoque, pierde la capacidad de heredar, y dado que no está usando obj.prototype.something, está definiendo las funciones cada vez que usa el objeto = más memoria y más lento.
algunos
12
var Student = (function () {
    function Student(firstname, lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.fullname = firstname + " " + lastname;
    }

    Student.prototype.sayMyName = function () {
        return this.fullname;
    };

    return Student;
}());

var user = new Student("Jane", "User");
var user_fullname = user.sayMyName();

Esa es la forma en que TypeScript compila la clase con el constructor a JavaScript.

Mick
fuente
10

La forma simple es:

function Foo(a) {
  var that=this;

  function privateMethod() { .. }

  // public methods
  that.add = function(b) {
    return a + b;
  };
  that.avg = function(b) {
    return that.add(b) / 2; // calling another public method
  };
}

var x = new Foo(10);
alert(x.add(2)); // 12
alert(x.avg(20)); // 15

El motivo thates que thispuede vincularse a otra cosa si le da un método como controlador de eventos, por lo que guarda el valor durante la creación de instancias y lo usa más tarde.

Editar: definitivamente no es la mejor manera, solo una manera simple. ¡Estoy esperando buenas respuestas también!

orip
fuente
1
El that = esta construcción no es necesaria aquí. Además, los métodos add () y avg () se copiarán para cada "instancia" de la clase Foo, en lugar de compartirlos entre ellos.
Tríptico
1
Es necesario (más o menos) en ese caso, pero no el caso simple que ha proporcionado.
Tríptico
9

Probablemente desee crear un tipo utilizando el Patrón de plegado:

    // Here is the constructor section.
    var myType = function () {
        var N = {}, // Enclosed (private) members are here.
            X = this; // Exposed (public) members are here.

        (function ENCLOSED_FIELDS() {
            N.toggle = false;
            N.text = '';
        }());

        (function EXPOSED_FIELDS() {
            X.count = 0;
            X.numbers = [1, 2, 3];
        }());

        // The properties below have access to the enclosed fields.
        // Careful with functions exposed within the closure of the
        // constructor, each new instance will have it's own copy.
        (function EXPOSED_PROPERTIES_WITHIN_CONSTRUCTOR() {
            Object.defineProperty(X, 'toggle', {
                get: function () {
                    var before = N.toggle;
                    N.toggle = !N.toggle;
                    return before;
                }
            });

            Object.defineProperty(X, 'text', {
                get: function () {
                    return N.text;
                },
                set: function (value) {
                    N.text = value;
                }
            });
        }());
    };

    // Here is the prototype section.
    (function PROTOTYPE() {
        var P = myType.prototype;

        (function EXPOSED_PROPERTIES_WITHIN_PROTOTYPE() {
            Object.defineProperty(P, 'numberLength', {
                get: function () {
                    return this.numbers.length;
                }
            });
        }());

        (function EXPOSED_METHODS() {
            P.incrementNumbersByCount = function () {
                var i;
                for (i = 0; i < this.numbers.length; i++) {
                    this.numbers[i] += this.count;
                }
            };
            P.tweak = function () {
                if (this.toggle) {
                    this.count++;
                }
                this.text = 'tweaked';
            };
        }());
    }());

Ese código le dará un tipo llamado myType . Tendrá campos privados internos llamados alternar y texto . También tendrá estos miembros expuestos: los campos cuenta y números ; las propiedades alternar , texto y númeroLongitud ; los métodos incrementanNumbersByCount y ajustan .

El patrón de plegado se detalla aquí: Patrón de plegado de Javascript

intrepidis
fuente
3

Codifique golf para la respuesta de @ liammclennan .

var Animal = function (args) {
  return {
    name: args.name,

    getName: function () {
      return this.name; // member access
    },

    callGetName: function () {
      return this.getName(); // method call
    }
  };
};

var cat = Animal({ name: 'tiger' });
console.log(cat.callGetName());

tponthieux
fuente
2

MooTools (My Object Oriented Tools) se centra en la idea de clases . Incluso puede extender e implementar con herencia.

Cuando se domina, se convierte en javascript ridículamente reutilizable y potente.

Ryan Florence
fuente
2

Clases basadas en objetos con herencia

var baseObject = 
{
     // Replication / Constructor function
     new : function(){
         return Object.create(this);   
     },

    aProperty : null,
    aMethod : function(param){
      alert("Heres your " + param + "!");
    },
}


newObject = baseObject.new();
newObject.aProperty = "Hello";

anotherObject = Object.create(baseObject); 
anotherObject.aProperty = "There";

console.log(newObject.aProperty) // "Hello"
console.log(anotherObject.aProperty) // "There"
console.log(baseObject.aProperty) // null

Sencillo, dulce y listo.

Ulad Kasach
fuente
1

Una base

function Base(kind) {
    this.kind = kind;
}

Una clase

// Shared var
var _greeting;

(function _init() {
    Class.prototype = new Base();
    Class.prototype.constructor = Class;
    Class.prototype.log = function() { _log.apply(this, arguments); }
    _greeting = "Good afternoon!";
})();

function Class(name, kind) {
    Base.call(this, kind);
    this.name = name;
}

// Shared function
function _log() {
    console.log(_greeting + " Me name is " + this.name + " and I'm a " + this.kind);
}

Acción

var c = new Class("Joe", "Object");
c.log(); // "Good afternoon! Me name is Joe and I'm a Object"
Mikael Dúi Bolinder
fuente
1

Basado en el ejemplo de Triptych, esto podría ser incluso más simple:

    // Define a class and instantiate it
    var ThePerson = new function Person(name, gender) {
        // Add class data members
        this.name = name;
        this.gender = gender;
        // Add class methods
        this.hello = function () { alert('Hello, this is ' + this.name); }
    }("Bob", "M"); // this instantiates the 'new' object

    // Use the object
    ThePerson.hello(); // alerts "Hello, this is Bob"

Esto solo crea una instancia de objeto único, pero sigue siendo útil si desea encapsular un montón de nombres para variables y métodos en una clase. Normalmente no habría argumentos "Bob, M" para el constructor, por ejemplo, si los métodos fueran llamadas a un sistema con sus propios datos, como una base de datos o una red.

Todavía soy demasiado nuevo con JS para ver por qué esto no usa la prototypecosa.

Roland
fuente
0

JavaScript está orientado a objetos , pero es radicalmente diferente a otros lenguajes OOP como Java, C # o C ++. No trates de entenderlo así. Tire ese viejo conocimiento y comience de nuevo. JavaScript necesita un pensamiento diferente.

Sugeriría obtener un buen manual o algo sobre el tema. Yo mismo encontré los Tutoriales ExtJS los mejores para mí, aunque no he usado el marco antes o después de leerlo. Pero da una buena explicación sobre qué es qué en el mundo de JavaScript. Lo sentimos, parece que ese contenido ha sido eliminado. Aquí hay un enlace a la copia archive.org en su lugar. Funciona hoy :PAGS

Vilx-
fuente
2
¿Orientado a objetos? Pensé que era funcional .
Peter Mortensen
El enlace "Tutoriales de ExtJS" está roto.
Peter Mortensen
Creo que sería más explicativo explicar que las funciones en javascript son objetos, y las reglas de javascript de alcance hacen que cada bloque de funciones se encapsule.
mibbit
-1

//new way using this and new
function Persons(name) {
  this.name = name;
  this.greeting = function() {
    alert('Hi! I\'m ' + this.name + '.');
  };
}

var gee=new Persons("gee");
gee.greeting();

var gray=new Persons("gray");
gray.greeting();

//old way
function createPerson(name){
 var obj={};
 obj.name=name;
 obj.greeting = function(){
 console.log("hello I am"+obj.name);
 }; 
  return obj;
}

var gita=createPerson('Gita');
gita.greeting();

Avinash Maurya
fuente