Czy JavaScript ma typ interfejsu (taki jak „interfejs” Java)?

Odpowiedzi:

649

Nie ma pojęcia, że ​​„ta klasa musi mieć te funkcje” (to znaczy brak interfejsów jako takich), ponieważ:

  1. Dziedziczenie JavaScript jest oparte na obiektach, a nie klasach. To nic wielkiego, dopóki nie uświadomisz sobie:
  2. JavaScript jest językiem niezwykle dynamicznie typowanym - możesz stworzyć obiekt za pomocą odpowiednich metod, które uczynią go zgodnym z interfejsem, a następnie zdefiniować wszystkie elementy, które go dostosowały . Tak łatwo byłoby obalić system typów - nawet przypadkowo! - że nie byłoby warto próbować stworzyć system typów.

Zamiast tego JavaScript używa tak zwanego pisania kaczego . (Jeśli chodzi jak kaczka i kwakuje jak kaczka, jeśli chodzi o JS, jest to kaczka.) Jeśli twój obiekt ma metody quack (), walk () i fly (), kod może użyć go tam, gdzie się spodziewa obiekt, który może chodzić, kwakać i latać, bez konieczności implementacji jakiegoś interfejsu „Duckable”. Interfejs jest dokładnie zestawem funkcji, z których korzysta kod (i zwracanymi wartościami z tych funkcji), a przy wpisywaniu kaczki dostajesz to za darmo.

Nie oznacza to, że kod nie zawiedzie w połowie, jeśli spróbujesz zadzwonić some_dog.quack(); otrzymasz TypeError. Szczerze mówiąc, jeśli każesz psom kwakać, masz nieco większe problemy; pisanie na kaczkach działa najlepiej, gdy trzymasz wszystkie kaczki w rzędzie, że tak powiem, i nie pozwalasz, aby psy i kaczki zlewały się ze sobą, chyba że traktujesz je jak zwierzęta ogólne. Innymi słowy, mimo że interfejs jest płynny, nadal tam jest; częstym błędem jest podanie psa do kodu, który oczekuje, że będzie kwakał i latał w pierwszej kolejności.

Ale jeśli masz pewność, że postępujesz właściwie, możesz obejść problem psiaka, sprawdzając, czy istnieje konkretna metoda, zanim spróbujesz jej użyć. Coś jak

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

Możesz więc sprawdzić wszystkie metody, których możesz użyć przed ich użyciem. Jednak składnia jest trochę brzydka. Jest nieco ładniejszy sposób:

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

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

Jest to standardowy JavaScript, więc powinien działać w każdym interpretatorze JS, którego warto użyć. Ma dodatkową zaletę czytania jak angielski.

W przypadku współczesnych przeglądarek (czyli praktycznie każdej przeglądarki innej niż IE 6-8) istnieje nawet sposób, aby nie wyświetlać się w for...in:

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

Problem polega na tym, że w ogóle nie ma obiektów IE7 .defineProperty, a w IE8 podobno działa tylko na obiektach hosta (czyli elementach DOM i tym podobnych). Jeśli problemem jest kompatybilność, nie możesz jej użyć .defineProperty. (Nie wspomnę nawet o IE6, ponieważ jest on raczej nieistotny poza Chinami.)

Inną kwestią jest to, że niektóre style kodowania zakładają, że każdy pisze zły kod, i zabraniają modyfikowania Object.prototypew przypadku, gdy ktoś chce na ślepo używać for...in. Jeśli Ci na tym zależy lub używasz ( zepsutego IMO ) kodu, który działa, wypróbuj nieco inną wersję:

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

