如何为JavaScript选择面向对象设计模式

6
通过OO,我指传统的面向对象编程。在定义我的“类”(JavaScript没有传统的类)时,我一直在使用模块模式提供隐私和使用对象字面量创建“公共静态集合”之间来回切换。
在创建“类”时,我没有指导性的力量让我确定要使用什么类型的组织方式。当然,我的代码通过了jshint和jslint的检查,没有设置选项。
我正在处理大约1500行代码,因此在代码变得难以管理并且必须放弃之前,我需要一个“指导性力量”。
我很清楚JavaScript中编写“类”的不同方法。Alex MacCaw撰写的《JavaScript Web Applications》中教授的方法以及SO中列出的众多方法。
但是,在应用程序方面,我不知道什么方法要使用。
最简单的方式似乎是将方法和变量的集合放在一个对象字面量中,如下所示:
var public_statics = {
    public_func: function () {},
    public_var: "hello"
}

其中最复杂的似乎是 - IIFE。

(function(){
    var private_var;
    function private_func(){
    }
})();

如何判断在众多中间变体中使用哪一个?

以控制器在MVC中为例,该如何选择:

目前(有点随意地选取),我是这样实现控制器的:

var Co = {};
Co.Controller = function(){
    // 'classes' from Mo are called here
    // 'classes' from Su are called here
}

然后我为Co.添加其他与控制相关的方法。

我该如何选择使用哪种OO风格?


更新

我的库目前分为4个命名空间:

var Mo = {},
    Vi = {},
    Co = {},
    Su = {};

模型(Model)、视图(View)和控制器(Controller)应该是不言自明的,而支持类((Su)pport)则适用于所有未包含在MVC中的“类”,例如DOM访问、特效、调试代码等。

我应该使用什么面向对象风格来进一步组织这个库/代码呢?

控制器“类”的示例:

/**
 **  Controller
 */

Co.Controller = function (o_p) {
    var o_p_string_send;
    Su.time();
    o_p = Mo[o_p.model].pre(o_p);
    if (o_p.result !== 'complete') {
        o_p_string_send = JSON.stringify(o_p);
        Su.time();
        //Su.log(o_p_string_send);
        Co.serverCall('pipe=' + o_p_string_send, function (o_p_string_receive) {
            Su.time();
            //Su.log(o_p_string_receive);
            o_p.server = JSON.parse(o_p_string_receive);
            Mo[o_p.model].post(o_p);
            Su.time(true);
            Su.log('Server time: [' + o_p.server.time + ']');
        });
    }
};

我没有你那么高的SO分数,让我担心我的观点可能是无知的,但难道面向对象设计的主要论点不是为了代码重用吗?也许对于单一使用但复杂的应用程序来说,可维护性也有一些帮助。在JavaScript中,你有选择,所以答案可能更加复杂。 - Jason Sperske
2
一切都取决于你想要实现什么。你是使用类以便从继承中受益吗? - Paul
你是否在代码中使用过 "new" 关键字,还是所有对象都像你的示例一样从普通对象构建? - Niko
你可以查看John Resig在这里所做的关于实例化和可继承Javascript类的工作:http://ejohn.org/blog/simple-javascript-inheritance/ 和 http://ejohn.org/blog/simple-class-instantiation/。 - devstruck
@vector - 看起来也很学术...我的意思是一些 JavaScript 专家(Resig、MacCaw、Crockford)以这种方式使用它。在 Google 上进行了验证。我认为问题在于,它有很多不同的实现方式...不像直接支持经典面向对象编程的语言,那里的方法更加明确定义。 - user656925
显示剩余2条评论
3个回答

2
IFFE经常很难阅读,我个人不知道为什么它们变得如此流行。我认为代码应该易于阅读和简洁。试图模拟不属于语言规范的语言行为通常是一个非常愚蠢的想法。
例如,JavaScript不支持多重继承、多态或许多其他有趣的范例。因此,我们经常看到人们尝试在JS中创建这些疯狂的方式,以某种程度上具有多态性或私有成员等。我认为这是一个错误。
我目前正在作为一种业余项目开发高性能的JS数据结构库(我试图超越Google的closure和一堆其他库)。来自C++和Java背景,我总是喜欢制造类和我喜欢继承等等。让我与您分享一些代码片段。起初,我认为我很聪明,因为我写了这样的东西:
function __namespace(n, v) {
    return {"meta":{"namespace":n,"version":v}};
}

var FJSL = FJSL == undefined ? new __namespace("Fast JavaScript Library", 0.1) : FJSL;

__using = function(parent, child) {
    clazz = new child();
    clazz.super = new parent();
    if (clazz.super == undefined) return clazz;
    for (a in clazz.super) {
        for (b in clazz) {
            if (a == "constructor" || b == "constructor") continue;
            if (clazz[b] === clazz.super[a]) continue;
            if (a == b && typeof clazz[b] != typeof clazz.super[a]) throw "Typesafety breached on '" + a + "' while trying to resolve polymorphic properties."; 
            if (a == b && typeof clazz[b] == typeof clazz.super[a]) {
                clazz["_"+a] = clazz.super[a];
            } else if (clazz[a] == undefined) {
                clazz[a] = clazz.super[a];
            }
        }
    }
    return clazz;
};

