Чи має тип інтерфейсу JavaScript (наприклад, інтерфейс Java)?


324

Я вчуся робити OOP за допомогою JavaScript . Чи є в ньому концепція інтерфейсу (наприклад, Java interface)?

Тож я міг би створити слухача ...


18
Для тих, хто шукає більше варіантів, TypeScript має інтерфейси .
SD

2
Ще один варіант, якщо ви хочете використовувати ванільний JS, це Imple.js , як показано тут
Річард Ловелл

Відповіді:


649

Немає поняття "цей клас повинен мати ці функції" (тобто ніяких інтерфейсів сам по собі), оскільки:

  1. Успадкування JavaScript засноване на об'єктах, а не на класах. Це не велика справа, поки ти не усвідомиш:
  2. JavaScript є надзвичайно динамічно набраною мовою - ви можете створити об’єкт належними методами, які б змусили його відповідати інтерфейсу, а потім визначити всі речі, які його зробили . Підібрати систему типу було б так просто - навіть випадково! - що не варто було б намагатися в першу чергу зробити типову систему.

Натомість JavaScript використовує те, що називається типом качок . (Якщо він ходить як качка, і стукає, як качка, наскільки це піклується JS, це качка.) Якщо у вашого об'єкта є методи крякання (), 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();
}

7
Це не так жахливо, як це зроблено. for...inтака небезпека - і завжди була загрожена, і кожен, хто це робить, хоча б не враховуючи, що хтось, до якого додано Object.prototype(що не є рідкісною методикою, за власним визнанням цієї статті), побачить, як їхній код порушився в чужих руках.
cHao

1
@entonio: Я вважаю, що корисність вбудованих типів є функцією, а не проблемою. Це велика частина того, що робить покриття / поліфіли можливими. Без цього ми або обмотуємо всі вбудовані типи можливо несумісними підтипами, або очікуємо універсальної підтримки браузера (яка може ніколи не прийти, коли браузери не підтримують матеріал, оскільки люди не використовують його, тому що браузери не роблять ' t підтримувати його). Оскільки вбудовані типи можна змінювати, ми можемо замість цього просто додати багато функцій, які ще не існують.
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
Хе-х ... люблю "Чесно кажучи, якщо ви говорите собакам, що блукають, у вас є трохи більші проблеми. Чудова аналогія, яка показує, що мови не повинні намагатися уникати дурості. Це завжди програна битва. -Шотт
Скооппа. com

72

Візьміть копію " Шаблонів дизайну JavaScript " від Дастіна Діаза . Існує кілька розділів, присвячених реалізації інтерфейсів JavaScript через друк Duck. Це також приємне читання. Але ні, немає інтерфейсної мовної реалізації інтерфейсу, ви повинні 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
}

Метод, описаний у книзі "про шаблони дизайну javascript" - це, мабуть, найкращий підхід з низки речей, які я прочитав тут, і з того, що я спробував. Ви можете використовувати спадщину поверх неї, що робить ще краще слідувати концепціям OOP. Дехто може стверджувати, що вам не потрібні концепції OOP в JS, але я прошу відрізнятися.
animageofmine

21

JavaScript (видання 3 ECMAScript) має 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 зобов’язаний це робити (боляче мозку), особливо коли вони надходять із більш чистих мовних втілень.
Стівен де Салас

10
@StevendeSalas: Е. JS насправді має тенденцію бути досить чистим, коли ви перестаєте намагатися ставитися до цього як до класно-орієнтованої мови. Всі лайно, необхідні для наслідування класів, інтерфейсів тощо ... саме це дійсно зробить ваш мозок боляче. Прототипи? Справді, прості речі, як тільки ви перестанете боротися з ними.
cHao

що в "// .. Перевірте логіку учасника." ? як це виглядає?
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 та використання прототипічного спадкування, важко забезпечити послідовні інтерфейси між класами - однак це можливо; і часто наслідується

На даний момент існує декілька певних способів емуляції інтерфейсів у 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
};

Учасники

Рецепт Resolver

Ця resolvePreceptфункція - це програма утиліта та помічник, яка використовується всередині вашого абстрактного класу . Його завдання полягає в тому, щоб забезпечити індивідуальну обробку реалізацією інкапсульованих рецептів (даних та поведінки) . Він може видаляти помилки або попереджати - AND - присвоювати значення за замовчуванням класу Implementor.

iAb абстрактClass

iAbstractClassВизначає інтерфейс , який буде використовуватися. Її підхід тягне за собою мовчазну угоду з класом Implementor. Цей інтерфейс призначає кожному рецепту однакове точне простір імен рецептур - АБО - тому, що повертається функція Precept Resolver . Однак мовчазна угода узгоджується з контекстом - положенням Виконавця.

Виконавець

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

Це дуже громіздкий підхід, який дуже довго служив моїй команді та я дуже добре працював у різних проектах. Однак у нього є деякі застереження та недоліки.

Недоліки

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

Це, здається, найкращий підхід до "Інтерфейсів у JavaScript" , проте я б хотів, щоб це було вирішено:

  • Твердження типів повернення
  • Твердження підписів
  • Заморожуйте об'єкти від deleteдій
  • Твердження про все інше, що переважає або потребує специфіки спільноти JavaScript

Це сказало, я сподіваюся, що це допоможе вам настільки, наскільки це має моя команда та я.


7

Інтерфейси в Java вам потрібні, оскільки вони набрані статично і контракт між класами повинен бути відомий під час компіляції. У JavaScript це інше. JavaScript динамічно набирається; це означає, що коли ви отримаєте об'єкт, ви можете просто перевірити, чи є у нього певний метод, і викликати його.


1
Насправді, вам не потрібні інтерфейси на Java, безпечно забезпечити об'єкти певним API, щоб ви могли замінити їх іншими реалізаціями.
BGerrissen

3
Ні, вони фактично потрібні в Java, щоб вони могли створювати vtables для класів, які реалізують інтерфейс під час компіляції. Заявивши, що клас реалізує інтерфейс, доручає компілятору створити невелику структуру, яка містить покажчики на всі методи, необхідні для цього інтерфейсу. В іншому випадку доведеться відправляти по імені під час виконання (як це роблять динамічно набрані мови).
чудовий

Я не думаю, що це правильно. Диспетчерство завжди є динамічним у Java (якщо, можливо, метод не є остаточним), і той факт, що метод належить до інтерфейсу, не змінює правила пошуку. Причина інтерфейсів потрібна в мовах статичного типу, тому ви можете використовувати той же «псевдо-тип» (інтерфейс) для позначення неспоріднених класів.
entonio

2
@entonio: Відправка не така динамічна, як виглядає. Справжній метод часто не відомий до часу виконання, завдяки поліморфізму, але байтовий код не говорить "виклик вашого методу"; там написано "виклик Superclass.yourMethod". JVM не може викликати метод, не знаючи, у якому класі його шукати. Під час посилання він може ставити yourMethodпід записом № 5 в Superclassvtable, а для кожного підкласу, який має свій власний 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;

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

Що ви не можете зробити:


3

в JavaScript немає власних інтерфейсів, є кілька способів імітувати інтерфейс. я написав пакет, який це робить

ви можете побачити імплантацію тут


2

У Javascript немає інтерфейсів. Але це може бути набрано качка, приклад можна знайти тут:

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


Мені подобається шаблон, який використовує стаття за цим посиланням для твердження про тип. Помилка, яку викидають, коли щось не реалізує метод, який він повинен бути, саме те, що я очікував, і мені подобається, як я можу згрупувати ці необхідні методи разом (як інтерфейс), якщо я це роблю таким чином.
Ерік Дубе

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


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()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.