if (can(someObject, "quack"))
{
    someObject.quack();
}
cHao
źródło
7
To nie jest tak straszne, jak się wydaje. for...injest - i zawsze był - obarczony takimi niebezpieczeństwami, a każdy, kto to zrobi, nie biorąc pod uwagę, że ktoś dodał do Object.prototype(nietypowa technika, jak przyznaje ten artykuł), zobaczy, że jego kod łamie się w czyichś rękach.
cHao
1
@entonio: Uważałbym plastyczność wbudowanych typów za cechę, a nie za problem. To duża część tego, co sprawia, że ​​podkładki / wypełnienia są wykonalne. Bez niego albo owijalibyśmy wszystkie wbudowane typy potencjalnie niekompatybilnymi podtypami, albo czekaliśmy na uniwersalną obsługę przeglądarek (co może nigdy nie nastąpić, gdy przeglądarki nie obsługują rzeczy, ponieważ ludzie ich nie używają, ponieważ przeglądarki nie „ t wspierać to). Ponieważ typy wbudowane można modyfikować, zamiast tego możemy po prostu dodać wiele funkcji, które jeszcze nie istnieją.
cHao
1
W ostatniej wersji Javascript (1.8.5) możesz zdefiniować właściwość obiektu, którego nie będzie można wyliczyć. W ten sposób możesz uniknąć for...inproblemu. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Tomas Prado
1
@ Tomás: Niestety, dopóki każda przeglądarka nie uruchomi czegoś kompatybilnego z ES5, nadal musimy martwić się o takie rzeczy. I nawet wtedy „ for...inproblem” nadal będzie do pewnego stopnia istniał, ponieważ zawsze będzie niechlujny kod ... no cóż, i Object.defineProperty(obj, 'a', {writable: true, enumerable: false, value: 3});to jest o wiele więcej pracy niż tylko obj.a = 3;. Całkowicie rozumiem ludzi, którzy nie próbują tego robić częściej. : P
cHao
1
Hehe ... uwielbiam "Szczerze mówiąc, jeśli każesz
psom kwakać
72

Odbierz kopię „ wzorców projektowych JavaScript ” autorstwa Dustina Diaza . Jest kilka rozdziałów poświęconych implementacji interfejsów JavaScript poprzez Duck Typing. To także miła lektura. Ale nie, nie ma natywnej implementacji interfejsu dla języka, musisz typ kaczki .

// 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
źródło
Metoda opisana w książce „pro javascript design patterns” jest prawdopodobnie najlepszym podejściem do wielu rzeczy, które przeczytałem tutaj i z tego, co próbowałem. Możesz na nim zastosować dziedziczenie, co sprawia, że ​​jeszcze lepiej jest stosować się do koncepcji OOP. Niektórzy mogą twierdzić, że nie potrzebujesz koncepcji OOP w JS, ale ja się różnię.
animageofmine
21

JavaScript (ECMAScript edycja 3) ma implementszarezerwowane słowo zapisane do wykorzystania w przyszłości . Myślę, że jest to przeznaczone właśnie do tego celu, jednak w pośpiechu, aby wydostać się ze specyfikacji, nie mieli czasu na określenie, co z nią zrobić, więc w chwili obecnej przeglądarki nie robią nic poza pozwól mu tam usiąść i od czasu do czasu narzekać, jeśli spróbujesz użyć go do czegoś.

Możliwe jest i naprawdę łatwe stworzenie własnej Object.implement(Interface)metody z logiką, która błąka się za każdym razem, gdy określony zestaw właściwości / funkcji nie jest zaimplementowany w danym obiekcie.

Napisałem artykuł o orientacji obiektowej, w którym stosuję własną notację w następujący sposób :

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

