在JavaScript中选择一个面向对象编程的模式

8
我已经在其他人和几个资源的帮助下整理了以下内容。我制作了一个完整的代码片段,并将简化后的代码发布如下。
基本上,我已经学会了如何使用这些模式,但我很好奇它们之间更基本的区别。任何这些模式都可以生成相同的下游代码,但是否有理由使用一个而不是另一个,除了个人喜好之外?如果您有更好的模式,请告诉我。
模式1(基于对象):
var mouseDiff = {
    "startPoint" : {"x" :0, "y" : 0},
    "hypotenuse" : function(a,b) {
        // do something
    },
    "init"       : function(){
        // do something
    }
}

mouseDiff.init();

模式2(据我所知最传统的模式):

function MouseDiff() {
    this.startPoint = {"x" :0, "y" : 0};
}

MouseDiff.prototype.hypotenuse = function(a,b) {
    // do something
}

MouseDiff.prototype.init = function() {
    // do something
}

var myMouse = new MouseDiff;
myMouse.init();

模式3(使用闭包):

var MouseDiff2 = (function() {
    var startPoint = {"x" :0, "y" : 0};
    var hypotenuse = function(a,b) {
        // do something
    };
    return {
        hypotenuse: hypotenuse,
        init : function(){
            // do something
        }
    };

}());
MouseDiff2.init();

2
模式4:Object.create。这是原型面向对象编程的实现方式。 - Raynos
2
进一步解释@Raynos的评论,Object.create通常与Pattern 1一起使用。调用var mouseDiff2 = Object.create(mouseDiff)会给你一个新的、独立的对象,并在mouseDiff2中存储一个引用。然后,您可以按照自己的意愿使用它,而不会影响原始对象。 - Ryan Kinal
1
@RyanKinal 这并不完全正确。您可以编辑原始对象。还要注意的是,mouseDiff被放置在mouseDiff2的原型链中。 - Raynos
@Raynos 哦,哇。是的,那是我的错。今天有点头脑不清。Object.create会给你一个新对象,通过原型链继承自旧对象。 - Ryan Kinal
澄清一下。var o1 = { "foo": { "bar": "baz" } }; var o2 = Object.create(o1); o2.foo.bar = 42; console.log(o1.foo.bar); 是“克隆”对象破坏原始状态的一个例子。 - Raynos
6个回答

12

模式1是单例模式。如果您只需要一个这样的对象,那就很好。

模式2构建新对象,并利用prototype对象,以便创建新的MouseDiff对象时,它不会创建函数的新副本(这些函数本身是JavaScript中的数据)。

模式3相对于普通单例需要更多的内存,但它提供了静态隐私。

我喜欢以下模式,因为它涵盖了各种功能,虽然它实际上是构造函数(模式2)和闭包(模式3)的组合:

var MouseDiff = (function () {

    var aStaticVariable = 'Woohoo!';
    // And if you really need 100% truly private instance
    // variables which are not methods and which can be
    // shared between methods (and don't mind the rather
    // big hassle they require), see
    // http://brettz9.blogspot.com/search?q=relator
    // (see also the new plans for a Map/WeakMap in ECMAScript)

    function _APrivateStaticMethod () {
        alert(aStaticVariable);
    }

    // An instance method meant to be called on the
    //   particular object as via ".call(this)" below
    function _APrivateInstanceMethod () {
        alert(this.startPoint.x);
    }

    // Begin Constructor
    function MouseDiff() {
        this.startPoint = {"x" :0, "y" : 0};
    }

    MouseDiff.prototype.hypotenuse = function(a,b) {
        // do something
    };

    MouseDiff.prototype.init = function() {
        // do something
        _APrivateStaticMethod(); // alerts 'Woohoo!'
        _APrivateInstanceMethod.call(this); // alerts 0 (but if not
        // called with this, _APrivateInstanceMethod's internal
        // "this" will refer (potentially dangerously) to the
        // global object, as in the window in the browser unless
        // this class was defined within 'strict' mode in which
        // case "this" would be undefined)
    };

    return MouseDiff;
}());

var myMouse = new MouseDiff;
myMouse.init();

