¿JavaScript tiene el tipo de interfaz (como la 'interfaz' de Java)?

Respuestas:

650

No existe la noción de "esta clase debe tener estas funciones" (es decir, no hay interfaces per se), porque:

  1. La herencia de JavaScript se basa en objetos, no en clases. Eso no es gran cosa hasta que te des cuenta:
  2. JavaScript es un lenguaje de tipo extremadamente dinámico: puede crear un objeto con los métodos adecuados, lo que haría que se ajustara a la interfaz y luego no definir todas las cosas que lo hicieron cumplir . Sería muy fácil subvertir el sistema de tipos, ¡incluso accidentalmente! - que no valdría la pena intentar hacer un sistema de tipos en primer lugar.

En cambio, JavaScript usa lo que se llama escribir pato . (Si camina como un pato y grazna como un pato, por lo que a JS le importa, es un pato). Si su objeto tiene métodos quack (), walk () y fly (), el código puede usarlo donde lo espera un objeto que puede caminar, graznar y volar, sin requerir la implementación de alguna interfaz "Duckable". La interfaz es exactamente el conjunto de funciones que usa el código (y los valores de retorno de esas funciones), y al escribir pato, obtienes eso de forma gratuita.

Ahora, eso no quiere decir que su código no fallará a la mitad, si intenta llamar some_dog.quack(); obtendrás un TypeError. Francamente, si le estás diciendo a los perros que graznen, tienes problemas un poco mayores; la escritura de patos funciona mejor cuando mantiene a todos sus patos en fila, por así decirlo, y no permite que los perros y los patos se mezclen a menos que los trate como animales genéricos. En otras palabras, aunque la interfaz es fluida, sigue ahí; a menudo es un error pasar un perro al código que espera que grazne y vuele en primer lugar.

Pero si está seguro de que está haciendo lo correcto, puede solucionar el problema del perro graznando probando la existencia de un método en particular antes de intentar usarlo. Algo como

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

Por lo tanto, puede verificar todos los métodos que puede usar antes de usarlos. Sin embargo, la sintaxis es un poco fea. Hay una forma un poco más bonita:

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

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

Este es JavaScript estándar, por lo que debería funcionar en cualquier intérprete JS que valga la pena usar. Tiene el beneficio adicional de leer como el inglés.

Para los navegadores modernos (es decir, prácticamente cualquier navegador que no sea IE 6-8), incluso hay una manera de evitar que la propiedad aparezca en for...in:

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

El problema es que los objetos IE7 no tienen .definePropertynada, y en IE8, supuestamente solo funciona en objetos host (es decir, elementos DOM y demás). Si la compatibilidad es un problema, no puede usar .defineProperty. (Ni siquiera mencionaré IE6, porque ya es bastante irrelevante fuera de China).

Otro problema es que a algunos estilos de codificación les gusta suponer que todos escriben códigos incorrectos y prohíben la modificación Object.prototypeen caso de que alguien quiera usar a ciegas for...in. Si le importa eso o está usando un código (IMO roto ) que lo hace, intente con una versión ligeramente diferente:

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

if (can(someObject, "quack"))
{
    someObject.quack();
}
cHao
fuente
77
No es tan horrible como se supone que es. for...inestá, y siempre ha estado, cargado de tales peligros, y cualquiera que lo haga sin al menos considerar que alguien agregado Object.prototype(una técnica no poco común, según la propia admisión de ese artículo) verá su código descifrado en las manos de otra persona.
cHao
1
@entonio: Consideraría la maleabilidad de los tipos incorporados como una característica más que como un problema. Es una gran parte de lo que hace factibles las cuñas / polyfills. Sin él, estaríamos envolviendo todos los tipos incorporados con subtipos posiblemente incompatibles o esperando el soporte universal del navegador (lo que podría nunca llegar, cuando los navegadores no admiten cosas porque las personas no lo usan porque los navegadores no lo hacen) t lo soporta). Debido a que los tipos incorporados pueden modificarse, podemos agregar muchas de las funciones que aún no existen.
cHao
1
En la última versión de Javascript (1.8.5) puede definir la propiedad de un objeto para que no sea enumerable. De esta manera puedes evitar el for...inproblema. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Tomas Prado
1
@ Tomás: Lamentablemente, hasta que cada navegador ejecute algo compatible con ES5, todavía tenemos que preocuparnos por cosas como esta. E incluso entonces, el " for...inproblema" seguirá existiendo hasta cierto punto, porque siempre habrá un código descuidado ... bueno, eso, y Object.defineProperty(obj, 'a', {writable: true, enumerable: false, value: 3});es bastante más trabajo que simplemente obj.a = 3;. Puedo entender totalmente a las personas que no intentan hacerlo con más frecuencia. : P
cHao
1
Jeje ... me encanta "Francamente, si le estás diciendo a los perros que graznen, tienes problemas un poco más grandes. Gran analogía para mostrar que los idiomas no deberían tratar de evitar la estupidez. Esa es siempre una batalla perdida.
-Scott
73

