Есть ли в JavaScript тип интерфейса (например, Java-интерфейс)?

324

Я учусь делать ООП с помощью JavaScript . Есть ли у него концепция интерфейса (например, Java interface)?

Так что я бы смог создать слушателя ...

Том Брито
источник
18
Для тех, кто ищет больше возможностей, в TypeScript есть интерфейсы .
SD
2
Другой вариант, если вы хотите использовать vanilla JS, это реализовать.js , как показано здесь
Ричард Ловелл

Ответы:

650

Там нет понятия "этот класс должен иметь эти функции" (то есть, интерфейсы как таковые), потому что:

  1. Наследование JavaScript основано на объектах, а не на классах. Это не имеет большого значения, пока вы не поймете:
  2. JavaScript - это язык с чрезвычайно динамической типизацией - вы можете создать объект с надлежащими методами, которые приведут его в соответствие с интерфейсом, а затем отменить определение всего, что сделало его соответствующим . Было бы так легко подорвать систему типов - даже случайно! - что в первую очередь не стоит пытаться создать систему типов.

Вместо этого в JavaScript используется так называемая « утиная печать» . (Если он ходит как утка и крякает как утка, то, что касается JS, это утка.) Если ваш объект имеет методы quack (), walk () и fly (), код может использовать его везде, где он ожидает объект, который может ходить, крякать и летать, не требуя реализации какого-либо "Duckable" интерфейса. Интерфейс - это в точности набор функций, которые использует код (и возвращаемые значения из этих функций), и с помощью утки вы получаете это бесплатно.

Это не значит, что ваш код не потерпит неудачу на полпути, если вы попытаетесь позвонить some_dog.quack(); вы получите TypeError. Честно говоря, если вы говорите собакам крякать, у вас есть немного большие проблемы; Печатание утки работает лучше всего, когда вы, так сказать, держите всех своих уток в ряд и не позволяете собакам и уткам смешиваться, если вы не относитесь к ним как к обычным животным. Другими словами, несмотря на то, что интерфейс плавный, он все еще там; часто бывает ошибкой передавать собаку в код, который ожидает, что она сначала будет крякать и летать.

Но если вы уверены, что поступаете правильно, вы можете обойти проблему собаководства, проверив наличие конкретного метода, прежде чем пытаться его использовать. Что-то вроде

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

Таким образом, вы можете проверить все методы, которые вы можете использовать, прежде чем использовать их. Синтаксис довольно уродливый, хотя. Есть немного более красивый способ:

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

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

Это стандартный JavaScript, поэтому он должен работать в любом интерпретаторе JS, который стоит использовать. Это имеет дополнительное преимущество чтения как английский.

Для современных браузеров (то есть практически любого браузера, кроме IE 6-8), есть даже способ, чтобы свойство не отображалось в for...in:

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

Проблема заключается в том, что у объектов IE7 нет .definePropertyвообще, а в IE8 он предположительно работает только с хост-объектами (то есть с элементами DOM и т. Д.). Если совместимость является проблемой, вы не можете использовать .defineProperty. (Я даже не буду упоминать IE6, потому что он больше не имеет значения за пределами Китая.)

Другая проблема заключается в том, что некоторым стилям кодирования нравится предполагать, что каждый пишет плохой код, и запрещать модификацию Object.prototypeв случае, если кто-то хочет использовать вслепую for...in. Если вы заботитесь об этом или используете (IMO сломанный ) код, который делает, попробуйте немного другую версию:

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

