Quali tecniche possono essere utilizzate per definire una classe in JavaScript e quali sono i loro compromessi?


686

Preferisco usare OOP in progetti su larga scala come quello a cui sto lavorando in questo momento. Devo creare diverse classi in JavaScript ma, se non sbaglio, ci sono almeno un paio di modi per farlo. Quale sarebbe la sintassi e perché dovrebbe essere fatta in quel modo?

Vorrei evitare di usare librerie di terze parti, almeno all'inizio.
Alla ricerca di altre risposte, ho trovato l'articolo Programmazione orientata agli oggetti con JavaScript, Parte I: Eredità - Doc JavaScript che discute la programmazione orientata agli oggetti in JavaScript. Esiste un modo migliore per ereditare?


nota: questo è un duplicato di stackoverflow.com/questions/355848
Jason S

3
Personalmente, mi piace dichiarare i membri della classe all'interno del corpo della funzione. Uso la tecnica di "correzione di questo" per creare la chiusura in modo che si comporti più come una classe. Ho un esempio dettagliato sul mio blog: ncombo.wordpress.com/2012/12/30/…
Jon

Ho portato la maggior parte delle funzionalità OOP di C ++ su JavaScript con una sintassi semplice e naturale. Vedere la mia risposta qui: stackoverflow.com/a/18239463/1115652

Non ci sono classi in JavaScript. Ma se vuoi simulare un comportamento simile alle classi in JS, puoi farlo. Vedi i dettagli in: symfony-world.blogspot.com/2013/10/…
ducin,

Risposte:


743

Ecco il modo di farlo senza utilizzare alcuna libreria esterna:

// 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"

Ora la vera risposta è molto più complessa di così. Ad esempio, non esistono cose come le classi in JavaScript. JavaScript utilizza uno prototypeschema di ereditarietà basato.

Inoltre, ci sono numerose librerie JavaScript popolari che hanno il loro stile di funzionalità approssimative simili a classi in JavaScript. Ti consigliamo di controllare almeno Prototype e jQuery .

Decidere quale di questi è il "migliore" è un ottimo modo per iniziare una guerra santa con Stack Overflow. Se ti stai imbarcando in un progetto più ampio e pesante di JavaScript, vale sicuramente la pena imparare una libreria popolare e farlo a modo suo. Sono un ragazzo prototipo, ma Stack Overflow sembra inclinarsi verso jQuery.

Per quanto ci sia solo "un modo per farlo", senza dipendenze da librerie esterne, il modo in cui ho scritto è praticamente tutto.


48
Ma non funziona come il linguaggio X dove ho imparato l'unico vero modo in cui un oggetto usato per creare istanze di oggetti dovrebbe funzionare :(
Erik Reppen,

2
Secondo developer.mozilla.org/en-US/docs/Web/JavaScript/… anche le proprietà dovrebbero essere aggiunte al prototipo ("Person.prototype.name = '';")
DaveD

1
@DaveD - forse lo ha fatto, ma non sembra più ..?
Kieren Johnstone,

6
jQuery non fornisce nemmeno alcun modo per creare funzionalità di classe ??? (Tutte le classi che ha sono classi CSS) Dovresti rimuoverlo da quella parte della risposta.
Bergi,

7
Dalla seconda metà del 2015 è stato rilasciato il nuovo standard EcmaScript 6, quindi propongo di farlo nel modo nuovo (molto più semplice e pulito) es6-features.org/#ClassDefinition
DevWL

213

Il modo migliore per definire una classe in JavaScript è di non definire una classe.

Sul serio.

Esistono diversi tipi di orientamento agli oggetti, alcuni dei quali sono:

  • OO basato sulla classe (introdotto per la prima volta da Smalltalk)
  • OO basato su prototipo (introdotto per la prima volta da Self)
  • OO basato su metodi multipli (introdotto per la prima volta da CommonLoops, credo)
  • OO basato su predicato (nessuna idea)

E probabilmente altri che non conosco.

