JavaScript是否具有接口类型(例如Java的“接口”)?


Answers:


648

没有“此类必须具有这些功能”的概念(也就是说,本身没有接口),因为:

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

7
它并不像事实那样可怕。 for...in一直(而且一直以来)都充满着这样的危险,任何这样做而至少没有考虑到有人添加的人Object.prototype(该文章自己承认这是一种不常见的技术)将看到他们的代码在别人的手中。
cHao 2010年

1
@entonio:我认为内置类型的可延展性是一个功能,而不是一个问题。这是使垫片/填充物可行的重要原因。没有它,我们要么将所有内置类型包装为可能不兼容的子类型,要么等待通用浏览器支持(当浏览器不支持某些东西导致人们不使用它而导致浏览器不支持时,这种情况永远不会出现)不支持)。因为可以修改内置类型,所以我们可以只添加许多尚不存在的功能。
cHao 2013年

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 2013年

1
呵呵...喜欢“坦白说,如果您要让狗嘎嘎叫,您就会遇到更大的问题。一个很好的类比表明语言不应该避免愚蠢。这总是一场失败的战斗。-斯科特
斯科普帕。 com

72

拿起Dustin Diaz的“ JavaScript设计模式 ” 的副本。有几章专门介绍如何通过Duck Typing实现JavaScript接口。读起来也不错。但是,没有,没有语言的本地实现接口,您必须使用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
}

在“ pro javascript设计模式”一书中描述的方法可能是我在这里阅读过的东西和尝试过的东西中最好的方法。您可以在继承之上使用继承,这使遵循OOP概念变得更好。有些人可能声称您在JS中不需要OOP概念,但是我希望有所不同。
animageofmine15年

21

JavaScript(ECMAScript版本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);

有很多方法可以使这只猫变皮,但这是我用于自己的Interface实现的逻辑。我发现我更喜欢这种方法,并且易于阅读和使用(如您在上面看到的)。这确实意味着增加了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
当来自更清晰的OO语言实现时,JavaScript一定会这样做(伤脑筋)。
史蒂文·德·萨拉斯

10
@StevendeSalas:恩。当您停止尝试将它视为面向类的语言时,JS实际上往往非常干净。模拟类,接口等所需的所有废话,实际上会让您的大脑受伤。原型?实际上,一旦您停止与之抗争,便会获得一些简单的东西。
cHao 2014年

“ // ..检查成员的逻辑。”中的内容 ?看起来像什么?
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
};

参加者

戒律解析器

resolvePrecept函数是在Abstract类内部使用的实用程序和辅助函数。它的工作是允许对封装的规范(数据和行为)进行定制的实现处理。它可能会引发错误或发出警告-并且-将默认值分配给Implementor类。

iAbstractClass

iAbstractClass定义要使用的接口。它的方法需要与其实现者类达成默契。此接口将每个规范分配给相同的确切规范名称空间-或-分配给Precept Resolver函数返回的任何内容。但是,默认协议解决了一个环境 -实施者的规定。

实施者

实现者简单地(“一致”与接口iAbstractClass在这种情况下)和通过使用其应用于构造劫持iAbstractClass.apply(this)。通过定义上面的数据和行为,然后劫持接口的构造函数(将Implementor的上下文传递给Interface构造函数),我们可以确保将添加Implementor的替代,并且Interface将显示警告和默认值。

这是一种非常轻松的方法,在一段时间内以及在不同的项目中为我的团队和我都提供了很好的服务。但是,它确实有一些警告和缺点。

缺点

尽管这在很大程度上有助于实现整个软件的一致性,但它并没有实现真正的接口,而是模拟了它们。尽管定义,默认值和警告或错误已被阐明,但使用的表现是由开发人员强制执行和断言的(与许多JavaScript开发一样)。

这似乎是“ JavaScript接口”的最佳方法,但是,我希望看到以下解决方法:

  • 断言类型的断言
  • 签名断言
  • delete动作冻结对象
  • 断言JavaScript社区的特殊性中普遍存在或需要的其他任何事物

