JavaScript ha il tipo di interfaccia (come "interfaccia" di Java)?


Risposte:


650

Non esiste la nozione di "questa classe deve avere queste funzioni" (cioè nessuna interfaccia in sé), perché:

  1. L'eredità JavaScript si basa su oggetti, non su classi. Non è un grosso problema fino a quando non ti rendi conto:
  2. JavaScript è un linguaggio estremamente dinamico: puoi creare un oggetto con i metodi appropriati, che lo renderebbero conforme all'interfaccia e quindi definire tutte le cose che lo hanno reso conforme . Sarebbe così facile sovvertire il sistema di tipi - anche per caso! - che non varrebbe la pena provare a creare un sistema di tipi in primo luogo.

Invece, JavaScript utilizza ciò che si chiama tipizzazione anatra . (Se cammina come un'anatra e ciondola come un'anatra, per quanto riguarda JS, è un'anatra.) Se il tuo oggetto ha metodi quack (), walk () e fly (), il codice può usarlo dove si aspetta un oggetto che può camminare, ciarlare e volare, senza richiedere l'implementazione di un'interfaccia "Duckable". L'interfaccia è esattamente l'insieme di funzioni che il codice utilizza (e i valori restituiti da quelle funzioni), e con la digitazione duck, la ottieni gratuitamente.

Ora, questo non vuol dire che il tuo codice non fallirà a metà strada, se provi a chiamare some_dog.quack(); otterrai un TypeError. Francamente, se stai dicendo ai cani di ciarlare, hai problemi leggermente più grandi; la tipizzazione delle anatre funziona meglio quando si tengono tutte le anatre di fila, per così dire, e non si lascia che cani e anatre si mescolino insieme a meno che non si tratti di animali generici. In altre parole, anche se l'interfaccia è fluida, è ancora lì; è spesso un errore passare un cane al codice che si aspetta che cacci e voli in primo luogo.

Ma se sei sicuro di fare la cosa giusta, puoi aggirare il problema del cane ciarlatano testando l'esistenza di un metodo particolare prima di provare a usarlo. Qualcosa di simile a

if (typeof(someObject.quack) == "function")
{
    // This thing can quack
}

Quindi puoi verificare tutti i metodi che puoi usare prima di usarli. La sintassi è piuttosto brutta, però. C'è un modo leggermente più carino:

Object.prototype.can = function(methodName)
{
     return ((typeof this[methodName]) == "function");
};

if (someObject.can("quack"))
{
    someObject.quack();
}

Questo è JavaScript standard, quindi dovrebbe funzionare con qualsiasi interprete JS che valga la pena usare. Ha l'ulteriore vantaggio di leggere come l'inglese.

Per i browser moderni (ovvero praticamente qualsiasi browser diverso da IE 6-8), c'è persino un modo per impedire la visualizzazione della proprietà in for...in:

Object.defineProperty(Object.prototype, 'can', {
    enumerable: false,
    value: function(method) {
        return (typeof this[method] === 'function');
    }
}

Il problema è che gli oggetti IE7 non hanno .definePropertyaffatto, e in IE8, funziona presumibilmente solo su oggetti host (ovvero, elementi DOM e simili). Se la compatibilità è un problema, non è possibile utilizzare .defineProperty. (Non menzionerò nemmeno IE6, perché è piuttosto irrilevante al di fuori della Cina.)

Un altro problema è che ad alcuni stili di codifica piace supporre che tutti scrivano codice errato e proibiscono di modificarli Object.prototypenel caso in cui qualcuno voglia utilizzarli ciecamente for...in. Se ti interessa, o stai usando un codice ( rotto IMO ), prova una versione leggermente diversa:

function can(obj, methodName)
{
     return ((typeof obj[methodName]) == "function");
}

if (can(someObject, "quack"))
{
    someObject.quack();
}

7
Non è così orribile come è stato fatto per essere. for...inè - ed è sempre stato - irto di tali pericoli, e chiunque lo faccia senza almeno considerare che qualcuno aggiunto a Object.prototype(una tecnica non insolita, per ammissione di quell'articolo) vedrà il proprio codice infrangere nelle mani di qualcun altro.
cHao,

1
@entonio: considererei la malleabilità dei tipi integrati come una caratteristica piuttosto che un problema. È una grande parte di ciò che rende possibili spessori / polifilloli. Senza di essa, o avremmo racchiuso tutti i tipi predefiniti con sottotipi eventualmente incompatibili o in attesa del supporto universale del browser (che potrebbe non arrivare mai, quando i browser non supportano roba perché le persone non la usano perché i browser non " t supportarlo). Poiché i tipi predefiniti possono essere modificati, possiamo invece semplicemente aggiungere molte delle funzioni che non esistono ancora.
cHao,

1
Nell'ultima versione di Javascript (1.8.5) è possibile definire la proprietà di un oggetto da non enumerare. In questo modo puoi evitare il for...inproblema. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Tomas Prado,

1
@ Tomás: Purtroppo, fino a quando su ogni browser non è in esecuzione qualcosa di compatibile con ES5, dobbiamo ancora preoccuparci di cose come questa. E anche allora, il " for...inproblema" esisterà ancora in una certa misura, perché ci sarà sempre un codice sciatto ... beh, quello, ed Object.defineProperty(obj, 'a', {writable: true, enumerable: false, value: 3});è un po 'più di lavoro che semplice obj.a = 3;. Posso capire totalmente le persone che non provano a farlo più spesso. : P
cHao,

1
Hehe ... adoro "Francamente, se stai dicendo ai cani di ciarlare, hai problemi leggermente più grandi. Grande analogia per mostrare che le lingue non dovrebbero cercare di evitare la stupidità. È sempre una battaglia persa. -Scott
Skooppa. com

73

Prendi una copia di " Pattern di progettazione JavaScript " di Dustin Diaz . Ci sono alcuni capitoli dedicati all'implementazione delle interfacce JavaScript tramite Duck Typing. È anche una bella lettura. Ma no, non esiste un'implementazione nativa in lingua di un'interfaccia, devi Duck Type .

// example duck typing method
var hasMethods = function(obj /*, method list as strings */){
    var i = 1, methodName;
    while((methodName = arguments[i++])){
        if(typeof obj[methodName] != 'function') {
            return false;
        }
    }
    return true;
}

// in your code
if(hasMethods(obj, 'quak', 'flapWings','waggle')) {
    //  IT'S A DUCK, do your duck thang
}

Il metodo descritto nel libro "Pro javascript design patterns" è probabilmente l'approccio migliore di un mucchio di cose che ho letto qui e da quello che ho provato. È possibile utilizzare l'ereditarietà su di esso, il che rende ancora meglio seguire i concetti di OOP. Alcuni potrebbero affermare che non hai bisogno dei concetti OOP in JS, ma mi permetto di dissentire.
animageofmine,

21

JavaScript (ECMAScript edition 3) ha una implementsparola riservata salvata per uso futuro . Penso che questo sia destinato esattamente a questo scopo, tuttavia, in fretta per ottenere le specifiche fuori dalla porta, non hanno avuto il tempo di definire cosa fare con esso, quindi, al momento, i browser non fanno nulla oltre lascialo stare lì e occasionalmente ti lamenti se provi a usarlo per qualcosa.

È possibile e davvero abbastanza facile crearne uno tuo Object.implement(Interface) metodo con una logica che sfugge ogni volta che un determinato insieme di proprietà / funzioni non è implementato in un determinato oggetto.

Ho scritto un articolo sull'orientamento agli oggetti dove uso la mia notazione come segue :

// Create a 'Dog' class that inherits from 'Animal'
// and implements the 'Mammal' interface
var Dog = Object.extend(Animal, {
    constructor: function(name) {
        Dog.superClass.call(this, name);
    },
    bark: function() {
        alert('woof');
    }
}).implement(Mammal);

Esistono molti modi per abbellire questo particolare gatto, ma questa è la logica che ho usato per la mia implementazione dell'interfaccia. Trovo che preferisco questo approccio, ed è facile da leggere e usare (come puoi vedere sopra). Significa aggiungere un metodo "implement" al Function.prototypequale alcune persone potrebbero avere un problema, ma trovo che funzioni magnificamente.

Function.prototype.implement = function() {
    // Loop through each interface passed in and then check 
    // that its members are implemented in the context object (this).
    for(var i = 0; i < arguments.length; i++) {
       // .. Check member's logic ..
    }
    // Remember to return the class being tested
    return this;
}

4
Questa sintassi mi fa davvero male al cervello, ma l'implementazione qui è piuttosto interessante.
Cypher,

2
Javascript è destinato a farlo (ferendo il cervello) specialmente quando proviene da implementazioni di linguaggio OO più pulite.
Steven de Salas,

10
@StevendeSalas: Eh. JS in realtà tende ad essere piuttosto pulito quando smetti di provare a trattarlo come un linguaggio orientato alla classe. Tutta la merda necessaria per emulare classi, interfacce, ecc ... è questo che ti farà davvero male al cervello. Prototipi? Roba semplice, davvero, quando smetti di combatterli.
cHao,

cosa c'è in "// .. Controlla la logica del membro." ? che aspetto ha?
Positivo

Ciao @We, controllare la logica dei membri significa passare in rassegna le proprietà desiderate e lanciare un errore se ne manca uno var interf = arguments[i]; for (prop in interf) { if (this.prototype[prop] === undefined) { throw 'Member [' + prop + '] missing from class definition.'; }}. Vedere la parte inferiore del collegamento dell'articolo per un esempio più elaborato.
Steven de Salas,

12

Interfacce JavaScript:

Sebbene JavaScript non abbiainterface tipo, spesso è necessario. Per motivi legati alla natura dinamica di JavaScript e all'uso dell'ereditarietà prototipica, è difficile garantire interfacce coerenti tra le classi, tuttavia è possibile farlo; e frequentemente emulato.

A questo punto, esistono alcuni modi particolari per emulare le interfacce in JavaScript; la varianza degli approcci di solito soddisfa alcuni bisogni, mentre altri non vengono affrontati. Spesso l'approccio più solido è eccessivamente ingombrante e ostacola l'implementatore (sviluppatore).

Ecco un approccio alle Interfacce / Classi astratte che non è molto ingombrante, è esplicativo, mantiene al minimo le implementazioni all'interno delle Astrazioni e lascia abbastanza spazio a metodologie dinamiche o personalizzate:

function resolvePrecept(interfaceName) {
    var interfaceName = interfaceName;
    return function curry(value) {
        /*      throw new Error(interfaceName + ' requires an implementation for ...');     */
        console.warn('%s requires an implementation for ...', interfaceName);
        return value;
    };
}

var iAbstractClass = function AbstractClass() {
    var defaultTo = resolvePrecept('iAbstractClass');

    this.datum1 = this.datum1 || defaultTo(new Number());
    this.datum2 = this.datum2 || defaultTo(new String());

    this.method1 = this.method1 || defaultTo(new Function('return new Boolean();'));
    this.method2 = this.method2 || defaultTo(new Function('return new Object();'));

};

var ConcreteImplementation = function ConcreteImplementation() {

    this.datum1 = 1;
    this.datum2 = 'str';

    this.method1 = function method1() {
        return true;
    };
    this.method2 = function method2() {
        return {};
    };

    //Applies Interface (Implement iAbstractClass Interface)
    iAbstractClass.apply(this);  // .call / .apply after precept definitions
};

I partecipanti

Risolutore Precetto

La resolvePreceptfunzione è un'utilità e una funzione di supporto da utilizzare all'interno della tua classe astratta . Il suo compito è consentire la gestione personalizzata dell'implementazione dei Precetti incapsulati (dati e comportamento) . Può generare errori o avvisare - AND - assegnare un valore predefinito alla classe Implementor.

iAbstractClass

La iAbstractClassdefinisce l'interfaccia da utilizzare. Il suo approccio implica un tacito accordo con la sua classe Implementor. Questa interfaccia assegna ciascun precetto allo stesso spazio dei nomi precetto esatto - O - a qualunque cosa restituisca la funzione Precept Resolver . Tuttavia, l'accordo tacito si risolve in un contesto : una disposizione dell'attore.

implementor

L'implementazioni semplicemente 'd'accordo' con un'interfaccia ( iAbstractClass in questo caso) e lo applica con l'uso di costruttore-Hijacking : iAbstractClass.apply(this). Definendo i dati e il comportamento sopra e quindi dirottando il costruttore dell'interfaccia - passando il contesto dell'implementatore al costruttore dell'interfaccia - possiamo garantire che vengano aggiunte le sostituzioni dell'attuatore e che l'interfaccia spiegherà avvisi e valori predefiniti.

Questo è un approccio molto ingombrante che ha servito il mio team e io molto bene nel corso del tempo e in diversi progetti. Tuttavia, presenta alcuni avvertimenti e svantaggi.

svantaggi

Sebbene ciò aiuti a implementare la coerenza in tutto il software in misura significativa, non implementa interfacce vere, ma le emula. Sebbene siano esplicitate definizioni, valori predefiniti e avvertenze o errori , la spiegazione dell'uso viene applicata e affermata dallo sviluppatore (come con gran parte dello sviluppo di JavaScript).

Questo è apparentemente il miglior approccio alle "Interfacce in JavaScript" , tuttavia, mi piacerebbe vedere quanto segue risolto:

  • Asserzioni di tipi restituiti
  • Asserzioni di firme
  • Congela gli oggetti dalle deleteazioni
  • Affermazioni di qualsiasi altra cosa prevalente o necessaria nella specificità della comunità JavaScript

Detto questo, spero che questo ti aiuti tanto quanto la mia squadra e io.


7

Sono necessarie interfacce in Java poiché è tipizzato staticamente e il contratto tra le classi dovrebbe essere noto durante la compilazione. In JavaScript è diverso. JavaScript è digitato in modo dinamico; significa che quando ottieni l'oggetto puoi semplicemente verificare se ha un metodo specifico e chiamarlo.


1
In realtà, non hai bisogno di interfacce in Java, è sicuro che gli oggetti abbiano una certa API in modo da poterli scambiare con altre implementazioni.
BGerrissen,

3
No, in realtà sono necessari in Java in modo che possa creare vtables per le classi che implementano un'interfaccia in fase di compilazione. Dichiarare che una classe implementa un'interfaccia indica al compilatore di costruire una piccola struttura che contenga puntatori a tutti i metodi necessari per quell'interfaccia. Altrimenti, dovrebbe essere spedito per nome in fase di esecuzione (come fanno le lingue tipizzate dinamicamente).
munificente

Non penso sia corretto. Il dispacciamento è sempre dinamico in java (a meno che forse un metodo non sia definitivo) e il fatto che il metodo appartenga a un'interfaccia non modifica le regole di ricerca. Il motivo per cui sono necessarie interfacce nei linguaggi tipicamente statici è quindi che è possibile utilizzare lo stesso 'pseudo-tipo' (l'interfaccia) per fare riferimento a classi non correlate.
entonio,

2
@entonio: la spedizione non è così dinamica come sembra. Il metodo attuale spesso non è noto fino al runtime, grazie al polimorfismo, ma il bytecode non dice "invoke yourMethod"; dice "invoke Superclass.yourMethod". La JVM non può invocare un metodo senza sapere in quale classe cercarlo. Durante il collegamento, potrebbe mettere yourMethodalla voce # 5 nella Superclasstabella, e per ogni sottoclasse che ha la sua yourMethod, indica semplicemente la voce # 5 della sottoclasse all'implementazione appropriata.
cHao,

1
@entonio: Per le interfacce, le regole non cambiano un po '. (Non in termini linguistici, ma il bytecode generato e il processo di ricerca della JVM sono diversi.) Una classe denominata Implementationche implementa SomeInterfacenon dice semplicemente che implementa l'intera interfaccia. Ha informazioni che dicono "I implement SomeInterface.yourMethod" e indica la definizione del metodo per Implementation.yourMethod. Quando JVM chiama SomeInterface.yourMethod, cerca nella classe informazioni sulle implementazioni del metodo di quell'interfaccia e trova che deve chiamare Implementation.yourMethod.
cHao,

6

Spero che chiunque stia ancora cercando una risposta la trovi utile.

Puoi provare usando un proxy (è standard da ECMAScript 2015): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

latLngLiteral = new Proxy({},{
    set: function(obj, prop, val) {
        //only these two properties can be set
        if(['lng','lat'].indexOf(prop) == -1) {
            throw new ReferenceError('Key must be "lat" or "lng"!');
        }

        //the dec format only accepts numbers
        if(typeof val !== 'number') {
            throw new TypeError('Value must be numeric');
        }

        //latitude is in range between 0 and 90
        if(prop == 'lat'  && !(0 < val && val < 90)) {
            throw new RangeError('Position is out of range!');
        }
        //longitude is in range between 0 and 180
        else if(prop == 'lng' && !(0 < val && val < 180)) {
            throw new RangeError('Position is out of range!');
        }

        obj[prop] = val;

        return true;
    }
});

Quindi puoi facilmente dire:

myMap = {}
myMap.position = latLngLiteral;

5

Quando si desidera utilizzare un transcompiler, è possibile provare TypeScript. Supporta bozze di funzionalità ECMA (nella proposta, le interfacce sono chiamate " protocolli ") simili a quelle che fanno linguaggi come coffeescript o babel.

In TypeScript la tua interfaccia può apparire come:

interface IMyInterface {
    id: number; // TypeScript types are lowercase
    name: string;
    callback: (key: string; value: any; array: string[]) => void;
    type: "test" | "notATest"; // so called "union type"
}

Cosa non puoi fare:


3

non ci sono interfacce native in JavaScript, ci sono diversi modi per simulare un'interfaccia. ho scritto un pacchetto che lo fa

puoi vedere l'impianto qui


2

Javascript non ha interfacce. Ma può essere dattiloscritto, qui puoi trovare un esempio:

http://reinsbrain.blogspot.com/2008/10/interface-in-javascript.html


Mi piace lo schema che l'articolo su quel link usa per fare affermazioni sul tipo. Un errore viene generato quando qualcosa non implementa il metodo che dovrebbe essere esattamente quello che mi aspetterei e mi piace come posso raggruppare questi metodi richiesti (come un'interfaccia) se lo faccio in questo modo.
Eric Dubé,

1
Odio la traspilazione (e mappe di origine per il debug) ma Typescript è così vicino a ES6 che sono propenso a trattenere il naso e ad immergermi in Typescript. ES6 / Typescript è interessante perché consente di includere proprietà oltre ai metodi durante la definizione di un'interfaccia (comportamento).
Reinsbrain

1

So che questo è vecchio, ma recentemente mi sono trovato sempre più bisogno di avere un'API utile per il controllo degli oggetti rispetto alle interfacce. Quindi ho scritto questo: https://github.com/tomhicks/methodical

È disponibile anche tramite NPM: npm install methodical

Praticamente fa tutto ciò che è stato suggerito sopra, con alcune opzioni per essere un po 'più rigoroso, e tutto senza doverne fare un sacco if (typeof x.method === 'function') boilerplate.

Spero che qualcuno lo trovi utile.


Tom, ho appena visto un video TDD di AngularJS e quando installa un framework, uno dei pacchetti dipendenti è il tuo pacchetto metodico! Buon lavoro!
Cody,

Haha eccellente. Fondamentalmente l'ho abbandonato dopo che le persone al lavoro mi avevano convinto che le interfacce in JavaScript non fossero necessarie. Recentemente ho avuto un'idea su una libreria che in pratica proxyava un oggetto per assicurarmi che solo alcuni metodi fossero usati su di essa, che è fondamentalmente quello che è un'interfaccia. Penso ancora che le interfacce abbiano un posto in JavaScript! Puoi collegare quel video a proposito? Mi piacerebbe dare un'occhiata
Tom,

Puoi scommettere, Tom. Proverò a trovarlo presto. Grazie anche all'aneddoto sulle interfacce come proxy. Saluti!
Cody,

1

Questa è una vecchia domanda, tuttavia questo argomento non smette mai di infastidirmi.

Poiché molte delle risposte qui e in tutto il web si concentrano sulla "applicazione" dell'interfaccia, vorrei suggerire una visione alternativa:

Sento maggiormente la mancanza di interfacce quando uso più classi che si comportano in modo simile (ovvero implementano un'interfaccia ).

Ad esempio, ho un generatore di e-mail che si aspetta di ricevere le fabbriche di sezioni di e-mail , che "sanno" come generare il contenuto e l'HTML delle sezioni. Quindi, hanno tutti bisogno di avere una sorta di getContent(id)e getHtml(content)metodi.

Il modello più vicino alle interfacce (anche se è ancora una soluzione alternativa) che mi viene in mente è l'utilizzo di una classe che otterrà 2 argomenti, che definiranno i 2 metodi dell'interfaccia.

La sfida principale con questo modello è che i metodi devono essere static, o ottenere come argomento l'istanza stessa, per accedere alle sue proprietà. Tuttavia, ci sono casi in cui trovo che questo compromesso valga la pena.

class Filterable {
  constructor(data, { filter, toString }) {
    this.data = data;
    this.filter = filter;
    this.toString = toString;
    // You can also enforce here an Iterable interface, for example,
    // which feels much more natural than having an external check
  }
}

const evenNumbersList = new Filterable(
  [1, 2, 3, 4, 5, 6], {
    filter: (lst) => {
      const evenElements = lst.data.filter(x => x % 2 === 0);
      lst.data = evenElements;
    },
    toString: lst => `< ${lst.data.toString()} >`,
  }
);

console.log('The whole list:    ', evenNumbersList.toString(evenNumbersList));
evenNumbersList.filter(evenNumbersList);
console.log('The filtered list: ', evenNumbersList.toString(evenNumbersList));


0

interfaccia astratta come questa

const MyInterface = {
  serialize: () => {throw "must implement serialize for MyInterface types"},
  print: () => console.log(this.serialize())
}

creare un'istanza:

function MyType() {
  this.serialize = () => "serialized "
}
MyType.prototype = MyInterface

e usalo

let x = new MyType()
x.print()
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.