OO Javascript 构造函数模式:新古典与原型

Posted

技术标签:

【中文标题】OO Javascript 构造函数模式:新古典与原型【英文标题】:OO Javascript constructor pattern: neo-classical vs prototypal 【发布时间】:2010-12-21 01:29:29 【问题描述】:

我看着a talk by Douglas Crockford on the good parts in javascript 和我的眼睛 被打开了。有一次他说,“Javascript 是唯一一种优秀的程序员相信他们可以有效地使用它而无需学习的语言。”然后我意识到,我就是那个人。

在那次演讲中,他发表了一些对我来说非常令人惊讶和富有洞察力的陈述。例如,JavaScript 是地球上最重要的编程语言。或者它是这个星球上最流行的语言。而且,它以许多严重的方式被破坏。

对我来说,他最令人惊讶的声明是“新的就是危险的”。他不再使用它了。他也不使用this

他提出了一种有趣的 Javascript 构造函数模式,它允许私有和公共成员变量,并且既不依赖于 new,也不依赖于 this。它看起来像这样:

// neo-classical constructor
var container =  function(initialParam) 
    var instance = ; // empty object 

    // private members
    var privateField_Value = 0;
    var privateField_Name = "default";

    var privateMethod_M1 = function (a,b,c) 
        // arbitrary
    ; 

    // initialParam is optional
    if (typeof initialParam !== "undefined") 
        privateField_Name= initialParam;
    

    // public members
    instance.publicMethod = function(a, b, c) 
        // because of closures,
        // can call private methods or
        // access private fields here. 
    ;

    instance.setValue = function(v) 
        privateField_Value = v;
    ;

    instance.toString = function()
        return "container(v='" + privateField_Value + "', n='" + privateField_Name + "')";
    ;

    return instance;



// usage
var a = container("Wallaby");
WScript.echo(a.toString()); 
a.setValue(42);
WScript.echo(a.toString()); 

var b = container();
WScript.echo(b.toString()); 

EDIT:更新代码以切换到小写类名。

这种模式是从Crockford's earlier usage models演变而来的。

问题:你使用这种构造函数模式吗?你觉得可以理解吗?你有更好的吗?

【问题讨论】:

天才...不像构造函数,更像是工厂! 确保你避免像 var c = new Container(); 这样的使用,这可能涉及检查 this 的值。 我同意@Kevin Hakanson 的观点,Douglas Crockford 实际上建议对这种方法使用小写的首字母,而对于打算与 new 一起使用的构造函数方法则使用大写的首字母。 @Kevin,是否有人用 new 调用它有关系吗?构造函数中没有对this 的引用,所以……这有关系吗?无论我是否使用 new,(rv instanceof Container) 都会返回 false,其中 rv 是返回值。我认为反之亦然:如果假设是新的,那么您需要进行测试。在这种情况下,不假定 new,看起来不需要测试。真的吗? @Eric - 谢谢,我更新了代码以使用小写。 【参考方案1】:

如果我们需要创建始终相同类型的对象,我宁愿使用原型构造器模式,因为它允许通过自动委托通过共享方法和属性>原型链

我们也可以保留私有对象;看看这种方法:

var ConstructorName = (function()  //IIFE
    'use strict';

    function privateMethod (args) 

    function ConstructorName(args) 
        // enforces new (prevent 'this' be the global scope)
        if (!(this instanceof ConstructorName)) 
            return new ConstructorName(args);
        
        // constructor body
    

    // shared members (automatic delegation)
    ConstructorName.prototype.methodName = function(args) 
        // use private objects
    ;

    return ConstructorName;
());

我建议查看这个答案,我们可以找到一种有趣的方式来创建构造函数:Different way of creating a Javascript Object

这是一个可以使用原型构造方法的例子:How do I create a custom Error in JavaScript?

【讨论】:

【参考方案2】:

认为这可以帮助讨论在这里报告有关此模式的要点,正如 Douglas Crockford 在他的“高级 JavaScript”演示视频中所解释的那样。

主要的一点是避免使用 new 运算符。 因为当有人在调用对象构造函数时忘记使用 new 操作符时,构造函数中添加的对象成员最终都会进入全局命名空间。 请注意,在这种情况下,没有通知错误的运行时警告或错误,全局命名空间只是被污染了。 事实证明,这会产生严重的安全隐患,并且会导致许多难以诊断的错误。

这就是这个 Powerconstructor 模式的要点。