就是说,希望对我和我的团队有帮助。


7

您需要Java中的接口,因为它是静态类型的,并且在编译期间应该知道类之间的协定。在JavaScript中则有所不同。JavaScript是动态类型的;这意味着当您获得对象时,您只需检查它是否具有特定的方法并调用它即可。


1
实际上,您不需要Java中的接口,确保对象具有特定的API是一种故障保护,因此可以将它们换出其他实现。
BGerrissen

3
不,Java实际上需要它们,以便它可以为在编译时实现接口的类构建vtable。声明一个类实现一个接口将指示编译器构建一个小的结构,该结构包含指向该接口所需的所有方法的指针。否则,它将必须在运行时按名称调度(就像动态类型语言一样)。
优厚

我认为那是不对的。在Java中,调度始终是动态的(除非方法是最终的),并且该方法属于接口这一事实不会更改查找规则。静态类型语言需要接口的原因是,因此您可以使用相同的“伪类型”(接口)来引用不相关的类。
13年

2
@entonio:调度并不像看起来那样动态。由于多态性,直到运行时为止,实际的方法通常都不为人所知,但是字节码并没有说“ invoke yourMethod”。它说“调用Superclass.yourMethod”。JVM无法在不知道要查找哪个类的情况下调用方法。在链接期间,它可能放在vtable yourMethod中的条目#5上Superclass,并且对于每个具有自己的yourMethod子类,只需指向该子类的条目#5在适当的实施。
cHao 2013年

1
@entonio:对于接口,规则确实会有所改变。(不是语言方面的,但是生成的字节码和JVM的查找过程是不同的。)名为Implementation实现的类SomeInterface并不只是说它实现了整个接口。它具有表示“我实现SomeInterface.yourMethod”并指向的方法定义的信息Implementation.yourMethod。JVM调用时SomeInterface.yourMethod,它将在类中查找有关该接口的方法的实现的信息,并发现需要调用Implementation.yourMethod
cHao 2013年

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

您不能做什么:



2

Javascript没有接口。但是它可以是鸭子类型的,可以在这里找到一个示例:

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


我喜欢该链接上的文章用来对类型进行断言的模式。当某事未实现其应有的方法时引发的错误正是我所期望的,并且我喜欢如何以这种方式将这些必需的方法组合在一起(例如接口)。
EricDubé15年

1
我讨厌转译(以及用于调试的源映射),但是Typescript距离ES6非常近,以至于我倾向于hold着鼻子跳进Typescript。ES6 / Typescript很有趣,因为它在定义接口(行为)时允许您在方法之外包括属性。
Reinsbrain 2015年

1

我知道这是一个旧的,但是最近我发现自己越来越需要一个方便的API来根据接口检查对象。所以我写了这个:https : //github.com/tomhicks/methodical

也可以通过NPM获得: npm install methodical

它基本上完成了上面建议的所有操作,并提供了一些更严格的选项,并且所有这些都无需进行if (typeof x.method === 'function')样板工作。

希望有人发现它有用。


汤姆,我刚刚看了一个AngularJS TDD视频,当他安装框架时,相关软件包之一就是您的方法软件包!做得好!
科迪

哈哈!在工作人员说服我使用JavaScript界面​​是行不通之后,我基本上放弃了它。最近,我想到了一个库,该库基本上是对象的代理,以确保仅在其上使用某些方法,而这基本上就是接口。我仍然认为接口在JavaScript中占有一席之地!您可以顺便链接该视频吗?我想看看。
汤姆(Tom)

你打赌,汤姆。我会尽快找到它。还要提到有关接口作为代理的轶事。干杯!
科迪2014年

1

这是一个古老的问题,不过,这个话题永远不会困扰我。

由于此处和整个网络上的许多答案都集中在“强制”界面上,我想提出一个替代的观点:

当我使用行为相似的多个类(即实现一个interface)时,我感到最缺乏接口

例如,我有一个电子邮件生成器,期望接收“ 电子邮件节工厂”,“知道”如何生成节的内容和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()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.