if (can(someObject, "quack"))
{
    someObject.quack();
}
Chao
источник
7
Это не так ужасно, как было задумано. for...inэто - и всегда было - чревато такими опасностями, и любой, кто делает это, по крайней мере, не считая, что кто-то добавил Object.prototype(что весьма необычно, по собственному признанию этой статьи), увидит, как его код сломается в чужих руках.
Чао
1
@entonio: Я бы рассматривал гибкость встроенных типов как особенность, а не проблему. Это большая часть того, что делает прокладки / полифиллы осуществимыми. Без этого мы либо обернули бы все встроенные типы с возможно несовместимыми подтипами, либо ждали бы универсальной поддержки браузера (которая может никогда не прийти, если браузеры не поддерживают вещи, потому что люди не используют их, потому что браузеры не ' не поддерживает это). Поскольку встроенные типы могут быть изменены, мы можем вместо этого просто добавить многие функции, которые еще не существуют.
cHao
1
В последней версии Javascript (1.8.5) вы можете определить свойство объекта как не перечисляемое. Таким образом, вы можете избежать for...inпроблемы. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Томас Прадо
1
@ Tomás: К сожалению, пока в каждом браузере работает что-то совместимое с ES5, нам все еще приходится беспокоиться о таких вещах. И даже тогда, « for...inпроблема» все еще будет существовать в некоторой степени, потому что всегда будет неаккуратный код ... ну, это, и Object.defineProperty(obj, 'a', {writable: true, enumerable: false, value: 3});это немного больше работы, чем просто obj.a = 3;. Я могу полностью понять людей, которые не пытаются делать это чаще. : P
cHao
1
Хе-хе ... обожаю: «Честно говоря, если вы говорите собакам крякать, у вас есть немного большие проблемы. Отличная аналогия, чтобы показать, что языки не должны пытаться избежать глупости. Это всегда проигранная битва. -Скотт
Скуппа. ком
73

Возьмите копию « Шаблоны дизайна JavaScript » Дастина Диаса . Есть несколько глав, посвященных реализации интерфейсов JavaScript через Duck Typing. Это тоже приятно читать. Но нет, языковой реализации интерфейса не существует, вы должны использовать 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
источник
Метод, описанный в книге «Шаблоны проектирования pro javascript», вероятно, является лучшим подходом из множества вещей, которые я прочитал здесь и из того, что я попробовал. Кроме того, вы можете использовать наследование, что делает его еще лучше следовать концепциям ООП. Некоторые могут утверждать, что вам не нужны концепции ООП в JS, но я позволю себе не согласиться.
animageofmine
21

В JavaScript (ECMAScript edition 3) implementsзарезервированное слово сохранено для будущего использования . Я думаю, что это предназначено именно для этой цели, однако, в спешке, чтобы вывести спецификацию за дверь, у них не было времени, чтобы определить, что с ней делать, поэтому в настоящее время браузеры ничего не делают, кроме пусть он сидит там и иногда жалуется, если вы пытаетесь использовать его для чего-то.

Возможно и действительно достаточно просто создать свой собственный Object.implement(Interface)метод с логикой, которая искажает всякий раз, когда определенный набор свойств / функций не реализован в данном объекте.

Я написал статью об объектной ориентации, в которой использую мою собственную запись следующим образом :

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

Есть много способов обшить эту конкретную кошку, но эту логику я использовал для собственной реализации интерфейса. Я считаю, что предпочитаю этот подход, и его легко читать и использовать (как вы можете видеть выше). Это означает добавление метода «внедрить», с Function.prototypeкоторым у некоторых людей могут возникнуть проблемы, но я считаю, что он прекрасно работает.

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
Этот синтаксис действительно вредит моему мозгу, но реализация здесь довольно интересна.
Cypher
2
Javascript обязан делать это (вредит мозгу), особенно если он исходит из более чистых реализаций языка OO.
Стивен де Салас
10
@StevendeSalas: Эх. JS на самом деле имеет тенденцию быть довольно чистым, когда вы перестаете пытаться рассматривать его как класс-ориентированный язык. Все дерьмо, необходимое для эмуляции классов, интерфейсов и т. Д., Вот что действительно сделает ваш мозг болит. Прототипы? Простые вещи, правда, когда ты перестанешь с ними бороться.
Чао
что в "// .. Проверьте логику члена." ? На что это похоже?
PositiveGuy
Привет @We, проверка логики членов означает циклический просмотр нужных свойств и выдачу ошибки, если она отсутствует ... что-то вроде var interf = arguments[i]; for (prop in interf) { if (this.prototype[prop] === undefined) { throw 'Member [' + prop + '] missing from class definition.'; }}. Смотрите в нижней части статьи ссылку для более подробного примера.
Стивен де Салас
12

Интерфейсы JavaScript:

Хотя JavaScript это не имеет interfaceтип, это зачастую необходимо. По причинам, связанным с динамической природой JavaScript и использованием Prototypical-Inheritance, трудно обеспечить согласованные интерфейсы между классами - однако это возможно; и часто подражать.

На данный момент есть несколько конкретных способов эмулировать интерфейсы в JavaScript; Разница в подходах обычно удовлетворяет некоторые потребности, в то время как другие остаются без внимания. Часто самый надежный подход слишком громоздок и мешает разработчику (разработчику).

Вот подход к интерфейсам / абстрактным классам, который не очень громоздок, объясняет, сводит к минимуму реализации внутри абстракций и оставляет достаточно места для динамических или пользовательских методологий:

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

участники

Завет Резолвер

Эта resolvePreceptфункция является вспомогательной и вспомогательной функцией для использования внутри вашего абстрактного класса . Его работа заключается в том, чтобы обеспечить настраиваемую обработку реализации инкапсулированных Предписаний (данных и поведения) . Он может выдавать ошибки или предупреждать - И - назначать значение по умолчанию для класса Implementor.

iAbstractClass

iAbstractClassОпределяет интерфейс , который будет использоваться. Его подход влечет за собой молчаливое соглашение с его классом Implementor. Этот интерфейс назначает каждую заповедь одному и тому же пространству имен точных заповедей - ИЛИ - любому, что возвращает функция Rescept Resolver . Однако молчаливое соглашение разрешает контекст - положение исполнителя.

Implementor

Implementor просто «согласен» с интерфейсом ( iAbstractClass в данном случае) и применяет его с помощью Конструктора-Угон : iAbstractClass.apply(this). Определив данные и поведение выше, а затем перехватив конструктор интерфейса - передав контекст реализатора конструктору интерфейса - мы можем гарантировать, что переопределения реализатора будут добавлены и что интерфейс будет объяснять предупреждения и значения по умолчанию.

Это очень не обременительный подход, который послужил моей команде и мне очень хорошо в течение времени и различных проектов. Тем не менее, у него есть некоторые предостережения и недостатки.

Недостатки

Хотя это в значительной степени помогает обеспечить согласованность всего программного обеспечения, оно не реализует настоящие интерфейсы, а имитирует их. Хотя определения, по умолчанию, и предупреждения или ошибки являются пояснено, экспликация использования в исполнение и утверждается застройщиком (как и большая часть развития JavaScript).

Это, казалось бы, лучший подход к «Интерфейсам в JavaScript» , однако мне бы хотелось, чтобы было решено следующее:

  • Утверждения типов возврата
  • Утверждения подписей
  • Заморозить объекты из deleteдействий
  • Утверждения о чем-либо еще преобладающем или нужном в специфике сообщества JavaScript

Тем не менее, я надеюсь, что это поможет вам так же, как и моя команда и я.

Cody
источник
7

Вам нужны интерфейсы в Java, так как он статически типизирован и контракт между классами должен быть известен во время компиляции. В JavaScript это отличается. JavaScript динамически типизируется; это означает, что когда вы получаете объект, вы можете просто проверить, есть ли у него определенный метод, и вызвать его.

Алекс Рейтборт
источник
1
На самом деле вам не нужны интерфейсы в Java, отказоустойчиво гарантировать, что у объектов есть определенный API, чтобы вы могли поменять их для других реализаций.
Б.Герриссен
3
Нет, они действительно нужны в Java, чтобы он мог создавать таблицы для классов, которые реализуют интерфейс во время компиляции. Объявление о том, что класс реализует интерфейс, указывает компилятору на создание небольшой структуры, содержащей указатели на все методы, необходимые для этого интерфейса. В противном случае он должен был бы отправлять по имени во время выполнения (как это делают языки с динамической типизацией).
Великолепно
Я не думаю, что это правильно. Диспетчеризация всегда является динамической в ​​Java (если, возможно, метод не является окончательным), и тот факт, что метод принадлежит интерфейсу, не меняет правила поиска. Причины, по которым интерфейсы необходимы в статически типизированных языках, заключаются в том, что вы можете использовать один и тот же псевдотип (интерфейс) для ссылки на несвязанные классы.
entonio
2
@entonio: отправка не так динамична, как кажется. Фактический метод часто не известен до времени выполнения, благодаря полиморфизму, но байт-код не говорит «invoke yourMethod»; он говорит "вызвать Superclass.yourMethod". JVM не может вызывать метод, не зная, в каком классе его искать. Во время компоновки он может поместить yourMethodв запись № 5 в Superclassтаблице vtable, а для каждого подкласса, который имеет свой собственный yourMethod, просто указывает запись этого подкласса № 5. при соответствующей реализации.
cHao
1
@entonio: Для интерфейсов, правила действительно немного изменить. (Не языком, но сгенерированный байт-код и процесс поиска JVM отличаются.) Класс с именем, Implementationкоторый реализует SomeInterface, не просто говорит, что он реализует весь интерфейс. В нем есть информация, которая говорит «Я реализую SomeInterface.yourMethod» и указывает на определение метода для Implementation.yourMethod. Когда JVM вызывает SomeInterface.yourMethod, он ищет в классе информацию о реализациях метода этого интерфейса и находит, что ему нужно вызвать Implementation.yourMethod.
cHao
6

Надеюсь, что любой, кто все еще ищет ответ, найдет его полезным.

Вы можете попробовать использовать прокси (это стандартно с 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;
    }
});

