JavaScript是否有接口类型(如Java's 'interface')?

我正在学习如何使OOP与JavaScript。它是否有接口概念(如Java的interface)?

这样我就可以创建一个监听器。

409787 次浏览

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

没有“这个类必须有这些函数”的概念(也就是说,本身没有接口),因为:

  1. JavaScript继承基于对象,而不是类。这没什么大不了的,直到你意识到:
  2. JavaScript是一种动态类型语言——你可以用适当的方法创建一个对象,这将使它符合接口然后取消所有使它符合的东西。颠覆类型系统是如此容易——甚至是意外地!一开始就尝试创建一个类型系统是不值得的。

相反,JavaScript使用所谓的duck typing。(如果它像鸭子一样走路,像鸭子一样嘎嘎叫,在JS看来,它就是一只鸭子。)如果您的对象具有quack()、walk()和fly()方法,那么代码可以在任何期望对象能够walk、quack和fly的地方使用它,而不需要实现某种“Duckable”接口。接口正是代码使用的函数集(以及这些函数的返回值),通过duck键入,您可以免费获得这些功能。

现在,这并不是说你的代码不会中途失败,如果你试图调用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();
}

通过达斯汀·迪亚兹获取'JavaScript设计模式'的副本。有几章专门介绍通过Duck Typing实现JavaScript接口。这也是一本好书。但是不,没有接口的语言本机实现,你必须鸭子类型

// 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接口:

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

参与者

< em >规则解析器< / em >

resolvePrecept函数是一个实用程序。在抽象类中使用的helper函数。它的工作是允许对封装的戒律(数据&行为)进行自定义的实现处理。它可以抛出错误或警告——并且——将默认值赋给实现者类。

< em > iAbstractClass < / em >

iAbstractClass定义了要使用的接口。它的方法需要与其实现者类达成默契。该接口将每个格言分配给相同的规则名称空间——OR——给命令解析器函数返回的任何东西。然而,默认协议解决为上下文——实现者的一个规定。

< em >实现者< / em >

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

这是一种非常简单的方法,对我的团队非常有用。我很适应时间和不同的项目。然而,它确实有一些警告&缺点。

缺点

虽然这有助于在很大程度上实现整个软件的一致性,但它不实现true接口——但模仿它们。虽然定义、默认值、警告或错误都是解释的,但开发人员对使用的解释是执行,断言(就像大多数JavaScript开发一样)。

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

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

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

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

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

它基本上做了上面建议的所有事情,有一些更严格的选项,而且所有这些都不需要加载if (typeof x.method === 'function')样板文件。

希望有人觉得它有用。

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

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

希望任何还在寻找答案的人都能从中找到帮助。

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

当你想使用转换器时,你可以尝试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"
}

你不能做的:

  • 为类型值定义RegExp模式
  • 定义验证,比如字符串长度
  • 数字范围 . .
    JavaScript中没有本机接口, 有几种方法可以模拟接口。我已经写了一个包,它

    你可以看到植入在这里

像这样的抽象接口

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

这是一个老问题,但这个话题一直困扰着我。

由于这里和整个网络上的许多答案都集中在“强制”界面上,我想建议另一种观点:

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

例如,我有一个电子邮件发电机,它期望接收电子邮件部门工厂,它“知道”如何生成节的内容和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));

找到一个尽可能低影响的模拟接口的解决方案也困扰着我。

一种解决方案是制作一个工具:

/**
@parameter {Array|object} required : method name list or members types by their name
@constructor
*/
let Interface=function(required){
this.obj=0;
if(required instanceof Array){
this.obj={};
required.forEach(r=>this.obj[r]='function');
}else if(typeof(required)==='object'){
this.obj=required;
}else {
throw('Interface invalid parameter required = '+required);
}
};
/** check constructor instance
@parameter {object} scope : instance to check.
@parameter {boolean} [strict] : if true -> throw an error if errors ar found.
@constructor
*/
Interface.prototype.check=function(scope,strict){
let err=[],type,res={};
for(let k in this.obj){
type=typeof(scope[k]);
if(type!==this.obj[k]){
err.push({
key:k,
type:this.obj[k],
inputType:type,
msg:type==='undefined'?'missing element':'bad element type "'+type+'"'
});
}
}
res.success=!err.length;
if(err.length){
res.msg='Class bad structure :';
res.errors=err;
if(strict){
let stk = new Error().stack.split('\n');
stk.shift();
throw(['',res.msg,
res.errors.map(e=>'- {'+e.type+'} '+e.key+' : '+e.msg).join('\n'),
'','at :\n\t'+stk.join('\n\t')
].join('\n'));


}
}
return res;
};

使用实例:

