为什么JavaScript原型设计?

Con*_*sed 40 javascript

这可能会让你觉得这是一个语法不正确和可能是疯狂的问题,但这就是我的意思:当试图理解prototypeJavaScript中的概念时,我遇到了以下几个或多或少复杂版本的例子:

//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');
Run Code Online (Sandbox Code Playgroud)

所以,关于我的问题:为什么你想要这样做?你为什么不直接把这个play功能放进Guitar去?为什么声明一个实例然后开始添加方法?我能看到的唯一原因是,如果你想myGuitarplay最初创建它时无法访问它,但我可以想出没有一个例子来说明为什么你会想要这样的东西.

看起来这样做会更有意义:

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');
Run Code Online (Sandbox Code Playgroud)

这里真正的问题是第二个例子对我有意义,而第一个例子没有,而实际上,第一个例子可能因某些原因更好.不幸的是,我所发现的每一个教程都经历了使用的步骤,prototype但不是为什么prototype范式一直存在.

它似乎prototype允许你做一些你原本无法做到的事情,但我没有充分的理由说明你为什么要这样做.

编辑:一些回复:

  • 当我说"为什么声明一个实例然后开始添加方法?" 我更批评我看到的所有例子,按照我的第一个例子的顺序播放.当这个顺序改变时,如下面Harmen的回答,它确实在视觉上更有意义.但是,这并没有改变这样一个事实,即与我的第一个例子一样,你可以创建一个空的对象函数构造函数,声明这个对象的100个实例,然后只有通过给它来定义原始对象的实际内容.方法和属性通过prototype.也许通常这样做是为了暗示下面概述的Copy vs. Reference的想法.
  • 根据几个响应,这是我的新理解:如果您将所有属性和方法添加到对象函数构造函数,然后创建该对象的100个实例,您将获得所有属性和方法的100个副本.相反,如果将所有属性和方法添加到prototype对象函数构造函数中,然后创建该对象的100个实例,则会获得对对象属性和方法的单个(1)副本的100个引用.这显然更快,更有效,并且是使用的原因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');
Run Code Online (Sandbox Code Playgroud)

hvg*_*des 24

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

Javascript不是一种"经典"继承语言.它使用原型继承.就是这样.既然如此,在'类'上创建方法的正确方法是将方法放在原型上.请注意,我将'class'放在引号中,因为严格来说JS没有'class'的概念.在JS中,您处理对象,这些对象被定义为函数.

您可以在定义Guitar的函数中声明该方法,但是,当您这样做时,每个新吉他都会获得自己的播放方法副本.当您开始创建吉他时,将它放在原型上会在运行时环境中更有效.每个实例共享相同的play方法,但是在调用时设置了context/scope,因此它在您的经典继承语言中扮演了一个适当的实例方法.

注意区别.在您发布的"为什么不是这种方式"示例中,每次创建新吉他时,都需要创建一个与其他播放方法相同的新播放方法.然而,如果游戏是在原型上,所有吉他都从相同的原型中借用,所以他们都共享相同的游戏代码.它是x个吉他之间的区别,每个吉他都有相同的游戏代码(所以你有x个游戏副本)和x个共享相同游戏代码的吉他数量(无论有多少个吉他都有1个游戏副本).权衡当然是在运行时播放需要与调用它的对象相关联,但是javascript有一些方法可以让你非常有效和轻松地完成它(即callapply方法)

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


编辑 - 在回答您更新的问题时,为什么不能这样做

function Guitar() {
    this.prototype.play = function()....
}
Run Code Online (Sandbox Code Playgroud)

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


cwa*_*ole 12

作为开始之前的注释 - 我在这里使用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!
}
Run Code Online (Sandbox Code Playgroud)

但是,这正是ECMAScript中可用的内容(我相信Io也允许这样的东西,但不要引用我).

我之所以说这是原始的原因是这种类型的设计理念与McCarthy使用Lambda Calculus实现Lisp的方式非常相关.与closuresJava OOP相比,它更多地与Java 的概念有关.

所以,在当天,Alonzo Church写道The Calculi Lambda Conversion,Lambda微积分中的开创性工作.在其中,他提出了两种查看多参数函数的方法.首先,它们可以被认为是接受单体,元组,三元组等的函数.基本上f(x,y,z)将被理解为接受参数(x,y,z)的f.(顺便说一下,我的拙见认为这是Python参数列表结构的主要推动力,但这是猜想).

另一个(为了我们的目的(并且,诚实地,教会的目的)更重要的)定义被麦卡锡接受.应将f(x,y,z)转换为f(xg(yh(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)))
Run Code Online (Sandbox Code Playgroud)

现在,这与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}
}
Run Code Online (Sandbox Code Playgroud)

这里是原型开始进入的地方.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}
  })());
Run Code Online (Sandbox Code Playgroud)

至于在原始定义中使用prototype关键字,那么在大多数情况下都不会起作用,因为"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 );
Run Code Online (Sandbox Code Playgroud)


Spa*_*ile 1

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