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

IT技术 javascript oop
2021-02-05 18:04:18

我正在学习如何使用 JavaScript 进行 OOP有没有接口概念(比如Java的interface)?

所以我将能够创建一个监听器......

6个回答

没有“这个类必须具有这些功能”的概念(即,本身没有接口),因为:

  1. JavaScript 继承基于对象,而不是类。这没什么大不了的,直到你意识到:
  2. JavaScript 是一种非常动态的类型语言——您可以使用适当的方法创建一个对象,这将使其符合接口,然后取消定义使其符合的所有内容颠覆类型系统是如此容易——即使是偶然的!-- 首先尝试创建类型系统是不值得的。

相反,JavaScript 使用所谓的鸭子类型(如果它像鸭子一样走路,像鸭子一样嘎嘎叫,就 JS 而言,它就是一只鸭子。)如果您的对象具有 quack()、walk() 和 fly() 方法,则代码可以在它期望的任何地方使用它一个可以行走、嘎嘎叫和飞行的对象,不需要实现一些“Duckable”接口。接口正是代码使用的函数集(以及这些函数的返回值),通过鸭子输入,您可以免费获得这些。

现在,这并不是说您的代码不会中途失败,如果您尝试调用some_dog.quack(); 你会得到一个类型错误。坦率地说,如果你让狗嘎嘎叫,你的问题会稍微大一些;可以这么说,当您将所有鸭子排成一排时,鸭子打字效果最佳,并且除非您将它们视为通用动物,否则不要让狗和鸭子混在一起。换句话说,即使界面是流动的,它仍然存在;将狗传递给期望它嘎嘎飞行的代码通常是错误的。

但是,如果您确定自己在做正确的事情,则可以通过在尝试使用特定方法之前测试其是否存在来解决嘎嘎狗问题。就像是

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();
}
它并不像人们想象的那么可怕。 for...in是——而且一直是——充满了这样的危险,任何人如果没有至少考虑到有人加入Object.prototype(一种并不罕见的技术,该文章自己承认)就会看到他们的代码在其他人手中被破坏。
2021-03-16 18:04:18
@entonio:我认为内置类型的延展性是一个特性而不是一个问题。这是使 shims/polyfills 可行的很大一部分。没有它,我们要么用可能不兼容的子类型包装所有内置类型,要么等待通用浏览器支持(这可能永远不会出现,当浏览器不支持某些东西时,人们不会使用它,因为浏览器不支持它)不支持)。因为可以修改内置类型,我们可以改为添加许多尚不存在的函数。
2021-03-17 18:04:18
在最新版本的 Javascript (1.8.5) 中,您可以将对象的属性定义为不可枚举。这样你就可以避免这个for...in问题。developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
2021-03-24 18:04:18
呵呵...喜欢“坦率地说,如果你让狗嘎嘎叫,你的问题会稍微大一些。很好的类比表明语言不应该试图避免愚蠢。那总是一场失败的战斗。-斯科特
2021-03-29 18:04:18
@Tomás:可悲的是,直到每个浏览器都运行与 ES5 兼容的东西,我们仍然需要担心这样的事情。即便如此,“for...in问题”在某种程度上仍然存在,因为总会有草率的代码......嗯,那,而且Object.defineProperty(obj, 'a', {writable: true, enumerable: false, value: 3});不仅仅是obj.a = 3;. 我完全可以理解人们不想更频繁地这样做。:P
2021-04-01 18:04:18

获取一份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 design patterns”一书中描述的方法可能是我在这里读过的以及我尝试过的一堆东西中最好的方法。您可以在它之上使用继承,这使得遵循 OOP 概念变得更好。有些人可能会声称您不需要 JS 中的 OOP 概念,但我不同意。
2021-03-24 18:04:18

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

有很多方法可以给这只特定的猫剥皮,但这是我用于我自己的接口实现的逻辑。我发现我更喜欢这种方法,而且它易于阅读和使用(如您所见)。这确实意味着添加一个“实现”方法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;
}
这种语法真的很伤我的大脑,但这里的实现很有趣。
2021-03-17 18:04:18
嗨@We,检查成员逻辑意味着循环访问所需的属性,如果缺少则抛出错误.. 类似于var interf = arguments[i]; for (prop in interf) { if (this.prototype[prop] === undefined) { throw 'Member [' + prop + '] missing from class definition.'; }}. 有关更详细的示例,请参阅文章链接的底部
2021-03-18 18:04:18
@StevendeSalas:嗯。当您不再尝试将 JS 视为面向类的语言时,它实际上往往非常干净。模拟类、接口等所需的所有废话……这才是真正让你的大脑受伤的原因。原型?简单的东西,真的,一旦你停止与他们战斗。
2021-03-23 18:04:18
当来自更清晰的 OO 语言实现时,Javascript 必然会这样做(伤害大脑)。
2021-03-24 18:04:18
“// .. Check member's logic 中的内容。” ? 那看起来像什么?
2021-04-07 18:04:18

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 Class 中使用它的工作是允许对封装的Precepts(数据和行为)进行定制的实现处理它可以抛出错误或警告——并且——为Implementor 类分配一个默认值。

抽象类

iAbstractClass定义要使用的接口。它的方法需要与它的Implementor 类达成默契。该接口将每个戒律分配给相同的戒律命名空间——或者——分配给戒律解析器函数返回的任何内容。然而,默认协议解决了一个上下文——一个Implementor 的规定。

实施者

实现者只是“同意”一个接口(在本例中为iAbstractClass)并通过使用构造函数劫持来应用它iAbstractClass.apply(this)通过定义上面的数据和行为,然后劫持接口的构造函数——将实现者的上下文传递给接口构造函数——我们可以确保将添加实现者的覆盖,并且该接口将解释警告和默认值。

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

缺点

尽管这有助于在很大程度上实现整个软件的一致性,但它并没有实现真正的接口——而是模拟它们。虽然定义,默认值,警告或错误阐明,使用的解释是执行和断言由开发商(如多JavaScript开发的)。

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

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

也就是说,我希望这对您和我的团队一样有帮助。

希望,任何仍在寻找答案的人都会发现它有帮助。

您可以尝试使用代理(这是自 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;

如果您想通过instanceof(@Kamaffeather 询问)进行检查,您可以将其包装在一个对象中,如下所示:

class LatLngLiteral {
    constructor(props)
    {
        this.proxy = new Proxy(this, {
            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;
            }
        })
        return this.proxy
    }
}

这可以在不使用Proxy而是使用gettersetter类的情况下完成

class LatLngLiteral {
    #latitude;
    #longitude;

    get lat()
    {
        return this.#latitude;
    }

    get lng()
    {
        return this.#longitude;
    }
    
    set lat(val)
    {
        //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(!(0 < val && val < 90)) {
            throw new RangeError('Position is out of range!');
        }
        
        this.#latitude = val
    }
    
    set lng(val)
    {
        //the dec format only accepts numbers
        if(typeof val !== 'number') {
            throw new TypeError('Value must be numeric');
        }

        //longitude is in range between 0 and 180
        if(!(0 < val && val < 180)) {
            throw new RangeError('Position is out of range!');
        }
        
        this.#longitude = val
    }
}
有什么方法可以使用代理也有一个可以通过检查的命名接口instanceof喜欢true === myMap.position instanceof latLngLiteral
2021-03-18 18:04:18
@Kamafeather 好吧,稍微改变一下,这是可能的(请参阅我更新的答案)
2021-04-04 18:04:18