// create interface tool
let dataInterface=new Interface(['toData','fromData']);
// abstract constructor
let AbstractData=function(){
dataInterface.check(this,1);// check extended element
};
// extended constructor
let DataXY=function(){
AbstractData.apply(this,[]);
this.xy=[0,0];
};
DataXY.prototype.toData=function(){
return [this.xy[0],this.xy[1]];
};


// should throw an error because 'fromData' is missing
let dx=new DataXY();

与类

class AbstractData{
constructor(){
dataInterface.check(this,1);
}
}
class DataXY extends AbstractData{
constructor(){
super();
this.xy=[0,0];
}
toData(){
return [this.xy[0],this.xy[1]];
}
}

它仍然有点性能完善,需要依赖于接口类,但可以用于调试或开放api。

这是旧的,但我实现了在ES6上使用的接口,没有转译器。

https://github.com/jkutianski/ES6-Interfaces

通过接口,您可以实现一种多态方式。Javascript确实需要接口类型来处理这个和其他interface的东西。为什么?Javascript是一种动态类型语言。以具有相同方法的类数组为例:

Circle()
Square()
Triangle()

如果你想知道多态是如何工作的,David krugman linsky的书MFC是很棒的(为c++编写的)。

在这些类中实现draw()方法,推入数组中这些类的实例,并在迭代数组的循环中调用draw()方法。这是完全正确的。你可以说你隐式地实现了一个abstract class。它不存在于现实中,但在你的脑海中,你做到了,Javascript没有问题。与实际接口的区别在于你来实现所有的接口方法,在这种情况下这是不需要的。

接口是一个契约。您必须实现所有的方法。只有让它是静态的,你才需要这样做。

把Javascript这样的语言从动态变成静态是有问题的。静止是不可取的。有经验的开发人员对Javascript的动态特性没有问题。

所以我不清楚使用Typescript的原因。如果你将NodeJS和Javascript结合使用,你可以建立非常高效和经济的企业网站。Javascript/NodeJS/MongoDB组合已经是伟大的赢家。

不,但它有mixins。 你可以使用抽象子类或mixins作为替代https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#mix-ins

尝试这样做:将接口描述为类,并使用@implements JSDoc来显示给定的类实现了定义的接口。如果它没有实现某些属性,你会在类名上看到红色的弯曲线。我用VSCode进行了测试。

// @ts-check


// describe interface using a class
class PlainInterface {
size = 4;
describe() {}
show(){ }
}


/**
* @implements  PlainInterface
*/
class ConcretePlain {
size = 4;
describe() {
console.log('I am described')
}
show(){
console.log('I am shown')
}
}


const conc = new ConcretePlain();
conc.describe();

Js没有接口,但typescript有!

虽然javaScript中不像Java中那样有接口,但你可以用这条消息下的代码来模仿这种行为。因为接口基本上是一个强制契约,你可以自己构建它。

下面的代码来自3个类:接口、父类和子类。

  • 接口有检查方法和属性是否存在的方法。

  • 父类用于使用Interface类在子类中强制执行所需的方法和属性。

  • Child是父母规则强制执行的类。

在您正确地设置它之后,如果子程序中缺少方法或属性,那么您将在控制台中看到一个错误,如果子程序正确地实现了契约,则什么也没有。

class Interface {
checkRequiredMethods(methodNames) {
setTimeout( () => {
const loopLength = methodNames.length;
let i = 0
for (i; i<loopLength; i++) {
if (typeof this[methodNames[i]] === "undefined") {
this.throwMissingMethod(methodNames[i]);
}


else if (typeof this[methodNames[i]] !== "function") {
this.throwNotAMethod(methodNames[i]);
}
}
}, 0);
}


checkRequiredProperties(propNames) {
setTimeout( () => {
const loopLength = propNames.length;
let i = 0
for (i; i<loopLength; i++) {
if (typeof this[propNames[i]] === "undefined") {
this.throwMissingProperty(propNames[i]);
}


else if (typeof this[propNames[i]] === "function") {
this.throwPropertyIsMethod(propNames[i]);
}
}
}, 0);
}


throwMissingMethod(methodName) {
throw new Error(`error method ${methodName} is undefined`);
}


throwNotAMethod(methodName) {
throw new Error(`error method ${methodName} is not a method`);
}


throwMissingProperty(propName) {
throw new Error(`error property ${propName} is not defined`);
}


throwPropertyIsMethod(propName) {
throw new Error(`error property ${propName} is a method`);
}
}
class Parent extends Interface {
constructor() {
super()


this.checkRequiredProperties([
"p1",
"p2",
"p3",
"p4",
"p5"
]);


this.checkRequiredMethods([
"m1",
"m2",
"m3",
"m4"
]);
}
}
class Child extends Parent {
p1 = 0;
p2 = "";
p3 = false;
p4 = [];
p5 = {};


constructor() {
super();
}


m1() {}
m2() {}
m3() {}
m4() {}
}
new Child()