Тогда вы можете легко сказать:

myMap = {}
myMap.position = latLngLiteral;
shaedrich
источник
5

Если вы хотите использовать транскомпилятор, вы можете попробовать TypeScript. Он поддерживает черновые функции ECMA (в предложении интерфейсы называются « протоколы »), аналогичные тем, которые используются в таких языках, как coffeescript или babel.

В TypeScript ваш интерфейс может выглядеть так:

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

Что вы не можете сделать:

shaedrich
источник
3

в JavaScript нет нативных интерфейсов, есть несколько способов симулировать интерфейс. я написал пакет, который делает это

вы можете увидеть имплантацию здесь

Амит Вагнер
источник
2

Javascript не имеет интерфейсов. Но это может быть напечатано уткой, пример можно найти здесь:

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

Reinsbrain
источник
Мне нравится шаблон, который используется в статье по этой ссылке, чтобы делать утверждения о типе. Выдается ошибка, когда что-то не реализует метод, который он должен был сделать, именно то, что я ожидал, и мне нравится, как я могу сгруппировать эти необходимые методы вместе (например, интерфейс), если я сделаю это таким образом.
Эрик Дубе
1
Я ненавижу транспилирование (и исходные карты для отладки), но Typescript настолько близок к ES6, что я склонен держать нос и погружаться в Typescript. ES6 / Typescript интересен тем, что он позволяет включать свойства в дополнение к методам при определении интерфейса (поведения).
Reinsbrain
1

