当你在JavaScript中定义一个函数,它有一些预定义的属性,其中之一就是令人迷惑的原型。本文将详细解释什么是原型,以及为什么要在项目中使用它。

什么是原型
对象初始化时原型是一个空对象,你可以将任何其他对象添加到原型上。
|
|
上面的代码中,我们创建了一个函数,如果我们调用 myObject(),它将简单的返回一个 window 对象,因为它还没有被实例化,而这个函数是在全局作用域中定义的,this 理所当然地指向了全局对象。
|
|
一个“隐秘”属性
继续之前,我想讨论一下关于原型的一个的“隐秘”属性。
JavaScript 中的每一个对象在被定义或实例化之后,都有一个叫做 __proto__ 的隐秘属性,这是原型链的核心。但是,我并不建议在代码中直接访问 __proto__,因为并不是所有的浏览器都支持它。
不能将 __proto__ 和对象的原型混为一谈,它们是两个不同的属性,但又紧密相关,可能初学者会感到非常困惑,很难将他们区分开来,下面我将详细道来。当我们创建 myObject 这个函数时,实际上是定义了一个 Function 类型的对象。
|
|
Function 是 JavaScript 中的一个预定义对象,它有自己的一些属性(比如 length 和 arugments) 和方法(比如 call 和 apply),还有自己的原型对象,以及“隐秘”的 __proto__ 属性。这意味着,在 JavaScript 引擎内的某个位置,可能有一些类似于下面的代码:
|
|
事实上,Function 的定义并不是如此简单,这里只是为了说明原型链的原理。
目前为止,我们定义了 myObject 这个函数,并为其指定了名为 name 的形参,但我们并没有为其设定任何属性(如 length)和方法(如 call),那么下面的代码是怎么回事呢?
|
|
这是因为在定义 myObject 这个对象时,它内置了 __proto__ 这个属性,并且其值是 Function.prototype。所以,当我们使用 myObject.length 时,首先将在 myObject 对象中查找名为 length 的属性, 没有找到,然后将通过 __proto__ 这个“隐秘”的属性查找其原型链,最后找到 length 这个属性并返回。
您可能想知道为什么 length 的值为什么是 1,而不是 0,或任何其他数字。这是因为 myObject 实际上是 Function 的一个实例。
|
|
当一个对象的实例被创建时,__proto__ 将指向构造函数的原型,在我们的例子中就是 Function 的原型。
|
|
当创建一个新的 Function 对象时,在 Function 的构造函数内将获取形参的数量,并更新 this.length 的值,在这里是 1。
如果我们使用 new 操作符创建一个 myObject 的实例,该实例的 __proto__ 将指向 myObject.prototype,因为 myObject 是该实例的构造函数。
|
|
现在 myInstance 除了可以访问 Function.prototype 中的原生方法(比如 call 和 apply)之外,还可以访问到 myObject.prototype 中的方法: getName。
|
|
译者注:一个对象实际上包含 __proto__ 和 prototype 两个属性,这两个属性代表着不一样的东西,__proto__ 指向创建该对象的构造函数的原型,原型链查找就是借助于 __proto__ 来实现;而 prototype 指向该对象自身的原型。
可以想象,这是非常方便的,我们可以用它来获取一个对象的结构,并根据需要创建实例,让我们开始讨论下一个话题。
为什么要使用原型
我们先来看一个实例,现在我们需要开发一个 canvas 上的游戏,需要在 canvas 一次性绘制一些(可能是数百个)对象,每个对象都包含一些自己的属性,比如 x 和 y 坐标、width、height等等。
我们可以这样做:
|
|
… 重复做 98 次 …
这将在内存中创建所有这些对象,这些对象都有单独的方法,比如 draw 和其他一些所需要的方法。这当然会很糟糕,因为这个游戏很可能将占光浏览器内存,并运行的非常缓慢,甚至停止响应。
虽然只有 100 个对象的时候这还不可能发生,但会对性能造成很大的影响,因为它需要查找一百个不同的对象,而不是一个相同的原型对象。
如何使用原型
为了使我们的应用运行的更快,遵循最佳实践,我们来重新定义 GameObject 的原型,GameObject 对象的每一个实例将使用 GameObject.prototype 中的方法,就像它们自身的方法一样。
|
|
然后,我们来实例化 100 个 GameObject 对象:
|
|
现在,我们有了一个有 100 个 GameObjects 实例的数组,这些实例对象共享相同的原型,这大大节省了应用所占用的内存。
当我们调用 draw 方法时,实际上调用的都是原型上相同的方法。
|
|
原型是活动对象(Live Object)
对象的原型是一个活动对象,什么意思呢?根据上面示例来说就是,当我创建 GameObject 对象的实例之后,我们可以修改 GameObject.prototype.draw 方法,来画一个圆,而不是画一个矩形,这样调用所有已经实例化的对象或后面再实例化的对象中的 draw 方法就会画一个圆。
|
|
修改内置对象的原型
你可能熟悉一些 JavaScript 库,比如 Prototype,他们都充分利用了这种方法。
来看一个简单的例子:
|
|
现在我们可以在任何字符串上使用该方法:
|
|
不过这样做也有一定的缺点。比如,你将这个方法应用到你的代码中,也许一两年之后,JavaScript 可能会在 String 的原型中实现了该方法,这意味着你的方法将覆盖 JavaScript 的原生方法。为了避免这种情况,我们需要在定义自身的方法前,做一个简单的判断:
|
|
如果存在原生的 trim 方法,我们就会使用原生的 trim 方法。
根据经验法则,通常也被认为是最佳实践,最好避免扩展内置对象的原型。但是,如果有必要,也可以不遵循这个规则。
总结
希望本文已经阐释清楚了 JavaScript 中的原型,现在你应该能够编写更加高效的代码了。
如果你有关于原型的任何问题,你可以写在评论中,我会尽力解答。
英文原文:Leigh Kaszick,翻译:布谷 bubkoo
