JavaScript中“抽象”函数的最佳实践是什么?

23

我刚写了一些 JavaScript 代码,遵循我认为的使用闭包和一些函数创建对象的良好实践:

var myStuff = (function() {
 var number = 0;
 var fn = {};
 fn.increment = function() { number++; };
 fn.decrement = function() { number--; };
 fn.getNumber = function() { return number; };
 return fn;
})();

myStuff.increment();
myStuff.increment();
alert(myStuff.getNumber()); // alerts '2'

我没有问题写出类似于之前片段的代码。 我想编写一些具有类似于 OOP“抽象”类的功能的代码。 这是我的努力结果:

var myStuff = (function () {
var number = 0;
var fn = {};
fn.increment = function () { number++; };
fn.decrement = function () { number--; };
fn.doSomethingCrazy = function () { throw new Error('not implemented'); }; // I want to specify later what this does.
fn.doSomethingCrazyTwice = function () { fn.doSomethingCrazy(); fn.doSomethingCrazy(); };
fn.getNumber = function () { return number; };
return fn;
})();

myStuff.doSomethingCrazy = function () { this.increment(); this.increment(); };
myStuff.doSomethingCrazyTwice();
alert(myStuff.getNumber()); // alerts '4'

上面的代码片段可以工作,但看起来不太优雅。也许我试图强迫 JavaScript(一种函数式语言)去做一些它并没有被设计去做的事情(对象继承)。

有没有一种好的方法在 JavaScript 中定义一个对象,以便稍后可以定义该对象的函数?


JavaScript支持面向对象、命令式以及函数式编程(或者说,脚本语言)。 - BoltClock
5个回答

26

只是不要定义函数。

Javascript 是一种鸭子类型语言。如果它看起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子。
你无需做任何特殊处理,只要在调用时函数存在,它就能正常工作。

如果在实例上调用没有这个函数的方法,你将在调用处得到一个错误。


正如您所看到的,我特意从同一对象中的另一个函数调用了我的“抽象”函数。 - Vivian River
3
@Rice:这并没有什么区别,这就是“鸭子类型”的意思。 - SLaks

18

我同意SLaks的观点,没有必要定义这个函数,但我还是倾向于这么做。因为对我来说,重要的部分在文档中。当有人阅读我的类时,我希望清楚地表明您必须实现哪些方法,将传递什么参数以及应返回什么内容。

这段话来自工作中的一个文件。其中有多个实现特性的代码,都是基于一个在时间间隔内进行数据加载的基类。

/**
 * Called when data is received and should update the data buffer
 * for each of the charts 
 * 
 * @abstract
 * @param {cci.ads.Wave[]} waves
 * @void
 */
updateChartsData: function(waves){
    throw "Abstract method updateChartsData not implemented";
},

2019年更新

如果可以的话,请使用TypeScript 在TypeScript中声明抽象方法


1
@Raynos:它的哪一部分是反模式?能否提供一些理由? - Ruan Mendes
2
@Raynos:仅仅因为一种语言没有这个概念,并不意味着它没有用处。拥有一个基本的空函数可能并不理想,因为它...然而,我觉得不可接受的是在抽象类中没有记录方法如何被调用,这就是我不喜欢“只是不定义一个函数”的原因。如果您选择使所有的abstract方法都没有下划线,那很好,我们的约定是使用下划线,因为这些方法不应该从类外部调用,只需保持一致即可。 - Ruan Mendes
1
@Raynos:在基类上使用抽象方法是实现依赖反转原则的标准方式。http://www.oodesign.com/dependency-inversion-principle.html。我的例子是一个用例,用于不同对象实现的情况,这样子类就不必担心其他细节,他们只需要知道在需要时会调用某个方法即可。 - Ruan Mendes
@Raynos:您介意解释一下您如何实现这个用例吗:我在JavaScript中提供了一个页面对象,我的其他开发人员继承/扩展它(无论您在JS中如何称呼它)。 我需要他们实现一个initialLoad()方法,否则超级方法将失败。 将inialLoad()设为抽象方法可以强制开发人员正确实现我的页面对象。 - Oliver Watkins
@OliverWatkins var myPage = Page(function initialLoad() { ... })@OliverWatkins var myPage = Page(function initialLoad() { ... }) - Raynos
显示剩余3条评论

7
随着我们团队的扩大和javascript项目变得更加复杂,我们不得不开始实现面向对象的特性。
在我们的javascript '抽象方法'中,我们只需抛出一个错误或弹出一个警告。这是来自我们页面对象的一个例子:
Page.initialLoad = function() { //abstract
    alert('Page.initialLoad not implemented');
};

在Java世界中,它类似于:
public void abstract initialLoad();

Java代码会在编译时出现错误,但Javascript中会出现运行时错误(即弹出脏错误对话框,指示实现对象尚未实现该方法)。
我们有很多不同的团队使用页面对象;“鸭子类型”哲学绝对不能满足我们的需求。如果没有这些类似伪“抽象”的方法,我们就会出现API通信的普遍缺乏,有时还会出现超级对象的破坏(例如,因为用户不知道他们应该去实现该方法)。
我厌倦了这种“鸭子类型”的理念。我不确定支持者是否曾经参与过由10多个开发人员构建的复杂Javascript项目。

3
如果您觉得代码不够优雅,可能有一些方法可以创建一些函数来简化流程,使其看起来更好。但回到主题...
是的,Javascript具有内置的委托,也就是通过原型继承。
给定一个原型对象:
var proto = {
    f: function(){ console.log(this.x); }
}

我们可以创建一个从它继承的新对象:
var obj = Object.create(proto);
obj.x = 42;

obj.f(); //should work!

for(var i in obj) console.log(i);
//should print x, f and some other stuff perhaps

请注意,直接通过Object.create进行操作并不总是被支持的(旧版浏览器等)。旧的(有些人可能会说是正常的)做法是通过奇怪的new运算符来完成操作(不要对名称过于纠结——它是故意让Java人分心的)。

function Constructor(arg){
    this.x = arg;
}

Constructor.prototype = {
    f: function(){ ... }
};

var obj = new Constructor(17);
obj.f();

在考虑原型继承时需要注意一个重要的区别,那就是缺乏私有变量。只有公共变量可以被继承!因此,一种常见的惯例是使用下划线作为私有和受保护变量的前缀。


1
JavaScript 中的 Everything 都是为了分散 Java 开发者的注意力:从语言名称一直到二进制运算符和 'new' 关键字! :-) - Vivian River
Object.create 的第二个参数应该是 propertyDescriptor。 - Raynos
@Raynos:修复了脑抽问题;我想起了旧的Crockford Object.create,哈哈 - hugomg
@missingno 你可能想看看pd,因为你选择了.x = 42而不是{ x: { value: 42 } } - Raynos

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接