【问题标题】:Why is JavaScript prototyping?为什么要进行 JavaScript 原型设计?
【发布时间】:2011-01-10 19:13:59
【问题描述】:

这可能会让您觉得这是一个语法不正确且可能很疯狂的问题,但我的意思是:当我试图理解 JavaScript 中 prototype 的概念时,我遇到了以下示例,它们或多或少有些复杂:

//Guitar function constructor
function Guitar(color, strings) {
    this.color = color;
    this.strings = strings;
}
//Create a new instance of a Guitar
var myGuitar = new Guitar('Black', ['D', 'A', 'D', 'F', 'A', 'E']);
//Adding a new method to Guitar via prototype
Guitar.prototype.play = function (chord) {
    alert('Playing chord: ' + chord);
};
//Now make use of this new method in a pre-declared instance
myGuitar.play('D5');

那么,关于我的问题:你到底为什么要这样做?你为什么不把play 函数放在Guitar 开始呢?为什么要声明一个实例,然后再开始添加方法?我能看到的唯一原因是,如果您希望 myGuitar 在最初创建时无法访问 play,但我无法举出任何示例来说明您为什么想要这样的东西的原因。

这样做似乎更有意义:

function Guitar(color, string) {
    this.color = color;
    this.strings = strings;
    this.play = function (chord) {
        alert('Playing chord: ' + chord);
    };
}
var myGuitar = new Guitar('White', ['E', 'A', 'D', 'G', 'B', 'E']);
myGuitar.play('E7#9');

这里真正的问题是第二个例子对我有意义,而第一个例子没有,而实际上,出于某种原因,第一个例子可能更好。不幸的是,我发现的每个教程都只是通过使用prototype 的步骤,而不是为什么prototype 范式一开始就存在。

prototype 似乎允许你做你本来不能做的事情,但我想不出你为什么想做这些事情的充分理由。

编辑:一些回应:

  • 当我说“为什么要声明一个实例,然后再开始添加方法?”我更多地批评了我看到的所有示例,这些示例按照我的第一个示例的顺序进行。当这个顺序改变时,如下面的哈门回应,它确实在视觉上更有意义。然而,这并没有改变这样一个事实,就像我的第一个示例一样,您可以创建一个空对象函数构造函数,声明该对象的 100 个实例,然后再定义原始对象实际上是什么 通过prototype 为其提供方法和属性。也许这样做通常是为了暗示下面概述的复制与参考的想法。
  • 基于几个回复,这是我的新理解:如果您将所有属性和方法添加到对象函数构造函数,然后创建该对象的 100 个实例,您将获得所有属性和方法的 100 个副本。相反,如果您将所有属性和方法添加到对象函数构造函数的 prototype,然后创建该对象的 100 个实例,您将获得 100 个 references 到对象的属性和方法。这显然更快、更有效,这就是为什么使用prototype 的原因(除了改变String 和Image 之类的东西,如下所述)。那么,为什么不这样做呢:

(显然,项目符号列表会破坏任何代码,因此我必须在此处添加一行单独的文本)

function Guitar(color, strings) {
    this.prototype.color = color;
    this.prototype.strings = strings;
    this.prototype.play = function (chord) {
        alert('Playing chord: ' + chord);
    };
}
var myGuitar = new Guitar('Blue', ['D', 'A', 'D', 'G', 'B', 'E']);
myGuitar.play('Dm7');

【问题讨论】:

  • 我为您的编辑更新了我的答案。

标签: javascript


【解决方案1】:

那么,关于我的问题:你到底为什么要这样做?你为什么不把播放功能放在吉他开始呢?为什么要声明一个实例,然后再开始添加方法?

Javascript 不是“经典”继承语言。它使用原型继承。它就是这样。在这种情况下,在“类”上创建方法的正确方法是将方法放在原型上。请注意,我将“类”放在引号中,因为严格来说 JS 没有“类”的概念。在 JS 中,您处理定义为函数的对象。

您可以在定义 Guitar 的函数中声明该方法,但是,当您这样做时,每把新吉他都会获得其 自己的 play 方法副本。当您开始创建吉他时,将其放在原型上会在运行时环境中更有效。每个实例共享相同的播放方法,但在调用时会设置上下文/范围,因此它充当您在经典继承语言中习惯的正确实例方法。

