前言

javaScript中并没有内置的类似java,C++的创建或实现接口的方法。也没有内置的方法可以用于判断一个对象是否实现了与另一个对象相同的方法,这使得对象很难互换使用。好在javaScrip有出色的灵活性,能让我我们模仿这些特性。

我们按照一下的顺序来逐步认识接口:

1.什么是接口?

2.javaScript模仿接口的三种方法。

3.接口的利弊

4.一个示例



什么是接口

接口提供了一种用以说明一个对象应该具有那些方法的手段,但并不规定这些方法应该如何实现。

好比一个厨师(chef),会三种菜 foodA,foodB,foodC, 我们很容易从语义上理解,这个厨师可以完成三种菜,但cooker并没有实现这三种菜的方法。

从代码上看类似于:

var chef= ['foodA','foodB','foodC'];

而不是:

var chef= {
  foodA:function(){
    //foodA实现方法
  },
  foodB:function(){
    //foodB实现方法
  },
  foodC:function(){
    //foodC实现方法
  }
}

这样我们可以利用接口这个工具,按照对象提供的特性对他们进行分组。

例如:一些对象存在着很大的差异,但是他们都实现了setName这个方法,就可以互换使用这些对象

var baseName = {
 name:'bird',
  someAttr:function(fn){
    this.name = fn;
  }
}
//这三个对象的差别很大,但都可以实现setName的方法
objA = {
  a:function(){},
  b:function(){},
  setName:function(newName){
    return newName;
  }
}
objB = {
  c:function(){},
  d:function(){},
  setName:function(newName){
    return newName;
  }
}
objC = {
  e:function(){},
  f:function(){},
  setName:function(newName){
    return newName;
  }
}
//可以互换使用这些对象,完成相同的效果
baseName.someAttr(objA.setName('dog'));
console.log(baseName.name);//dog
baseName.someAttr(objB.setName('dog'));
console.log(baseName.name);//dog
baseName.someAttr(objC.setName('dog'));
console.log(baseName.name);//dog

还可以使用接口来开发不同类之间的共同性。如果把原本要求以一个特定类为参数的函数改为要求以一个特定的接口为参数的函数,那么任何实现了该接口的对象都可以作为参数传递给它。如果理解了上面的例子,这个应该很容易理解。



模仿接口的三种方法



注释法

//    通过注释的方法描述接口
//    这种方法模仿其他面向对象语言中的做法,
//    使用interface(接口) 和 implements(实现) 关键字
//    但把他们放在注释中,以免引起语法错误

/*
 Interface ChefOne{
  function foodA(){}
  function foodB(){}
  function foodC(){}
}
Interface ChefTwo {
  function foodD(){}
  function foodE(){}
  function foodF(){}
}*/

//implement the ChefOne Interface  实现ChefOne接口
var ChefOne = function() {};
CookerOne.prototype.foodA = function() {};
CookerOne.prototype.foodB = function() {};
CookerOne.prototype.foodC = function() {};
//implement the ChefTwo Interface  实现ChefTwo接口
var ChefOne = function() {};
CookerOne.prototype.foodD = function() {};
CookerOne.prototype.foodE = function() {};
CookerOne.prototype.foodF = function() {};

注释描述接口,并没有去检查是否真的实现了接口,对接口的约定靠的都是认为的把控。虽然这不是一个好的方法,但是它易于实现,不需要额外的类和函数。不会影响文件的大小和执行速度。但是因为没有报错机制,对测试和调试没有帮助。



属性检查法

所有的类都明确的声明了自己实现哪些接口,可以针对这些声明进行检查。

接口自身仍然可以注释。

//有一个类TopChef  它自称实现了两个接口 chefOne chefTwo
var TopChef = function() {
  this.implementInterfaces = ['chefOne', 'chefTwo'];
  this.foodA = function() {};
  this.foodC = function() {};
}
//检查是否实现接口
function ensureImplements(obj) {
  for(var i = 1; i < arguments.length; i++) {
    var interfaceName = arguments[i];
    var interfaceFound = false;
    for(var j = 0; j < obj.implementInterfaces.length; j++) {
      if(obj.implementInterfaces[j] === interfaceName) {
        interfaceFound = true;
        break;
      }
    }
    if(!interfaceFound) {
      return false
    }
  }
  return true;
}
var topChef = new TopChef();

if(ensureImplements(topChef, 'chefOne', 'chefTwo')) {
  console.log('All interfaces were implement'); //所有的接口已经实现
} else {
  console.log('An interface was not implement'); //没有实现所有的接口
}