Я знаю, что это старый, но в последнее время я все больше и больше нуждался в удобном API для проверки объектов по интерфейсам. Поэтому я написал это: https://github.com/tomhicks/methodical

Это также доступно через NPM: npm install methodical

Он в основном делает все, что предложено выше, с некоторыми вариантами, чтобы быть немного более строгим, и все без необходимости if (typeof x.method === 'function')загружать шаблон.

Надеюсь, кто-то найдет это полезным.

Том
источник
Том, я только что посмотрел видео AngularJS TDD, и когда он устанавливает фреймворк, один из зависимых пакетов - ваш методический пакет! Хорошая работа!
Коди
Хаха отлично. Я в основном отказался от этого после того, как люди на работе убедили меня, что интерфейсы в JavaScript не нужны. Недавно у меня возникла идея библиотеки, которая в основном проксирует объект, чтобы гарантировать, что в нем используются только определенные методы, то есть интерфейс. Я все еще думаю, что интерфейсы имеют место в JavaScript! Можете ли вы связать это видео между прочим? Я хотел бы взглянуть.
Том
Вы держите пари, Том. Я постараюсь найти это в ближайшее время. И еще анекдот про интерфейсы как прокси. Ура!
Коди
1

Это старый вопрос, тем не менее эта тема не перестает меня беспокоить.

Поскольку многие ответы здесь и в Интернете направлены на «усиление» интерфейса, я хотел бы предложить альтернативное представление:

Больше всего мне не хватает интерфейсов, когда я использую несколько классов, которые ведут себя одинаково (т.е. реализуют интерфейс ).

Например, у меня есть Генератор электронной почты, который ожидает получать фабрики разделов электронной почты , которые «знают», как генерировать содержимое разделов и HTML. Следовательно, все они должны иметь какие-то getContent(id)и getHtml(content)методы.

Самый близкий шаблон к интерфейсам (хотя это все еще обходной путь), о котором я мог подумать, это использование класса, который получит 2 аргумента, которые будут определять 2 метода интерфейса.

Основная проблема с этим шаблоном заключается в том, что методы должны быть staticили получить в качестве аргумента сам экземпляр, чтобы получить доступ к его свойствам. Однако есть случаи, в которых я считаю, что этот компромисс стоит хлопот.

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
источник
0

абстрактный интерфейс, как это

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

создать экземпляр:

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

и использовать это

let x = new MyType()
x.print()
Кевин Краус
источник