JavaScript有原型,并且原型很奇怪。 如此奇怪的是,实际上,某些语言(例如CoffeeScript)会编译为JavaScript,试图以此为依据,试图呈现出更健康,更易消化的软件包。 但是,一旦您学会使用原型,它们就会成为您武器库中非常有用的工具。

无阶级社会

因此,您在自己的工作室里,制作了热门单曲《谋杀的呼唤》中的最新作品。 您已经完成了其中的八个操作,因此您决定对其进行更改,并使用JavaScript编写整个代码。 很容易,对吧? 但是,存在一个小问题– JavaScript表示它是面向对象的,但是您不知道如何定义一个类。 事实证明,您不能使用类! 至少不是传统意义上的。 但是肯定有对象。

您的第一个对象

soldier = new Object()

前面的代码创建一个对象,并将其称为soldier 。 你想让士兵做什么? 跳起来怎么样? 您可以将A按钮映射到在其他地方定义的jump()函数,如下所示。

soldier.a = jump

您实际上是将士兵a属性设置为等于功能。 注意,我们没有包含括号。 这是因为当您在JavaScript中将括号包含在函数中时,它将调用该函数。 如果不包括括号,它将仅返回对函数本身的引用。 因此,如果您调用soldier.a() ,它将执行该函数。 但是,如果您在不带括号的情况下编写soldier.a ,则它将返回实际功能。

另外,请注意,您是直接在对象上分配功能。 到目前为止,我们已经注意到了JavaScript的怪癖,但与原型系统没有直接关系。 因此,鉴于您对JavaScript脚本编写有相当的经验,因此继续进行操作,并将功能分配给其他按钮,如下所示。

soldier.b = punch
soldier.x = reload
soldier.r = machineGun

复制粘贴地狱

现在,您想要创建一个与第一个玩家几乎完全相同的第二个玩家,只是他们使用狙击步枪而不是机枪。 有两种方法可以做到这一点。 首先是重复代码,如下所示。

sniper = new Object()
sniper.a = jump
sniper.b = punch
sniper.x = reload
sniper.r = snipe

它有效,但是很乏味。 特别是因为您向游戏设计师保证他可以在游戏中放置100个不同类型的角色,每个角色具有相同的200个动作的不同组合。 那是很多重复的代码,很多复制粘贴,以及很多潜在的错误。

被原型抢救

因此,您可以使用object()函数对狙击手进行重新编码,如下所示。 我们将回到object()的实际实现。

sniper = object(soldier)
sniper.r = snipe

您可以确定要加载游戏,然后以狙击手的身份开始游戏。 果然, A按钮使您的角色跳跃。 您按X按钮,它会重新加载。 您按R按钮,它会射击狙击步枪。 狙击手似乎已经继承了士兵的所有特征,然后覆盖了映射到R的函数。 如果您用英语解释它,您会说“狙击手就像士兵,除了它会发射狙击步枪。” 请注意,英语句子与定义狙击手的代码相似。

功能盗窃

让我们实现一个受伤的狙击手。 他们就像一个普通的狙击手,除了当他们试图跳下时会痛苦地哭出来。

woundedSniper = object(sniper)
woundedSniper.a = function(){console.log('aaaargh my leg!')}

在这里,我们使用匿名函数代替以前命名的函数。 很好,但这意味着我们在定义woundedSoldier类时必须做些不同的woundedSoldier

woundedSoldier = object(soldier)
woundedSoldier.a = woundedSniper.a

你看见了吗? 您直接从另一个对象偷走了一个功能! 尝试使用常规的面向对象语言进行操作。 我赌你! (请注意:这是一种实际的胆识。如果有人做到了,我不会感到惊讶,但是如果它是如此简单或如此漂亮,我会感到惊讶。)现在是时候来看一下object()函数了。

幕后

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}

object()接受一个参数( o ,或者我们传统上称为父对象的参数)。 它创建一个虚拟的“类”函数F() ,并带有一个空的构造函数。 它将类的prototype属性设置为您传入的参数。然后,它返回哑类的实例。 道格拉斯·克罗克福德object(o) Douglas Crockford) 正式认可类系统上的object(o)方法 。 实际上,这就是我们获得功能的地方。

prototype属性

您留下的一个挥之不去的问题是, prototype属性是什么。 也许在您自己的代码中有用吗? 如果在创建狙击手时将F.prototype打印到控制台,则会得到以下输出:

{ a: [Function], b: [Function], x: [Function], r: [Function] }

您将认识到这些是我们从各种按钮到功能的映射。 这是可以预期的,因为我们已经将士兵对象分配给了伪类F的原型。 但是,我们然后创建虚拟类的实例并将其分配给狙击手。 如果您询问狙击手的原型是什么,则会得到undefined 。 发生了什么? 当您按下X按钮时,狙击手怎么知道该怎么办?

更奇怪的是,当您创建woundedSniperF的原型仅返回到R按钮的映射。 但是,很明显, woundedSniper可以重装并猛击。 我们如何解释所有这些?

__proto__

令人困惑的是,有两个原型属性。 调用构造函数时, __proto__ prototype分配给__proto__属性。 sniper.__proto__返回以下内容:

{ a: [Function], b: [Function], x: [Function], r: [Function] }

更加有趣的是, sniper.__proto__拥有自己的__proto__sniper.__proto__.__proto__提供了soldierObject继承的功能。 同时, prototype仅在构造函数中使用,以在实际对象上设置__proto__属性。

结论

通过使用object(o)函数和对__proto__的发现,我们看到原型代码可以完成与经典的面向对象代码一样的工作。 动态定义唯一实例的能力以及随时将函数直接从一个实例转移到另一个实例的能力使它最终变得更强大。

进一步阅读

本文的部分灵感来自Steve Yegge的博客文章/书 。 阅读该文章,并关注即将发表的关于CoffeeScript如何利用JavaScript原型创建传统类系统的文章。