Jest wiele sposobów na skórowanie tego konkretnego kota, ale taką logikę zastosowałem przy własnej implementacji interfejsu. Uważam, że wolę takie podejście i jest łatwe do odczytania i użycia (jak widać powyżej). Oznacza to dodanie metody „implementuj”, z Function.prototypektórą niektórzy mogą mieć problem, ale uważam, że działa ona pięknie.

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
źródło
4
Ta składnia naprawdę boli mój mózg, ale implementacja tutaj jest dość interesująca.
Cypher
2
JavaScript jest do tego zobowiązany (raniąc mózg), szczególnie gdy pochodzi z czystszych implementacji języka OO.
Steven de Salas,
10
@StevendeSalas: Eh. JS faktycznie jest dość czysty, gdy przestajesz traktować go jako język zorientowany na klasy. Wszystkie bzdury wymagane do naśladowania klas, interfejsów itp.… To naprawdę sprawi, że twój mózg będzie bolał. Prototypy? Proste rzeczy, naprawdę, kiedy przestaniesz z nimi walczyć.
cHao
co jest w „// .. Sprawdź logikę członka”. ? Jak to wygląda?
PositiveGuy,
Cześć @We, sprawdzanie logiki członków oznacza przechodzenie przez pożądane właściwości i zgłaszanie błędu, jeśli brakuje jednego ... czegoś wzdłuż linii var interf = arguments[i]; for (prop in interf) { if (this.prototype[prop] === undefined) { throw 'Member [' + prop + '] missing from class definition.'; }}. Zobacz bardziej szczegółowy przykład na dole linku do artykułu .
Steven de Salas,
12

Interfejsy JavaScript:

Chociaż JavaScript nie ma tego interfacetypu, często jest potrzebny. Z przyczyn związanych z dynamicznym charakterem JavaScript i wykorzystaniem Prototypical-Inheritance trudno jest zapewnić spójne interfejsy między klasami - jednak jest to możliwe; i często emulowane.

W tym momencie istnieje kilka konkretnych sposobów emulacji interfejsów w JavaScript; wariancja podejść zazwyczaj zaspokaja niektóre potrzeby, podczas gdy inne pozostają bez odpowiedzi. Często najbardziej niezawodne podejście jest zbyt uciążliwe i utrudnia implementację (programistę).

Oto podejście do interfejsów / klas abstrakcyjnych, które nie jest zbyt uciążliwe, objaśniające, ogranicza implementacje wewnątrz abstrakcji do minimum i pozostawia wystarczająco dużo miejsca na dynamiczne lub niestandardowe metodologie:

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

Uczestnicy

Precept Resolver

Ta resolvePreceptfunkcja jest funkcją narzędziową i pomocniczą do użycia wewnątrz klasy abstrakcyjnej . Jego zadaniem jest umożliwienie dostosowanej obsługi implementacji enkapsulowanych Wskazań (dane i zachowanie) . Może rzucać błędy lub ostrzegać - ORAZ - przypisywać wartość domyślną do klasy Implementor.

iAbstractClass

iAbstractClassOkreśla interfejs jest używany. Jego podejście wymaga milczącej umowy z klasą Implementor. Ten interfejs przypisuje każde przykazanie dokładnie tej samej przestrzeni nazw przykazań - LUB - do wszystkiego, co zwraca funkcja Rozpoznanie przykazań . Jednak milcząca umowa rozwiązuje się w kontekście - postanowienie Implementora.

Realizator

Implementator po prostu „zgadza się” z interfejsem ( iAbstractClass w tym przypadku) i stosuje je przez użycie Konstruktor-Hijacking : iAbstractClass.apply(this). Poprzez zdefiniowanie powyższych danych i zachowania, a następnie przejęcie konstruktora interfejsu - przekazanie kontekstu implementatora do konstruktora interfejsu - możemy zagwarantować, że zostaną dodane przesłonięcia implementatora, a interfejs wyjaśni objaśnienia i wartości domyślne.

Jest to bardzo nieporęczne podejście, które służyło mojemu zespołowi i mnie bardzo dobrze na czas i różne projekty. Ma jednak pewne zastrzeżenia i wady.

Wady

Chociaż pomaga to w znacznym stopniu zaimplementować spójność w oprogramowaniu, nie implementuje prawdziwych interfejsów, ale je emuluje. Chociaż definicje, domyślne, a ostrzeżenia lub błędy wyjaśniony, wyjaśnianiu obsługi jest egzekwowane i zapewnił przez programistę (podobnie jak w przypadku większości skryptów JavaScript).