Recoge una copia de ' Patrones de diseño de JavaScript ' de Dustin Diaz . Hay algunos capítulos dedicados a implementar interfaces JavaScript a través de Duck Typing. Es una buena lectura también. Pero no, no hay una implementación nativa del idioma de una interfaz, tienes que 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
}
BGerrissen
fuente
El método descrito en el libro "patrones de diseño de javascript pro" es probablemente el mejor enfoque de un montón de cosas que he leído aquí y de lo que he intentado. Puede usar la herencia encima, lo que hace que sea aún mejor seguir los conceptos de OOP. Algunos pueden afirmar que no necesita conceptos OOP en JS, pero le ruego que difiera.
animageofmine
21

JavaScript (ECMAScript edición 3) tiene una implementspalabra reservada guardada para uso futuro . Creo que esto está destinado exactamente para este propósito, sin embargo, en un apuro por sacar la especificación por la puerta, no tuvieron tiempo de definir qué hacer con ella, por lo que, en este momento, los navegadores no hacen nada más déjalo reposar allí y ocasionalmente queja si intentas usarlo para algo.

Es posible y de hecho bastante fácil crear su propio Object.implement(Interface)método con una lógica que retuerce cuando un conjunto particular de propiedades / funciones no se implementa en un objeto dado.

Escribí un artículo sobre orientación a objetos donde uso mi propia notación de la siguiente manera :

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

Hay muchas formas de crear este gato en particular, pero esta es la lógica que utilicé para mi propia implementación de interfaz. Creo que prefiero este enfoque, y es fácil de leer y usar (como puede ver arriba). Significa agregar un método de "implementación" con el Function.prototypeque algunas personas pueden tener un problema, pero creo que funciona de maravilla.

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;
}
Steven de Salas
fuente
44
Esta sintaxis realmente me duele el cerebro, pero la implementación aquí es bastante interesante.
Cypher
2
Javascript está obligado a hacer eso (dañar el cerebro) especialmente cuando proviene de implementaciones de lenguaje OO más limpias.
Steven de Salas
10
@StevendeSalas: Eh. JS en realidad tiende a ser bastante limpio cuando dejas de tratar de tratarlo como un lenguaje orientado a la clase. Toda la basura necesaria para emular clases, interfaces, etc ... eso es lo que realmente hará que te duela el cerebro. Prototipos? Cosas simples, realmente, una vez que dejas de luchar contra ellos.
cHao
qué hay en "// .. Verifique la lógica del miembro". ? ¿Cómo se ve eso?
PositiveGuy
Hola @We, verificar la lógica de los miembros significa recorrer las propiedades deseadas y arrojar un error si falta uno ... algo similar var interf = arguments[i]; for (prop in interf) { if (this.prototype[prop] === undefined) { throw 'Member [' + prop + '] missing from class definition.'; }}. Consulte la parte inferior del enlace del artículo para obtener un ejemplo más elaborado.
Steven de Salas
12

Interfaces JavaScript:

Aunque JavaScript no tiene el interfacetipo, a menudo es necesario. Por razones relacionadas con la naturaleza dinámica de JavaScript y el uso de la herencia prototípica, es difícil garantizar interfaces consistentes entre las clases; sin embargo, es posible hacerlo; y frecuentemente emulado.

En este punto, hay varias formas particulares de emular interfaces en JavaScript; La variación en los enfoques generalmente satisface algunas necesidades, mientras que otras no se abordan. Muchas veces, el enfoque más robusto es demasiado engorroso y obstaculiza el implementador (desarrollador).

Aquí hay un enfoque de Interfaces / Clases abstractas que no es muy engorroso, es explicativo, mantiene las implementaciones dentro de Abstracciones al mínimo y deja suficiente espacio para metodologías dinámicas o personalizadas:

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