这种方法并没有确保类真正的实现了自己声称的接口,如果上面代码中 chefOne 有方法 foodA,chefTwo有方法foodC, 但是后期修改了TopChef的所实现的方法后,比如添加或者删除一个属性,仍然能通过检测,但是在接口中并不存在这个方法。很容易埋下隐患,这种错误也很难排插。



鸭式辨型法

这个名称来自James Whitcomb Riley 的名言:‘’像鸭子一样走路并且嘎嘎叫的就是鸭子‘’。

因此,可以理解为,如果对象具有与接口定义的方法同名的所有方法,就可以认为实现了这个接口。

//一个辅助函数构造接口,传入接口名称和属性
function Interface(interfaceName, mothodArr) {
  if(arguments.length < 2) {
    throw new Error('必须传入接口名称,接口方法两个参数');
  }
  this.name = interfaceName; //接口的名称
  this.mothodArr = []; //接口方法的数组
  for(var i = 0; i < mothodArr.length; i++) {
    if(typeof mothodArr[i] !== "string") {
      throw new Error("接口方法(" + mothodArr[i] + ")参数类型必须为字符串");
    }
    this.mothodArr.push(mothodArr[i]);
  }
}
//在interface上扩展接口检测的方法
Interface.ensureImplements = function(obj) {
  if(arguments.length < 2) {
    throw new Error("至少传入两个参数");
  }
  //从第二个参数循环,即为定义的接口
  for(var i = 1; i < arguments.length; i++) {
    var inter = arguments[i]; //拿到其中的一个接口
    //判断接口的构造函数是否正确
    if(inter.constructor !== Interface) {
      throw new Error(inter + "的构造函数必须是Interface");
    }
    //循环接口中定义的方法,与实现接口的对象中的方法做比较
    for(var j = 0; j < inter.mothodArr.length; j++) {
      var mothod = inter.mothodArr[j];
      //如果 方法不存在,或者不是一个函数 即终止程序
      if(!obj[mothod] || typeof obj[mothod] !== "function") {
        throw new Error("接口" + inter.name + "没有实现" + mothod + "方法");
      }
    }
  }
  console.log("所有接口的所有方法已经实现")
}
//定义两个接口
var gimMan = new Interface('gimMan', ['add', 'max', 'min']);
var givMan = new Interface('givMan', ['set']);

function CommMan() {
  this.add = function() {
    console.log(1)
  };
  this.max = function() {
    console.log(2)
  };
  this.min = function() {
    console.log(3)
  };
  //
  this.set = function() {
    console.log(4)
  };
  Interface.ensureImplements(this, gimMan, givMan);
}
var c = new CommMan(); //所有接口的所有方法已经实现

和以上两种方式不同,这种方法不需要注释,而且检测过程的大部分是可以强制的。如果漏掉了任何一个方法都会报错,而且会抛出相对有用的错误信息。

这种方法中,并不声明自己实现了哪些接口,因此没有自我描述性。它需要的是一个辅助类,和一个辅助函数。但这种方法是最常用,也是最完善的一种。



接口的利弊



利:

既定的一批接口具有自我描述性,并能促进代码的重用。

如果熟悉一个接口,就知道了所有实现它的类,从而有可能重用现有的类。

接口能让代码百年的更稳固。如果接口添加了一个操作,但是类中并没有实现它,很显然会得到一个错误。



弊:

接口会对性能造成一定影响。但是在项目生产环境中,可以去掉接口这部分代码。

javaScript不像其他语言中有接口的概念,无法根除是否实现着接口这个问题,还是需要大家的相互遵守。

 



示例

//有一个类它有转换或者处理字符串的方法
//参数gstr 是getString 方法的一个实例
//我们最基本的实现方法如下
var stringConvert = function(gstr){
  if(!(gstr instanceof getString)){
    throw new Error(gstr+'不是getString的实例');
  }
  this.gstr = gstr;
}
stringConvert.prototype.A = function(someString){
  return this.gstr.mothed1(someString);
}
stringConvert.prototype.B = function(someString){
  return this.gstr.mothed2(someString);
}
//这种写法会对gstr参数进行检查,但是不能保证mothed1 ,mothed2两个方法都已经实现
//而且如果有另一个类 getString2 更好的实现了这两种方法,也会因为instance of 检测不能使用 
//因此我们可以使用接口来代替instance of 像下面这样更好的实现

//用到上面的鸭式辨型法
//首先定义接口类
var getStr = new Interface('getStr',['mothed1','mothed2']);
var stringConvert = function(gstr){
  Interface.ensureImplements(gstr,getStr);
  this.gstr = gstr;
}
stringConvert.prototype.A = function(someString){
  return this.gstr.mothed1(someString);
}
stringConvert.prototype.B = function(someString){
  return this.gstr.mothed2(someString);
}