JavaScript implementa OO basato su prototipo. In OO basato su prototipo, i nuovi oggetti vengono creati copiando altri oggetti (anziché essere istanziati da un modello di classe) e i metodi vivono direttamente negli oggetti anziché nelle classi. L'ereditarietà viene effettuata tramite delega: se un oggetto non ha un metodo o una proprietà, viene cercato sui suoi prototipi (ovvero sull'oggetto da cui è stato clonato), quindi sui prototipi del prototipo e così via.

In altre parole: non ci sono classi.

JavaScript in realtà ha una bella modifica di quel modello: i costruttori. Non solo puoi creare oggetti copiando quelli esistenti, ma puoi anche costruirli "dal nulla", per così dire. Se chiamate una funzione con la newparola chiave, quella funzione diventa un costruttore e la thisparola chiave non punterà sull'oggetto corrente ma invece su uno "vuoto" appena creato. Quindi, puoi configurare un oggetto come preferisci. In questo modo, i costruttori JavaScript possono assumere uno dei ruoli delle classi nell'OO tradizionale basato sulle classi: fungere da modello o modello per nuovi oggetti.

Ora, JavaScript è un linguaggio molto potente, quindi è abbastanza facile implementare un sistema OO basato su classi all'interno di JavaScript, se lo si desidera. Tuttavia, dovresti farlo solo se ne hai davvero bisogno e non solo perché è così che lo fa Java.


"Se chiami una funzione con la nuova parola chiave, quella funzione diventa un costruttore e la parola chiave this non punta all'oggetto corrente ma invece a uno" vuoto "appena creato. Se si chiama una funzione senza la nuova parola chiave, questo farà riferimento al contesto chiamante, per impostazione predefinita l'oggetto globale (finestra). In modalità rigorosa, l'impostazione predefinita è indefinita. call, apply e bind accetta il contesto di chiamata come primo parametro. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Elias Hasle

83

Classi ES2015