Participantes

Resolver preceptos

La resolvePreceptfunción es una función de utilidad y ayuda para usar dentro de su clase abstracta . Su trabajo es permitir el manejo personalizado de la implementación de Preceptos encapsulados (datos y comportamiento) . Puede arrojar errores o advertir - Y - asignar un valor predeterminado a la clase Implementador.

iAbstractClass

El iAbstractClassdefine la interfaz que se utilizará. Su enfoque implica un acuerdo tácito con su clase Implementor. Esta interfaz asigna cada precepto al mismo espacio de nombres de precepto exacto, O, a lo que devuelva la función de resolución de preceptos . Sin embargo, el acuerdo tácito se resuelve en un contexto : una disposición del Implementador.

Implementador

El implementador simplemente 'está de acuerdo' con una interfaz ( iAbstractClass en este caso) y lo aplica por el uso de Constructor-Secuestro : iAbstractClass.apply(this). Al definir los datos y el comportamiento anteriores, y luego secuestrar el constructor de la Interfaz, pasando el contexto del Implementador al constructor de la Interfaz, podemos asegurarnos de que se agreguen las anulaciones del Implementador, y que la Interfaz explicará las advertencias y los valores predeterminados.

Este es un enfoque no engorroso que nos ha servido a mi equipo y a mí muy bien durante el transcurso del tiempo y diferentes proyectos. Sin embargo, tiene algunas advertencias y desventajas.

Inconvenientes

Aunque esto ayuda a implementar consistencia en todo su software en un grado significativo, no implementa interfaces verdaderas , sino que las emula. Aunque las definiciones, valores predeterminados, y las advertencias o errores son explicadas, la explicación de su uso se hace cumplir y afirmó por el desarrollador (como en gran parte del desarrollo de JavaScript).

Este es aparentemente el mejor enfoque para "Interfaces en JavaScript" , sin embargo, me encantaría ver lo siguiente resuelto:

  • Afirmaciones de tipos de devolución
  • Afirmaciones de firmas
  • Congelar objetos de deleteacciones
  • Afirmaciones de cualquier otra cosa frecuente o necesaria en la especificidad de la comunidad JavaScript

Dicho esto, espero que esto te ayude tanto como a mi equipo y a mí.

Cody
fuente
7

Necesita interfaces en Java, ya que está estáticamente tipado y el contrato entre clases debe conocerse durante la compilación. En JavaScript es diferente. JavaScript se escribe dinámicamente; significa que cuando obtienes el objeto puedes verificar si tiene un método específico y llamarlo.

Alex Reitbort
fuente
1
En realidad, no necesita interfaces en Java, es seguro garantizar que los objetos tengan una determinada API para poder intercambiarlos por otras implementaciones.
BGerrissen
3
No, en realidad son necesarios en Java para que pueda construir vtables para clases que implementan una interfaz en tiempo de compilación. Declarar que una clase implementa una interfaz le indica al compilador que cree una pequeña estructura que contenga punteros a todos los métodos necesarios para esa interfaz. De lo contrario, tendría que despacharse por nombre en tiempo de ejecución (como lo hacen los idiomas de tipo dinámico).
munificent
No creo que sea correcto. El envío siempre es dinámico en Java (a menos que tal vez un método sea final), y el hecho de que el método pertenezca a una interfaz no cambia las reglas de búsqueda. La razón por la que se necesitan interfaces en lenguajes de tipo estático es para que pueda usar el mismo 'pseudotipo' (la interfaz) para referirse a clases no relacionadas.
entonio
2
@entonio: el envío no es tan dinámico como parece. El método real a menudo no se conoce hasta el tiempo de ejecución, gracias al polimorfismo, pero el código de bytes no dice "invocar su método"; dice "invocar Superclass.yourMethod". La JVM no puede invocar un método sin saber en qué clase buscarlo. Durante la vinculación, puede colocar yourMethoden la entrada # 5 en la Superclasstabla v, y para cada subclase que tiene la suya yourMethod, simplemente señala la entrada de la subclase # 5 en la implementación apropiada.
cHao
1
@entonio: Para las interfaces, las reglas no cambian un poco. (No en términos de lenguaje, pero el código de bytes generado y el proceso de búsqueda de JVM son diferentes). Una clase llamada Implementationque implementa SomeInterfaceno solo dice que implementa toda la interfaz. Tiene información que dice "Implemento SomeInterface.yourMethod" y apunta a la definición del método para Implementation.yourMethod. Cuando la JVM llama SomeInterface.yourMethod, busca en la clase información sobre implementaciones del método de esa interfaz, y encuentra que necesita llamar Implementation.yourMethod.
cHao
6

