为什么是 JavaScript 原型?

IT技术 javascript
2021-02-26 13:41:41

这可能会让你觉得这是一个语法不正确且可能是疯狂的问题,但我的意思是:在尝试理解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');

那么,回到我的问题:你到底为什么要这样做?你为什么不直接把这个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让您做原本无法做的事情,但我想不出很好的理由来说明您为什么想做这些事情。

编辑:一些回应:

  • 当我说“为什么先声明一个实例然后再开始添加方法?” 我更多地批评了我看到的所有示例,这些示例按照我的第一个示例的顺序进行。当这个顺序改变时,就像下面 Harmen 的回应一样,它在视觉上确实更有意义。然而,这并没有改变,在同样作为我的第一个例子,你可以创建一个空的目标函数构造的事实,声明此对象的100个实例,然后事后才定义一下原来的对象实际上给它方法和属性通过prototype. 也许这样做通常是为了暗示下面概述的复制与参考的想法。
  • 根据几个回复,我的新理解是:如果将所有属性和方法添加到对象函数构造函数中,然后创建该对象的 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');
6个回答

那么,回到我的问题:你到底为什么要这样做?为什么不直接在 Guitar 中加入播放功能呢?为什么先声明一个实例,然后再开始添加方法?

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

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

注意区别。在您发布的“为什么不这样”示例中,每次创建新吉他时,您都需要创建与其他所有演奏方法相同的新演奏方法。然而,如果在原型上演奏,所有吉他都借鉴了同一个原型,所以它们都共享相同的演奏代码。它是x把吉他之间的区别,每把吉他都有相同的演奏代码(所以你有x份演奏)与x把吉他共享相同的演奏代码(不管有多少吉他,演奏一份)。权衡当然是在运行时播放需要与调用它进行作用域的对象相关联,但是 javascript 具有允许您非常有效和轻松地执行此操作的方法(即callapply 方法)

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


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

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

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

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

我们这些在更“传统”的面向对象世界(阅读 Java/C#/PHP)中工作的人发现在运行时扩展类的想法几乎完全陌生。我的意思是,说真的,这应该是我的 OBJECT。我的目标将继续前进并做已设定的事情。子类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 的方式非常相关。它更多地与closuresJava OOP的想法有关

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

另一个(对于我们的目的(以及,老实说,Church 的目的)更重要的)定义被麦卡锡采纳。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)))

现在,这与 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}
  })());

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

由于this原始定义中的所有's 都是该对象的实例,因此修改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 这是一个了不起的解释,我只想说谢谢你花时间就这个主题发表如此内容丰富且易于理解的观点。
2021-05-02 13:41:41

如果不使用原型,则每次调用 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,因此具有“播放”功能。如果您愿意,您还可以在吉他中覆盖乐器的“播放”功能。

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

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

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

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

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

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

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

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

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

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

你给出的第一种方法更快,当你以另一种顺序编写它时,它真的开始有意义了:

//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 不需要执行构造函数来创建变量,它可以只使用原型的预定义变量。

如需证明,请参阅有关与问题非常相似的问题的速度测试


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

function Guitar(){
  // constructor
}

Guitar.prototype = {
  play: function(a){
    alert(a);
  },

  stop: function(){
    alert('stop it');
  }
};
@hvgotcodes:它将更改更改后创建的实例的原型。但它不会改变已经存在的实例的任何内容。他们仍然有对原始原型的参考。
2021-05-07 13:41:41
是否会用对象字面量一举覆盖整个原型,将总是应该在原型上的东西炸掉?
2021-05-19 13:41:41