私有/特权部分是次要的。可以以不同的方式做到这一点。 不使用原型成员也是如此。

HTH 卢卡

【讨论】:

我第一次提出这个问题已经有一段时间了,随着时间的推移,我有了一些看法。在我看来,Crockford 提倡做大量工作来防止一小部分错误 - 省略 new 运算符。为什么要这么麻烦?对于其他常见错误情况,我们没有类似的保护措施:例如,省略if 语句或return 语句。您说这是难以诊断的错误的根源。真的吗? 我还没有看到这方面的证据,而且我怀疑“省略新”会导致 Javascript 开发中的大部分痛苦。当然,最初的单元测试很快就会向开发人员展示,嘿!你忘了在这里调用 new! 然后问题就解决了。静态分析 (JSLINT) 也可以用来提供帮助。在我看来,到处执行 Crockford 公约以解决这个问题似乎反应过度。 为避免忘记新的,只需以if (!(this instanceof ConstructorName)) throw new Error("Invalid call of the constructor"); 开始构造函数【参考方案3】:

你使用这种构造函数模式吗?

没有

你觉得可以理解吗?

是的,它非常简单。

你有更好的吗?

我还没有看过演讲,但我很快就会开始。 在那之前,我看不到使用newthis 的危险,原因如下:

在没有听过他的观点的情况下,我只能假设他建议远离此类事情,因为 this 的性质,以及根据执行特定方法的上下文(直接在原始对象上或作为回调等)。作为一名教育工作者,他可能会教导避免使用这些关键字,因为在很大程度上没有了解和缺乏经验的开发人员在没有先了解该语言的本质的情况下涉足 JavaScript。对于熟悉该语言的经验丰富的开发人员,我不认为有必要避免该语言的这个特性,这赋予了它惊人的灵活性(这与避免像 with 这样的事情完全不同)。说了这么多,我现在就看。

无论如何,当不使用某种支持自动继承的框架时(如dojo.declare),或者在编写独立于框架的对象时,我目前采用以下方法。

定义:

var SomeObject = function() 
    /* Private access */
    var privateMember = "I am a private member";
    var privateMethod = bindScope(this, function() 
        console.log(privateMember, this.publicMember);
    );

    /* Public access */
    this.publicMember = "I am a public member";

    this.publicMethod = function() 
        console.log(privateMember, this.publicMember);
    ;
    this.privateMethodWrapper = function() 
        privateMethod();
    
;

用法

var o = new SomeObject();
o.privateMethodWrapper();

其中bindScope 是类似于Dojo 的dojo.hitch 或Prototype 的Function.prototype.bind 的实用函数。

【讨论】:

【参考方案4】:

你使用这种构造函数模式吗?

当我第一次学习 JavaScript 并偶然发现 Douglas Crockford 的文献时,我曾使用过这种模式。

你觉得可以理解吗?

只要懂闭包,这个方法就一目了然了。

你有更好的吗?

这取决于您要完成的工作。如果您正在尝试编写一个尽可能白痴证明的库,那么我可以完全理解私有变量的使用。它可能有助于防止用户无意中干扰对象的完整性。是的,时间成本可能会稍大一些(大约大 3 倍),但时间成本在实际影响您的应用程序之前是一个没有实际意义的论点。我注意到上一篇文章 (test) 中提到的测试没有考虑访问对象功能所需的时间。

我发现原型/构造方法通常会导致更快的构造时间,是的,但它不一定会节省检索时间。与将函数直接附加到您正在使用的对象相比,使用原型链查找函数需要额外的成本。因此,如果您要调用大量原型函数而不是构造大量对象,则使用 Crockford 模式可能更有意义。

不过,如果我不是为大量受众编写代码,我倾向于不喜欢使用私有变量。如果我和一群都是有能力的编码人员在一起,我通常可以相信,如果我自己编码和评论得当,他们会知道如何使用我的代码。然后我使用类似于 Crockford 的无私有成员/方法的模式。

【讨论】:

【参考方案5】:

这看起来像module pattern 的非单例版本,通过利用 JavaScript 的“闭包”可以模拟私有变量。

我喜欢它(有点...)。但我并没有真正看到以这种方式完成私有变量的优势,尤其是当这意味着添加的任何新方法(在初始化之后)都无法访问私有变量时。