注意区别。在您发布的“为什么不这样”示例中,每次创建新吉他时,都需要创建与其他所有演奏方法相同的新演奏方法。但是,如果原型上的 play 是从同一个原型中借用的所有吉他,那么它们都共享相同的 play 代码。它是 x 数量的吉他之间的差异,每个都有相同的播放代码(所以你有 x 个播放副本)与 x 数量的吉他共享相同的播放代码(无论有多少吉他,都需要 1 份播放)。权衡当然是在运行时 play 需要与调用它的对象相关联,但 javascript 有一些方法可以让您非常有效和轻松地做到这一点(即 call 和 apply 方法)

许多 javascript 框架定义了自己的实用程序来创建“类”。通常,它们允许您编写代码,就像您说您希望看到的示例一样。在幕后,他们正在为您将功能放在原型上。


编辑——回答你更新的问题,为什么不能这样做

function Guitar() {
    this.prototype.play = function()....
}

这与 javascript 如何使用“new”关键字创建对象有关。请参阅第二个答案here - 基本上当您创建实例时,javascript 会创建对象并 然后 分配原型属性。所以 this.prototype.play 真的没有意义;事实上,如果你尝试它,你会得到一个错误。

【讨论】:

    【解决方案2】:

    作为开始前的说明——我在这里使用 ECMAScript 而不是 JavaScript,因为 ActionScript 1 和 2 在运行时表现出完全相同相同的行为。

    我们这些在更“传统”的面向对象世界(阅读 Java/C#/PHP)中工作的人发现在运行时扩展类的想法几乎完全陌生。我的意思是,说真的,这应该是我的对象。我的对象将出去做已经提出的事情。子类EXTEND 其他CLASSES。它有一种非常有条理、坚实、像石头一样的感觉。而且,在大多数情况下,这很有效,而且效果很好。 (这是 Gosling 争论的原因之一,我认为我们大多数人都会相当有效地同意它非常适合大型系统)

    另一方面,ECMAScript 遵循更原始的 OOP 概念。不管你信不信,在 ECMAScript 中,类继承完全是一个巨大的装饰器模式。但这不仅仅是你可能会说存在于 C++ 和 Python 中的装饰器模式(你可以很容易地说那些是装饰器)。 ECMAScript 允许您将类原型分配给实例。

    想象一下这发生在 Java 中:

    class Foo {
        Foo(){}
    }
    
    class Bar extends new Foo() {
        // AAAHHHG!!!! THE INSANITY!
    }
    

    但是,这正是 ECMAScript 中可用的(我相信 Io 也允许这样的事情,但不要引用我的话)。

    我之所以说这是原始的,是因为这种设计理念与 McCarthy 使用 Lambda Calculus 实现 Lisp 的方式密切相关。它与 closures 的想法有关,而不是 Java OOP。

    所以,在过去,Alonzo Church 写了 The Calculi Lambda Conversion,这是 Lambda 微积分的开创性著作。在其中,他提出了两种看待多参数函数的方法。首先,它们可以被认为是接受单例、元组、三元组等的函数。基本上 f(x,y,z) 将被理解为接受参数 (x,y,z) 的 f。 (顺便说一句,我认为这是 Python 参数列表结构的主要推动力,但这是推测)。

    另一个(对于我们的目的(老实说,Church 的目的)更重要)的定义是由 McCarthy 提出的。 f(x,y,z) 应转换为 f(x g(y h(z)))。最外层方法的解析可能来自内部函数调用生成的一系列状态。存储的内部状态是闭包的基础,而闭包又是现代 OOP 的基础之一。闭包允许在不同点之间传递封闭的可执行状态。

    Land Of Lisp 一书的转移:

    ; Can you tell what this does? It it is just like your favorite 
    ; DB’s sequence!
    ; (getx) returns the current value of X. (increment) adds 1 to x 
    ; The beauty? Once the let parens close, x only exists in the 
    ; scope of the two functions! passable enclosed executable state!
    ; It is amazingly exciting!
    (let (x 0)
      ; apologies if I messed up the syntax
      (defun increment ()(setf x (+ 1 x)))
      (defun getx ()(x)))
    

    现在,这与 ECMAScript 与 Java 有什么关系?好吧,当一个对象在 ECMAScript 中创建时,它几乎可以完全遵循该模式:

     function getSequence()
    {
         var x = 0;
         function getx(){ return x }
         function increment(){ x++ }
         // once again, passable, enclosed, executable state
         return { getX: getX, increment:increment}
    }
    

    这就是原型开始出现的地方。ECMAScript 中的继承意味着“从对象 A 开始并添加到它”。它不会复制它。它采用这种神奇的状态,ECMAScript 将其附加。这就是为什么它必须允许MyClass.prototype.foo = 1的根源和顶峰。

    至于为什么要“事后”附加方法。在大多数情况下,它真的归结为风格偏好。在原始定义内部发生的所有事情都只不过是在外部发生的相同类型的装饰。

    在大多数情况下,将所有定义放在同一个位置在风格上是有益的,但有时这是不可能的。例如,jQuery 扩展基于直接附加 jQuery 对象原型的想法工作。 Prototype 库实际上有一种专门的方法来扩展它始终使用的类定义。

    如果我没记错 Prototype.js,它是这样的:

     var Sequence = function(){}
    
     // Object.extend takes all keys & values from the right object and
     // adds them to the one on the left.
     Object.extend( Sequence.prototype, (function()
     {
         var x = 0;
         function getx(){ return x }
         function increment(){ x++ }
         return { getX: getX, increment:increment}
      })());
    

    至于在原始定义中使用原型关键字,嗯,这在大多数情况下是行不通的,因为“this”指的是正在定义的对象的实例(在构造实例时)。除非实例还具有“原型”属性,否则 this.prototype 必然是未定义的!

    由于原始定义中的所有this 都是该对象的实例,因此修改this 就足够了。但是,(我笑着说这句话,因为它与原型一致)每个this 都有一个constructor 属性。

     // set the id of all instances of this “class”. Event those already 
     // instantiated...
     this.constructor.prototype.id = 2
     console.log( this.id );
    

    【讨论】:

    • @cwallenpoole 这是一个了不起的解释,我只想说感谢您抽出宝贵的时间发布关于这个主题的信息丰富且易于理解的观点。
    【解决方案3】:

    如果不使用原型,每次调用 Guitar 的构造函数时,都会创建一个新函数。如果您正在创建大量 Guitar 对象,您会注意到性能上的差异。

    使用原型的另一个原因是模拟经典继承。

    var Instrument = {
        play: function (chord) {
          alert('Playing chord: ' + chord);
        }
    };
    
    var Guitar = (function() {
        var constructor = function(color, strings) {
            this.color = color;
            this.strings = strings;
        };
        constructor.prototype = Instrument;
        return constructor;
    }());
    
    var myGuitar = new Guitar('Black', ['D', 'A', 'D', 'F', 'A', 'E']);
    myGuitar.play('D5');
    

    在本例中,Guitar 扩展了 Instrument,因此具有“播放”功能。如果您愿意,您还可以在 Guitar 中覆盖乐器的“播放”功能。

    【讨论】:

      【解决方案4】:

      JavaScript 是一种原型语言,是一种相当稀有的语言。这根本不是任意的,它是一种语言的要求,它是实时评估的并且能够“评估”、动态修改和 REPL。

      原型继承可以理解为与基于运行时“实时”类定义而不是静态预定义类定义的面向对象编程相比。

      编辑:从以下链接窃取的另一个解释也很有用。在面向对象的语言(类 -> 对象/实例)中,任何给定 X 的所有可能属性都在类 X 中枚举,并且实例为每个属性填充其自己的特定值。在原型继承中,您只描述了对 live X 和类似但不同的 live Y 的现有引用之间的差异,并且没有主副本。

      http://web.media.mit.edu/~lieber/Lieberary/OOP/Delegation/Delegation.html

      首先,您需要了解上下文。 JavaScript 是一种解释性语言,可以在实时环境中执行和修改。程序的内部结构本身可以在运行时进行修改。这与任何编译语言,甚至 CLR 链接语言(如 .Net 的东西)相比,都有不同的限制和优势。

      “eval”/REPL 的概念需要动态变量类型。您无法有效地实时编辑必须具有预定义的基于整体类的继承结构的环境。没意义,你还不如预编译成汇编或者字节码。

      取而代之的是,我们有原型继承,您可以在其中链接对象的实例的属性。这个概念是,如果你在一个全实时的环境中,类(静态的、预定义的构造)是不必要的限制。类建立在 JavaScript 中不存在的约束之上。

      使用这种策略,JavaScript 基本上依赖于“实时”的一切,没有什么是禁止的,没有你永远无法触及的“定义和完成”的类。变量中没有比你的代码更神圣的“一个真正的苏格兰人”,因为一切都遵循与你今天决定编写的代码相同的规则。

      这样做的后果很明显,而且非常以人为本。它促使语言实现者在提供原生对象时使用轻巧、高效的方式。如果他们做得不好,暴徒只会篡夺平台并重建自己的平台(阅读 MooTools 的源代码,它从字面上重新定义/重新实现了一切,从函数和对象开始)。这就是为旧 Internet Explorer 版本等平台带来兼容性的方式。它促进了浅而窄、功能密集的库。深度继承导致最常用的部分(很容易)被挑选出来并成为最终的首选库。当人们挑选他们需要的部分时,广泛的库会导致崩溃,因为咬一口很容易,而不是像在大多数其他环境中那样不可能。

      微库的概念在 JavaScript 中独树一帜,它绝对可以追溯到该语言的基础。它以其他语言(我所知道的)无法提倡的方式鼓励人类消费方面的效率和简洁。

      【讨论】:

        【解决方案5】:

        你给的第一个方法比较快,换个顺序写的时候才真正开始有意义:

        //Guitar function constructor
        function Guitar(color, strings) {
          this.color = color;
          this.strings = strings;
        }
        
        Guitar.prototype.play = function (chord) {
          alert('Playing chord: ' + chord);
        };
        
        var myGuitar = new Guitar('Black', ['D', 'A', 'D', 'F', 'A', 'E']);
        

        速度更快,因为Javascript不需要执行构造函数来创建变量,它可以使用原型的预定义变量。

        为了证明,see this speed test 就一个与此非常相似的问题。


        也许这个替代版本对你来说更有意义:

        function Guitar(){
          // constructor
        }
        
        Guitar.prototype = {
          play: function(a){
            alert(a);
          },
        
          stop: function(){
            alert('stop it');
          }
        };
        

        【讨论】:

        • 会用对象字面量一举覆盖整个原型吗?
        • @hvgotcodes:它将更改更改后创建的实例的原型。但它不会改变现有实例的任何内容。他们仍然有对原始原型的引用。
        【解决方案6】:

        一方面,您可以使用原型来扩展内置在 JavaScript 语言中的对象(如字符串)。我更喜欢自定义对象的第二个示例。

        【讨论】:

          【解决方案7】:

          Javascript 基于原型。在罗马时,在 JS 中像罗马人那样做,使用原型继承。

          它更有效,因为该方法可以在每个对象上继承。如果它不是原型方法,那么该对象的每个 实例都会有自己的play 方法。既然可以走高效自然的JS路线,为什么还要走低效、非传统的JS路线?

          【讨论】:

          • 还应注意,它比经典模型更强大,例如你可以很容易地在 JS 中实现经典,但反过来却相当复杂。
          【解决方案8】:

          你已经有很多很好的答案,这就是为什么我没有把你的所有观点都讲完。

          为什么要声明一个实例,然后再开始添加方法?

          这是不正确的。原型对象独立于每个实例而存在。它是函数对象的属性(构造函数)。
          当您创建一个新实例时,它会“继承”原型的所有属性(实际上,它有对它的引用)。

          实际上,考虑对象和引用是有意义的:共享一个对象的引用比每个实例都有自己的对象副本(对象在这种情况下是函数play)。

          至于为什么它是基于原型的:你也可以问为什么存在不同的语言范式(函数式、oo 式、声明式)。没有只有一个正确的方法来做某事。

          【讨论】:

            【解决方案9】:

            它基于原型创建设计模式。这个维基百科链接有一个很好的讨论。

            http://en.wikipedia.org/wiki/Prototype_pattern

            【讨论】:

            • @BJ - 您链接到的维基百科页面中的示例使用 Java,而不是 javascript。您的意思是链接到另一篇文章吗?
            • @John - 不。这就是我在编译期间试图挤入答案所得到的。我删除了错误的陈述。谢谢。
            • 作为对我之前的错误的一些忏悔,这里有一个很好的之前的 SO 讨论。确保点击链接回到 Steve Yegge 的博客并阅读 Prototypes 的讨论;哥德尔、埃舍尔和巴赫;和足球运动员,以及其他人。 stackoverflow.com/questions/228134/…
            【解决方案10】:

            兄弟让我问你一件事,如果你有吉他、卡西欧、小提琴,你想在这些乐器中演奏相同的和弦。

            所以我想我们为什么不单独保留一个函数 play_chord 并将这个(play_chord)函数与上述任何一种乐器一起使用,而不是在吉他、卡西欧或小提琴中使用每个函数。

            所以最后,当我们需要一个可以作为其他构造函数一部分的函数时,我们应该在原型中定义该特定函数并相应地使用:)

            【讨论】:

              猜你喜欢
              • 2011-03-06
              • 2013-04-14
              • 1970-01-01
              • 2012-01-17
              • 1970-01-01
              • 2012-11-16
              相关资源
              最近更新 更多