Espero que cualquier persona que todavía esté buscando una respuesta la encuentre útil.

Puede probar usando un Proxy (es estándar desde 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;
    }
});

Entonces puedes decir fácilmente:

myMap = {}
myMap.position = latLngLiteral;
Shaedrich
fuente
5

Cuando desee utilizar un transcompilador, puede probar TypeScript. Admite características de borrador de ECMA (en la propuesta, las interfaces se denominan " protocolos ") similares a lo que hacen los lenguajes como coffeescript o babel.

En TypeScript, su interfaz puede verse así:

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

Lo que no puedes hacer:

Shaedrich
fuente
3

no hay interfaces nativas en JavaScript, hay varias formas de simular una interfaz. He escrito un paquete que lo hace

puedes ver la implantación aquí

Amit Wagner
fuente
2

Javascript no tiene interfaces. Pero puede ser tipado, un ejemplo se puede encontrar aquí:

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

Reinsbrain
fuente
Me gusta el patrón que usa el artículo en ese enlace para hacer afirmaciones sobre el tipo. Un error que se produce cuando algo no implementa el método que se supone que es exactamente lo que esperaría, y me gusta cómo puedo agrupar estos métodos necesarios (como una interfaz) si lo hago de esta manera.
Eric Dubé
1
Odio la transpiración (y los mapas de origen para la depuración), pero Typecript está tan cerca de ES6 que me inclino a taparme la nariz y sumergirme en Typecript. ES6 / Typecript es interesante porque le permite incluir propiedades además de métodos al definir una interfaz (comportamiento).
Reinsbrain
1

Sé que este es antiguo, pero recientemente me he encontrado necesitando cada vez más tener una API práctica para verificar objetos en las interfaces. Entonces escribí esto: https://github.com/tomhicks/methodical

También está disponible a través de NPM: npm install methodical

Básicamente, hace todo lo sugerido anteriormente, con algunas opciones para ser un poco más estricto, y todo sin tener que hacer un montón de if (typeof x.method === 'function')repeticiones.

Esperemos que alguien lo encuentre útil.

Tom
fuente
Tom, acabo de ver un video AngularJS TDD y cuando instala un marco, ¡uno de los paquetes dependientes es tu paquete metódico! ¡Buen trabajo!
Cody
Jaja excelente. Básicamente lo abandoné después de que la gente en el trabajo me convenció de que las interfaces en JavaScript eran imposibles. Recientemente tuve una idea sobre una biblioteca que básicamente representa un objeto para garantizar que solo se usen ciertos métodos, que es básicamente lo que es una interfaz. ¡Todavía creo que las interfaces tienen un lugar en JavaScript! ¿Puedes vincular ese video por cierto? Me gustaría echar un vistazo.
Tom
Puedes apostar, Tom. Intentaré encontrarlo pronto. Gracias a la anécdota sobre las interfaces como servidores proxy, también. ¡Salud!
Cody
1

Esta es una vieja pregunta, sin embargo, este tema nunca deja de molestarme.

Como muchas de las respuestas aquí y en toda la web se centran en "hacer cumplir" la interfaz, me gustaría sugerir una vista alternativa:

Siento la falta de interfaces más cuando uso múltiples clases que se comportan de manera similar (es decir, implementan una interfaz ).

Por ejemplo, tengo un Generador de correo electrónico que espera recibir Fábricas de secciones de correo electrónico , que "saben" cómo generar el contenido y HTML de las secciones. Por lo tanto, todos tenemos que tener algún tipo de getContent(id)y getHtml(content)métodos.

El patrón más cercano a las interfaces (aunque todavía es una solución alternativa) podría pensar en usar una clase que obtenga 2 argumentos, que definirán los 2 métodos de interfaz.

El principal desafío con este patrón es que los métodos tienen que ser static, o para obtener como argumento la instancia misma, para poder acceder a sus propiedades. Sin embargo, hay casos en los que creo que esta compensación vale 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));

GalAbra
fuente
0

interfaz abstracta como esta

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

crear una instancia:

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

y úsalo

let x = new MyType()
x.print()
Kevin Krausse
fuente