另外,它没有利用 JavaScript 的原型模型。每次调用构造函数时都必须初始化所有方法和属性 - 如果您将方法存储在构造函数的原型中,则不会发生这种情况。事实是,使用传统的构造函数/原型模式要快得多!你真的认为私有变量让性能损失值得吗?

这种模型对模块模式很有意义,因为它只被初始化一次(创建一个伪单例),但我不太确定它在这里是否有意义。

你使用这种构造函数模式吗?

不,虽然我确实使用了它的单例变体,但模块模式...

你觉得可以理解吗?

是的,它可读且非常清晰,但我不喜欢将所有内容都集中在这样的构造函数中。

你有更好的吗?

如果你真的需要私有变量,那么一定要坚持下去。否则,只需使用传统的构造函数/原型模式(除非你和 Crockford 一样害怕 new/this 组合):

function Constructor(foo) 
    this.foo = foo;
    // ...


Constructor.prototype.method = function()  ;

与 Doug 对该主题的看法相关的其他类似问题:

Pseudo-classical vs. “The JavaScript way” Is JavaScript ‘s “new” Keyword Considered Harmful?

【讨论】:

什么是传统的构造函数模式?就像我说的,我就是那种在学习语言之前使用语言的人。另外,什么是性能命中?每次调用 ctor/factory 时都没有初始化函数……我认为这完全消除了私有变量。这就是你问“值得吗”的意思吗?在我看来,Crockford 的一个主要问题是缺乏模块化,并且不同的 js 库可能会意外地践踏彼此的变量。他的模块模式,他拒绝使用 new 或 this - 它们都旨在直接解决这个问题。 通过“常规构造函数模式”,我的意思是有一个构造函数,它利用隐式创建的自身实例(通过new 创建),它可以称为this。然后,可以将任何方法添加到构造函数的原型中 - 添加到此原型的任何方法都将立即可用于“类”的所有当前和未来实例。查看我添加到答案中的代码示例。性能命中比您想象的要多。在此处进行比较:gist.github.com/244166 对比传统方案和 Crockford 方案,我的性能速度相差 10 倍。 “另外,它没有利用 JavaScript 的原型模型。” +1 到目前为止,这是我使用这种方法的最大问题。 @Gregg Lind - 在 OO 意义上,它是“私有的”,其他代码无法看到或访问它。正如我在服务器上运行的 Java 类的“私有”成员仍然可以被开发人员看到或调试,但不能被其他类访问。​​【参考方案6】:

我避免使用这种模式,因为大多数人觉得它更难阅读。我通常遵循两种方法:

    如果我只有其中之一,那么我会使用匿名对象:

    var MyObject = 
        myMethod: function() 
            // do something
        
    ;
    

    对于不止一项我使用标准 javascript 原型继承

    var MyClass = function() 
    ;
    MyClass.prototype.myMethod = function() 
        // do something
    ;
    
    var myObject = new MyClass();
    

(1) 更容易阅读、理解和编写。 (2) 当有多个对象时效率更高。 Crockford 的代码每次都会在构造函数中创建一个新的函数副本。闭包也有更难调试的缺点。

虽然您丢失了真正的私有变量,但您还是按照惯例在应该是私有的成员前面加上 _

this 在 javascript 中是一个公认的难题,但可以使用 .call.apply 来正确设置它。我也经常使用var self = this; 创建一个闭包变量,作为this 在成员函数中定义的函数内部使用。

MyClass.prototype.myMethod = function() 
    var self = this;

    // Either
    function inner1() 
        this.member();
    
    inner1.call(this);

    // Or
    function inner2() 
        self.member();
    
    inner2();
;

【讨论】:

匿名对象?你是说对象字面量吗?【参考方案7】:

在他的书中它被称为功能继承(第 52 页)。我还没用。如果您向 Douglas 学习 javascript,这是可以理解的。我认为没有更好的方法。这个很好,因为它:

允许程序员创建私有成员

保护私有成员并消除 this 与伪装隐私相反(私有成员以 _ 开头)

使继承顺畅,没有不必要的代码

但是,它有一些缺点。你可以在这里阅读更多信息:http://www.bolinfest.com/javascript/inheritance.php

【讨论】:

以上是关于OO Javascript 构造函数模式:新古典与原型的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 创建对象 (工厂模式构造函数模式原型模式组合使用构造函数模式与原型模式)

JavaScript对象的继承

JavaScript的面向对象

javascript构造函数及原型对象

JavaScript创建对象的默认方式:组合使用构造函数模式和原型模式

javascript中的new有啥用