To pozornie najlepsze podejście do „Interfejsów w JavaScript” , jednak chciałbym zobaczyć rozwiązanie:

  • Asercje typów zwrotów
  • Oświadczenia o podpisach
  • Zamroź obiekty z delete działaniami
  • Twierdzenia o czymkolwiek innym, powszechnym lub potrzebnym w specyfice społeczności JavaScript

To powiedziawszy, mam nadzieję, że to pomoże ci tak bardzo, jak mój zespół i ja.

Cody
źródło
7

Potrzebujesz interfejsów w Javie, ponieważ jest on wpisany statycznie, a kontrakt między klasami powinien być znany podczas kompilacji. W JavaScript jest inaczej. JavaScript jest dynamicznie wpisywany; oznacza to, że po otrzymaniu obiektu możesz po prostu sprawdzić, czy ma określoną metodę i wywołać go.

Alex Reitbort
źródło
1
W rzeczywistości nie potrzebujesz interfejsów w Javie, bezpieczne jest, aby upewnić się, że obiekty mają określony interfejs API, aby można było zamienić je na inne implementacje.
BGerrissen
3
Nie, w rzeczywistości są one potrzebne w Javie, aby mogła budować vtables dla klas, które implementują interfejs w czasie kompilacji. Zadeklarowanie, że klasa implementuje interfejs instruuje kompilator do zbudowania małej struktury, która zawiera wskaźniki do wszystkich metod wymaganych przez ten interfejs. W przeciwnym razie musiałby być wysyłany według nazwy w czasie wykonywania (tak jak robią to języki z dynamicznym pisaniem).
hojny
Nie sądzę, że to prawda. Wysyłanie jest zawsze dynamiczne w Javie (chyba że metoda jest ostateczna), a fakt, że metoda należy do interfejsu, nie zmienia reguł wyszukiwania. Interfejsy są potrzebne w językach o typie statycznym, dlatego można używać tego samego „pseudo-typu” (interfejsu) w celu odniesienia do niepowiązanych klas.
entonio
2
@entonio: Wysyłka nie jest tak dynamiczna, jak się wydaje. Rzeczywista metoda często nie jest znana do czasu uruchomienia, dzięki polimorfizmowi, ale kod bajtowy nie mówi „wywołaj swoją Metodę”; jest napisane „wywołaj Superclass.yourMethod”. JVM nie może wywołać metody, nie wiedząc, w której klasie ma jej szukać. Podczas łączenia może umieścić yourMethodpozycję 5 w Superclasstabeli vtable, a dla każdej podklasy, która ma własną yourMethod, po prostu wskazuje pozycję tej podklasy 5 przy odpowiednim wdrożeniu.
cHao 18.04.13
1
@entonio: Dla interfejsów, reguły nie zmieniają się nieco. (Nie język, ale generowany kod bajtowy i proces wyszukiwania JVM są różne.) Klasa o nazwie, Implementationktóra implementuje SomeInterface, nie tylko mówi, że implementuje cały interfejs. Zawiera informacje z napisem „Wdrażam SomeInterface.yourMethod” i wskazuje definicję metody dla Implementation.yourMethod. Gdy JVM wywołuje SomeInterface.yourMethod, szuka w klasie informacji o implementacjach metody tego interfejsu i stwierdza, że ​​musi zadzwonić Implementation.yourMethod.
cHao 18.04.13
6

Mam nadzieję, że każdy, kto wciąż szuka odpowiedzi, uzna ją za pomocną.

Możesz wypróbować za pomocą serwera proxy (jest to standard od 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;
    }
});

Następnie możesz łatwo powiedzieć:

myMap = {}
myMap.position = latLngLiteral;
Shaedrich
źródło
5

Jeśli chcesz użyć transkompilatora, możesz wypróbować TypeScript. Obsługuje projekty funkcji ECMA (we wniosku interfejsy nazywane są „ protokołami ”) ”) podobne do języków, takich jak coffeescript lub babel.

W TypeScript twój interfejs może wyglądać następująco:

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

Czego nie możesz zrobić:

Shaedrich
źródło
3