Nella specifica ES2015, è possibile utilizzare la sintassi della classe che è semplicemente zucchero sul 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.`;
  }
}

Benefici

Il vantaggio principale è che gli strumenti di analisi statica trovano più facile indirizzare questa sintassi. È anche più facile per gli altri che provengono da lingue di classe utilizzare la lingua come poliglotta.

Avvertenze

Diffidare delle sue attuali limitazioni. Per ottenere proprietà private, è necessario ricorrere all'uso di Simboli o Mappe deboli . Nelle versioni future, le classi saranno molto probabilmente espanse per includere queste funzionalità mancanti.

Supporto

Il supporto del browser non è molto buono al momento (supportato da quasi tutti tranne IE), ma ora puoi usare queste funzionalità con un transpiler come Babel .

risorse


56

Preferisco usare Daniel X. Moore's {SUPER: SYSTEM}. Questa è una disciplina che offre vantaggi come variabili di istanza reali, ereditarietà basata sui tratti, gerarchie di classi e opzioni di configurazione. L'esempio che segue illustra l'uso di variabili di istanza reali, che ritengo sia il più grande vantaggio. Se non hai bisogno di variabili di istanza e sei soddisfatto solo delle variabili pubbliche o private, allora probabilmente ci sono sistemi più semplici.

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, non è molto utile da solo, ma dai un'occhiata all'aggiunta di una sottoclasse:

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"

Un altro vantaggio è la possibilità di avere ereditarietà basata su moduli e tratti.

// 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 esempio di avere la classe persona include il modulo associabile.

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!

Divulgazione: sono Daniel X. Moore e questo è mio {SUPER: SYSTEM}. È il modo migliore per definire una classe in JavaScript.


@DanielXMoore "Le variabili di istanza sono condivise tra le singole istanze di una classe" Quelle non sono variabili di istanza, quelle sono variabili statiche / di classe.
JAB

2
@JAB Che è errato, le variabili statiche / di classe sono condivise tra tutte le istanze di una classe. Ogni istanza ha le sue variabili di istanza.
Daniel X Moore,

(In altre parole, usando il normale significato del termine "variabile di istanza", indipendentemente dal fatto che una variabile sia o meno, è ortogonale al livello di accessibilità della variabile.)
JAB

2
Sembravi quasi un supereroe per aver rivendicato il miglior xD
Dadan,

Un approccio semplice per definire una classe Javascript usando oggetti 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'});

Questo è un modo molto elegante per costruire una struttura ad oggetti utilizzabile senza dover importare nulla. Stavo usando il sistema di classe di Resig, ma mi potrebbe piacere di più. Grazie.
Tim Scollick,

29
Il problema con questo approccio è che ogni volta che crei una nuova istanza di Animal ridefinirà le funzioni piuttosto che definirle solo una volta con il prototipo.
Giustino,

33

Di seguito sono riportati i modi per creare oggetti in JavaScript, che ho usato finora

Esempio 1:

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

Esempio 2:

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

Esempio 3:

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

Esempio 4: vantaggi effettivi di Object.create (). si prega di fare riferimento a [questo link]

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();

Esempio 5 (Object.create personalizzato di Crockford):

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();


Per mantenere la risposta aggiornata con ES6 / ES2015

Una classe è definita in questo modo:

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());

1
@Justin: Per favore fatemi sapere cosa non è valido?
Amol M Kulkarni,

Mentre studiavo queste notazioni, mi sono anche imbattuto in this.set (). Ad esempio: this.set ('port', 3000). Suppongo che questo sia usato per impostare la proprietà port per l'oggetto. In tal caso, perché non utilizziamo direttamente: {port: 3000}. C'è qualche documentazione in cui posso ottenere maggiori dettagli.
adityah,

24

Penso che dovresti leggere l' ereditarietà prototipo di Douglas Crockford in JavaScript e l'ereditarietà classica in JavaScript .

Esempi dalla sua pagina:

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

Effetto? Ti permetterà di aggiungere metodi in modo più elegante:

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

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

Consiglio anche i suoi video: JavaScript avanzato .

Puoi trovare altri video sulla sua pagina: http://javascript.crockford.com/ Nel libro di John Reisig puoi trovare molti esempi dal sito web di Douglas Crockfor.


25
Sono solo io? Come diamine è più elegante? Chiamerei 'strings'molte definizioni di funzioni con nome reale molte cose, ma elegant non è una di queste ...
fgysin ripristina Monica

4
@JAB, ma la riflessione è l'eccezione, non la regola. Con il metodo sopra, devi dichiarare tutti i tuoi metodi con stringhe.
Kirk Woll,

16

Perché non ammetterò il piano di fabbrica YUI / Crockford e perché mi piace mantenere le cose autosufficienti ed estensibili questa è la mia variazione:

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'});

dove idealmente il test di tipo è su qualcosa come il primo metodo prototipato


Mi piace. Uso molto spesso la sintassi standard di JS perché non mi piace l'idea di copiare funzioni in ogni istanza di oggetto. Mi è sempre mancata la bellezza della soluzione autonoma, e questo la risolve abbastanza bene.
Lukasz Korzybski,

1
Non sono sicuro, ma ho capito che la definizione della funzione prototipo all'interno dell'ambito (in qualche modo come una chiusura) di una funzione provoca una perdita di memoria poiché il garbage collector non può venire lì nell'istanza di quelle classi.
Sanne,

15

Se stai andando per semplice, puoi evitare completamente la "nuova" parola chiave e usare solo metodi di fabbrica. Preferisco questo, a volte, perché mi piace usare JSON per creare oggetti.

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

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

Tuttavia, non sono sicuro di quale sia il successo delle prestazioni per oggetti di grandi dimensioni.


La riga obj.instancevar1 = var1 non è necessaria, poiché l'oggetto interno avrà accesso ai parametri di getSomeObj ().
Trittico il

Wow. Questo mi fa male al cervello ma c'è una certa eleganza. Quindi la parte "obj.instancevar1 = var1" è l'inizio di una sorta di costruttore, suppongo?
Karim,

Ho appena visto il commento di Triptych. Vedo. Quindi, potresti semplicemente fare qualcosa come "instancevar1: var1" in cui viene istanziato l'oggetto interno.
Karim,

Esattamente ... quando usi {} per definire un oggetto, ha accesso alle variabili che sono attualmente nell'ambito.
Sam,

10
Con questo approccio perdi la capacità di ereditare e poiché non stai usando obj.prototype.something, definisci le funzioni ogni volta che usi l'oggetto = più memoria e più lento.
circa il

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();

Questo è il modo in cui TypeScript compila la classe con il costruttore in JavaScript.


10

Il modo semplice è:

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

Il motivo thatè che thispuò essere associato a qualcos'altro se si fornisce un metodo come gestore di eventi, quindi si salva il valore durante l'istanza e lo si utilizza in seguito.

Modifica: sicuramente non è il modo migliore, solo un modo semplice. Sto aspettando anche buone risposte!


1
Quello = questo costrutto non è necessario qui. Inoltre, i metodi add () e avg () verranno copiati per ogni "istanza" della classe Foo, anziché essere condivisi tra di loro.
Trittico il

1
È necessario (sorta) in quel caso, ma non il semplice caso che hai fornito.
Trittico,

9

Probabilmente vuoi creare un tipo usando il modello pieghevole:

    // 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';
            };
        }());
    }());

Quel codice ti darà un tipo chiamato myType . Avrà campi privati ​​interni chiamati toggle e text . Avrà anche questi membri esposti: i campi contano e numeri ; le proprietà toggle , text e numberLength ; i metodi incrementNumbersByCount e tweak .

Il modello pieghevole è dettagliato qui: Modello pieghevole Javascript


3

Codice golf per la risposta di @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());


2

MooTools (My Object Oriented Tools) è incentrato sull'idea delle classi . Puoi persino estendere e implementare con l'ereditarietà.

Quando masterizzato, rende javascript ridicolmente riutilizzabile e potente.


2

Classi basate sull'oggetto con ereditarietà

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

Semplice, dolce e fatto.


1

Una base

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

Una classe

// 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);
}

Azione

var c = new Class("Joe", "Object");
c.log(); // "Good afternoon! Me name is Joe and I'm a Object"

1

Sulla base dell'esempio del trittico, questo potrebbe anche essere più semplice:

    // 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"

Questo crea solo una singola istanza di oggetto, ma è comunque utile se si desidera incapsulare un gruppo di nomi per variabili e metodi in una classe. Normalmente non ci sarebbero argomenti "Bob, M" per il costruttore, ad esempio se i metodi fossero chiamate a un sistema con i propri dati, come un database o una rete.

Sono ancora troppo nuovo con JS per capire perché questo non usa la prototypecosa.


0

JavaScript è orientato agli oggetti , ma è radicalmente diverso rispetto ad altri linguaggi OOP come Java, C # o C ++. Non cercare di capirlo in quel modo. Butta via quella vecchia conoscenza e ricomincia da capo. JavaScript ha bisogno di un pensiero diverso.

Suggerirei di ottenere un buon manuale o qualcosa sull'argomento. Io stesso ho trovato ExtJS Tutorials il migliore per me, anche se non ho usato il framework prima o dopo averlo letto. Ma fornisce una buona spiegazione di cosa sia cosa nel mondo JavaScript. Spiacenti, sembra che quel contenuto sia stato rimosso. Ecco un link alla copia di archive.org invece. Funziona oggi : P


2
Orientato agli oggetti? Ho pensato che fosse funzionale .
Peter Mortensen,

Il link "Tutorial ExtJS" è interrotto.
Peter Mortensen,

Penso che sarebbe più esplicativo spiegare che le funzioni in javascript sono oggetti e che le regole del javascript con ambito parentale rendono ogni blocco funzione incapsulante.
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();

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.