我正在使用它,就像这样(以简单队列的示例为例):

FJSL.Array = function() { 
    this.data = [];

    this.contains = function(idx, element) {
        for (var i = idx; i < this.data.length; i++) {
            if (this.data[i] === element)
                return i;
        }
        return -1;
    }

    this.size = function() {
        return this.data.length;
    }
}

FJSL.Queue = function() {
    return __using(FJSL.Array, 
    function() {
        this.head = 0;
        this.tail = 0;

        this.enqueue = function(element) {
            this.data[this.tail++] = element;
        };

        this.dequeue = function() {
            if (this.tail == this.head)
                return undefined;
            return this.data[this.head++];
        };

        this.peek = function() {
            return this.data[this.head];
        };

        this.size = function() {
            return this.tail - this.head;
        };

        this.contains = function(element) {
            return this._contains(this.head, element);
        };
    }
)};

你会注意到我在假装继承(队列使用数组,哈哈,我真聪明)。然而,这对于阅读和理解来说绝对是疯狂的。我不禁想起了这个梗图:
让我展示给你一个功能等价的代码,没有我尝试做所有这些花哨的前后处理:
FJSL.Queue = function(opts) {
    this.options = opts;
    this.head = 0;
    this.tail = 0;
    this.data = [];
};

FJSL.Queue.prototype = {
    add : function(element) {
        this.data[this.tail++] = element;
    },

    enqueue : function(element) {
        this.data[this.tail++] = element;
    },

    dequeue : function() {
        if (this.tail == this.head) {
            return undefined;
        }
        return this.data[this.head++];
    },

    peek : function() {
        return this.data[this.head];
    },

    size : function() {
        return this.tail - this.head;
    },

    contains : function(element) {
        // XXX: for some reason a for : loop doesn't get JIT'ed in Chrome
        for (var i = this.head; i < this.data.length; i++) {
            if (this.data[i] === element) {
                return true;
            }
        }
        return false;
    },

    isEmpty : function() {
        if (size) {
            return true;
        }
        return false
    }, 

    clear : function() {
        this.data = [];
    }
};

显然,我必须为可能使用数组的任何其他结构复制原型构造函数,但我试图实现的目标非常清晰,即使是新手JS程序员也可以看出发生了什么。不仅如此,如果人们想修改代码,他们知道去哪里以及该做什么。
我的建议是不要陷入试图使JS表现得像C++或Java的疯狂状态中。它永远不会那样。是的,你可以伪造继承和私有/公共/受保护成员,但JS从未打算这样做。我认为拥有这种臃肿代码(试图模拟非标准行为)的后果对高性能Web应用程序及其类似物非常具有负担。
简而言之,我建议使用对象字面量:
var public_statics = {
    public_func: function () {},
    public_var: "hello"
}

易于理解、易于修改和易于扩展。如果您的系统足够脆弱,以至于如果有人不小心更改了某个“私有”变量就会崩溃,那么您只需要记录它。


那么对于中间部分呢,比如构造函数...var constructor_function = function(){ // private stuff },这为隐私和实例提供了自然语言结构,可以通过new constructor_function来使用。 - user656925
@HiroProtagonist:我认为这是一个非常合适的方法。构造函数是语言的一部分,即使它们可能有点难以理解,但确实提供了稍微面向对象的对象方法。然而,重型构造函数可能会对性能产生负面影响,特别是在(如果)尝试模拟继承等情况下。 - David Titarenco
似乎很奇怪,没有一种将原型与其相关对象组合在一起的方法……例如,var InstanceBased = function(){}; InstanceBased.prototype = {}; 这似乎是语言开始从经典面向对象语法和功能分离的地方,因为它们默认为公共而不是私有。 - user656925
@HiroProtagonist:Douglas Crockford在他的书中所讲的是JS的一个子集(其中他有一些额外的语法限制),请参见此问题以获取更多详细信息:https://dev59.com/oVzUa4cB1Zd3GeqP0zR2 - David Titarenco

1

我个人更喜欢IIFE,因为你可以将方法设为私有。否则,你就必须使用下划线来进行某种奇怪的约定。

此外,从风格上讲,如果你将其封装在一个函数中,你就有了选择,在普通的javascript中可以加分号。要求对象字面量中的每一行都以逗号结尾对我来说似乎很有趣。


在超过1500行的代码中,我可以选择为所有“类”使用IIFE吗?我需要担心立即执行会增加处理负担吗? - user656925
我相信如此。作为个人选择,我像在Java中一样将我的类写在单独的文件中。我使用像node.js的grunt这样的工具将文件合并/压缩以进行分发。稍微离题一下,你可能希望看看require.js,它封装了你的模块,并使它们同时适用于浏览器和节点。如果你的目标是接近熟悉的Java开发环境,它可能会让你更接近一些。 - badunk

0

由于JavaScript语言本身有许多不同的实现方式,即每个浏览器都不同,因此您必须从不一致性开始。

JavaScript不直接支持经典继承(它支持原型继承)。

对于我的需求,我将选择不实现经典继承。这并不理想,因为经典继承允许您直接实现最佳实践OO-封装、继承和多态性。

然而,我更喜欢等待语言直接支持通过经典继承或类似方式的面向对象编程。

在此期间,我将仅使用基本的原型继承来帮助实现代码重用。


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