w JavaScript nie ma natywnych interfejsów, istnieje kilka sposobów symulacji interfejsu. napisałem paczkę, która to robi

możesz zobaczyć implantację tutaj

Amit Wagner
źródło
2

JavaScript nie ma interfejsów. Ale można go wpisać w kaczkę, przykład można znaleźć tutaj:

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

Reinsbrain
źródło
Podoba mi się wzorzec, którego używa artykuł w tym łączu, aby wysunąć twierdzenia na temat typu. Zgłaszany jest błąd, gdy coś nie implementuje metody, na którą ma polegać, jest dokładnie tym, czego bym się spodziewał, i podoba mi się, jak mogę zgrupować te wymagane metody (jak interfejs), jeśli zrobię to w ten sposób.
Eric Dubé,
1
Nienawidzę transpilacji (i map źródłowych do debugowania), ale maszynopis jest tak blisko ES6, że mam skłonność do trzymania nosa i nurkowania w maszynie do pisania. ES6 / Typescript jest interesujący, ponieważ pozwala uwzględniać właściwości oprócz metod podczas definiowania interfejsu (zachowania).
Reinsbrain,
1

Wiem, że to stary, ale ostatnio odkryłem, że potrzebuję coraz więcej przydatnych narzędzi API do sprawdzania obiektów pod kątem interfejsów. Więc napisałem to: https://github.com/tomhicks/methodical

Jest również dostępny przez NPM: npm install methodical

Zasadniczo robi wszystko, co sugerowano powyżej, z pewnymi opcjami dotyczącymi bycia bardziej rygorystycznymi, a wszystko to bez konieczności wykonywania wielu zadań if (typeof x.method === 'function') .

Mam nadzieję, że ktoś uzna to za przydatne.

Tomek
źródło
Tom, właśnie obejrzałem wideo AngularJS TDD, a kiedy instaluje framework, jednym z zależnych pakietów jest twój pakiet metodyczny! Dobra robota!
Cody
Haha doskonale. Zasadniczo porzuciłem go, gdy ludzie w pracy przekonali mnie, że interfejsy w JavaScript są nie do przejścia. Ostatnio miałem pomysł na temat biblioteki, która zasadniczo proxy obiektu, aby upewnić się, że są w nim używane tylko niektóre metody, co jest w zasadzie interfejs. Nadal uważam, że interfejsy mają miejsce w JavaScript! Nawiasem mówiąc, czy możesz połączyć ten film wideo? Chciałbym rzucić okiem.
Tom
Obstawiasz, Tom. Spróbuję to wkrótce znaleźć. Dzięki anegdocie o interfejsach również jako serwerach proxy. Twoje zdrowie!
Cody,
1

To stare pytanie, ale ten temat nigdy nie przestaje mnie męczyć.

Ponieważ wiele odpowiedzi tutaj i w Internecie koncentruje się na „wymuszaniu” interfejsu, chciałbym zasugerować alternatywny widok:

Brak interfejsów najbardziej odczuwam, gdy korzystam z wielu klas, które zachowują się podobnie (tj. Implementują interfejs ).

Na przykład mam generator e-maili, który spodziewa się otrzymywać fabryki sekcji e-mail , które „wiedzą”, jak wygenerować zawartość sekcji i kod HTML. Dlatego wszyscy muszą mieć jakieś metody getContent(id)i getHtml(content)metody.

Najbliższy wzorzec interfejsów (choć wciąż jest to obejście), o którym mogłem pomyśleć, to użycie klasy, która otrzyma 2 argumenty, które zdefiniują 2 metody interfejsu.

Głównym wyzwaniem związanym z tym wzorcem jest to, że metody muszą albo być staticargumentem samej instancji, aby uzyskać dostęp do jej właściwości. Są jednak przypadki, w których uznaję to za kompromisowe.

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
źródło
0

taki abstrakcyjny interfejs

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

utwórz instancję:

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

i użyj go

let x = new MyType()
x.print()
Kevin Krausse
źródło