“Pedantry”认为在像JavaScript这样的语言中使用“singleton”是一个愚蠢的术语,因为这个概念与类紧密相连,而JS中并不存在类。 - Raynos
当然。但实际上,由于我们通过将事物与我们所知道的联系起来学习,我认为以这种方式描述它仍然有帮助,并且将new关键字使函数可用作“类”。 - Brett Zamir
单例模式仍然是错误的术语。它是一种只有一个与特定接口匹配的对象的方式(由C#/Java中的类定义)。在JS中,您无法这样做,因为您可以创建另一个对象来打破单例概念。我欣赏使用常见术语的帮助,但在这种情况下,“Singleton”被滥用了(不用担心,每个人都这样做)。 - Raynos
实际上有什么区别呢?可以这样想:每个使用的对象字面量都会生成一个匿名类,并遵循其自己的接口。因此,“其他对象”之后创建的概念上只是创建了不同的类和实例。 - Brett Zamir
我认为在概念接口方面,可以采用鸭子类型。如果它具有.quack.swim,那么它就是一只“鸭子”。因此,两者都实现了相同的概念接口(即您的文档中的接口)。事实上,可能存在两个“接口”对象的事实是无关紧要的。就像我说的那样,这是纠缠细节。 - Raynos

1

我对JavaScript的了解不足以告诉你这些方法之间是否存在性能差异。以下是我注意到的两个区别,我相信还有其他的。

模式1创建一个具有这些属性的对象,包括附加的方法。模式2允许我们轻松地创建许多具有相同方法的对象,而无需重写它们。

模式3就像一个工厂。工厂不依赖原型自动附加这些方法,而是新创建并返回对象。使用闭包可以隐藏对象的“成员变量”。除了通过返回的“公共”接口之外,没有办法访问startPointhypotenuse()

每当我回答这些理论上的JavaScript问题时,我总是担心我会忘记或忽视一些技术细节。如果有的话,请告诉我,我会修正答案。


模式3本可以成为一个工厂,但由于它立即调用,所以不是。 - Brett Zamir

0

http://www.jsoops.net/对于Js中的oop非常好。它提供了私有、受保护和公共变量和函数,还有继承特性。示例代码:

var ClassA = JsOops(function (pri, pro, pub)
{// pri = private, pro = protected, pub = public

    pri.className = "I am A ";

    this.init = function (var1)// constructor
    {
        pri.className += var1;
    }

    pub.getData = function ()
    {
        return "ClassA(Top=" + pro.getClassName() + ", This=" + pri.getClassName()
        + ", ID=" + pro.getClassId() + ")";
    }

    pri.getClassName = function () { return pri.className; }
    pro.getClassName = function () { return pri.className; }
    pro.getClassId = function () { return 1; }
});

var newA = new ClassA("Class");

//***Access public function
console.log(typeof (newA.getData));
// function
console.log(newA.getData());
// ClassA(Top=I am A Class, This=I am A Class, ID=1)

//***You can not access constructor, private and protected function
console.log(typeof (newA.init));            // undefined
console.log(typeof (newA.className));       // undefined
console.log(typeof (newA.pro));             // undefined
console.log(typeof (newA.getClassName));    // undefined

0

这个问题在其他地方已经被回答了很多次,但为了提供一些不同的选择,ds.oop 是在 JavaScript 中声明带有构造函数的类的好方法。它支持每种可能的继承类型(包括甚至 C# 不支持的一种类型),以及接口,这非常不错。

var Color = ds.make.class({
    type: 'Color',
    constructor: function (r,g,b) { 
        this.r = r;                     /* now r,g, and b are available to   */
        this.g = g;                     /* other methods in the Color class  */
        this.b = b;                     
    }
});
var red = new Color(255,0,0);   // using the new keyword to instantiate the class

0

还有一种可能的方法来实现这个。

var MouseDiff = {};
(function(context) { 
    var privateVarialble = 0;

    context.hypotenuse = function() {
         //code here    
    };

    context.int = function() {
      //code here    
    }
})(MouseDiff); 

在这里,我们只需将命名空间作为参数传递给自调用函数。变量privateVarialble是私有的,因为它没有被分配到上下文中。

我们甚至可以将上下文设置为全局对象(只需更改一个单词!)。在括号内(MouseDiff)改为(this)。这对于库供应商来说是一个很大的优势-他们可以将其功能包装在自调用函数中,并让用户决定它们是否应该是全局的。


0

模式二是我个人偏好的选择,因为它最接近传统的面向对象编程,并且允许轻松继承。

John Resig(JQuery背后的人)在这篇文章中使用了该方法... http://ejohn.org/blog/simple-javascript-inheritance/

[编辑]

实际上,我认为方法1或3没有任何好处。正如其他人所说,1是单例,不允许多个实例,而3...我不知道从哪里开始。


问题在于两者都是立即调用以创建对象,而不是以其通用形式呈现。方法1和3实际上在其通用工厂格式中具有有效的优势,而不是方法2。 - Raynos
模式1的好处是方便,而模式3的好处是静态(以及大量额外的工作)实例隐私保护。 - Brett Zamir
我刚刚注意到关于#3的问题。私有变量的事情非常有趣。我仍然认为失去了使用instanceof和继承的能力。#1和#3更像是命名空间而不是对象。 - Andrew Curioso

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