本文深入解析 JavaScript 中接口的三种模拟实现方式:注释描述、属性验证及鸭子类型。通过对比分析,重点推荐基于 Interface 类的鸭子类型方案,并阐述在大型团队协作与复杂系统设计中引入接口的必要性,最后给出从定义到验证的完整使用建议。
JavaScript 接口概述
本书开篇重点阐述了接口的概念。
接口通常被视为一种契约或规范。在强类型的面向对象语言中,接口的实现相对直接;然而,JavaScript 原生并未提供创建或验证接口的机制,也无法直接判断类型是否实现了特定接口。因此,我们需要利用 JavaScript 的灵活性来模拟接口的行为。
接口实现的三种方式
在 JavaScript 中,实现接口主要有三种途径:注释描述、属性验证以及鸭子类型。
注:本文基于英文原著翻译,部分术语可能因语言差异存在理解偏差,请读者重点把握核心思想。
注释描述法
1. 注释描述(Describing Interfaces with Comments)
示例如下:
代码实现如下:
/*
interface Composite {
function add(child);
function remove(child);
function getChild(index);
}
interface FormItem {
function save();
}
*/
var CompositeForm = function(id, method, action) { // implements Composite, FormItem
...
};
//Implement the Composite interface.
CompositeForm.prototype.add = function(child) {
...
};
CompositeForm.prototype.remove = function(child) {
...
};
CompositeForm.prototype.getChild = function(index) {
...
};
// Implement the FormItem interface.
CompositeForm.prototype.save = function() {
...
}; 这种模拟方式借用了其他面向对象语言中的 interface 和 implements 关键字,但必须将它们置于注释中,以避免语法错误。
其目的仅在于向其他开发者提示该类需实现的方法,起到文档化的作用。然而,这种方式缺乏实际的验证机制,无法确保类正确实现了接口定义的方法,因此仅作为一种文档约定。
属性验证法
2. 属性验证(Emulating Interfaces with Attribute Checking)
示例如下:
代码实现如下:
/* interface
Composite {
function add(child);
function remove(child);
function getChild(index);
}
interface FormItem {
function save();
}
*/
var CompositeForm = function(id, method, action) {
this.implementsInterfaces = ['Composite', 'FormItem'];
...
};
...
function addForm(formInstance) {
if(!implements(formInstance, 'Composite', 'FormItem')) {
throw new Error("Object does not implement a required interface.");
}
...
}
// The implements function, which checks to see if an object declares that it
// implements the required interfaces.
function implements(object) {
for(var i = 1; i < arguments.length; i++) {
// Looping through all arguments
// after the first one.
var interfaceName = arguments[i];
var interfaceFound = false;
for(var j = 0; j < object.implementsInterfaces.length; j++) {
if(object.implementsInterfaces[j] == interfaceName) {
interfaceFound = true;
break;
}
}
if(!interfaceFound) {
return false;
// An interface was not found.
}
}
return true;
// All interfaces were found.
} 相较于第一种方式,此方法有所改进。接口定义仍通过注释进行,但增加了验证逻辑,用于判断对象是否声明实现了特定接口。
鸭子类型法
3. 鸭子类型(Emulating Interfaces with Duck Typing)
代码实现如下:
// Interfaces.
var Composite = new Interface('Composite', ['add', 'remove', 'getChild']);
var FormItem = new Interface('FormItem', ['save']);
// CompositeForm class
var CompositeForm = function(id, method, action) {
...
};
...
function addForm(formInstance) {
ensureImplements(formInstance, Composite, FormItem);
// This function will throw an error if a required method is not implemented.
...
}
// Constructor.
var Interface = function(name, methods) {
if(arguments.length != 2) {
throw new Error("Interface constructor called with "
+ arguments.length + "arguments, but expected exactly 2.");
}
this.name = name;
this.methods = [];
for(var i = 0, len = methods.length; i < len; i++) {
if(typeof methods[i] !== 'string') {
throw new Error("Interface constructor expects method names to be "
+ "passed in as a string.");
}
this.methods.push(methods[i]);
}
};
// Static class method.
Interface.ensureImplements = function(object) {
if(arguments.length < 2) {
throw new Error("Function Interface.ensureImplements called with "
+arguments.length + "arguments, but expected at least 2.");
}
for(var i = 1, len = arguments.length; i < len; i++) {
var interface = arguments[i];
if(interface.constructor !== Interface) {
throw new Error("Function Interface.ensureImplements expects arguments"
+ "two and above to be instances of Interface.");
}
for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
var method = interface.methods[j];
if(!object[method] || typeof object[method] !== 'function') {
throw new Error("Function Interface.ensureImplements: object "
+ "does not implement the " + interface.name + " interface. Method " + method + " was not found.");
}
}
}
}; 何时使用接口?
接口的应用场景与使用建议
严格类型验证并非适用于所有 JavaScript 场景,因为许多开发者已习惯在无接口验证的环境下编程。然而,在设计复杂系统时,使用接口能带来显著益处。虽然接口看似限制了灵活性,实则通过解耦代码提升了系统的健壮性。它允许传递任意类型的变量,只要该变量包含所需方法即可,从而在多种场景下保持代码的灵活性。
在大型系统中,多名开发者共同参与项目时,接口显得尤为重要。开发者常需访问尚未实现的 API,或为他人提供依赖方法的存根。此时,接口作为文档和契约具有极高价值。当存根被实际 API 替换时,接口能确保无缝切换;若开发过程中 API 发生变更,只要新实现符合接口定义,即可平滑替换。
如何正确使用接口?
首要问题是判断代码是否适合使用接口。对于小型项目,引入接口可能增加不必要的复杂度,需权衡利弊。若确定使用接口,建议遵循以下步骤:
1. 在页面文件中引用 Interface 类。Interface 的源文件可在 http://jsdesignpatterns.com/ 获取。
2. 审查代码,确定哪些方法需要抽象为接口。
3. 创建接口对象,每个接口对象包含一组相关方法。
4. 移除所有构造函数中的验证逻辑,转而采用第三种实现方式,即鸭子类型。
5. 使用 Interface.ensureImplements 替代原有的构造函数验证。