本文也许能帮助你揭开JavaScript原型概念的神秘面纱。开始看到的是原型怎样让一个对象从其他对象中继承功能,然后看到怎样结合 new 操作符和构造函数来构建对象。这里所提到的,只是开启对象原型力量和灵活性的第一步,你自己发现学习有关原型和JavaScript语言的新信息才最重要。
有关对象和类
JavaScript 中全是对象,这指的是传统意义上的对象,也就是“一个包含了状态和行为的单一实体”。例如,JavaScript 中的数组是含有数个值,并且包含 push、reverse 和 pop 方法的对象。
var myArray = [1, 2];
myArray.push(3);
myArray.reverse();
myArray.pop();
var length = myArray.length;
现在问题是,push 这样的方法是从何而来的呢?我们前面提到的那些静态语言使用“类语法”来定义对象的结构,但是 JavaScript 是一个没有“类语法”的语言,无法用 Array“类”的语法来定义每个数组对象。而因为 JavaScript 是动态语言,我们可以在实际需要的情况下,将方法任意放置到对象上。例如下面的代码,就在二维空间中,定义了用来表示一个点的点对象,同时还定义了一个 add 方法。
var point = {
x: 10,
y: 5,
add: function(otherPoint) {
this.x += otherPoint.x;
this.y += otherPoint.y;
}
};
但是上面的做法可扩展性并不好。我们需要确保每一个点对象都含有一个 add 方法,同时也希望所有点对象都共享同一个 add 方法的实现,而不是这个方法手工添加每一个点对象上。这就是原型发挥它作用的地方。
有关原型
在 JavaScript 中,每个对象都保持着一块隐藏的状态—— 一个对另一个对象的引用,也被称作原型。我们之前创建的数组引用了一个原型对象,我们自行创建的点对象也是如此。上面说原型引用是隐藏的,但也有 ECMAScript(JavaScript 的正式名称)的实现可以通过一个对象的__proto__属性(例如谷歌浏览器)访问到这个原型引用。
展望未来,开发者将能够使用 Object.getPrototypeOf 函数,代替__proto__属性,取得对象原型的引用。在本文写出的时候,已经可以在 Google Chrome,FIrefox 和 IE9 浏览器中使用 Object.getPrototypeOf 函数。更多浏览器在未来会实现此功能,因为它已经是 ECMAScript 标准的一部分了。我们可以使用下面的代码,来证明我们建立的 myArray 和点对象引用的是两个不同的原型对象。
Object.getPrototypeOf(point) !=Object.getPrototypeOf(myArray);
对于本文的其余部分,我将交叉使用 __proto__和Object.getPrototypeOf 函数,主要是因为 __proto__ 在图和句子中更容易识别。需要记住的是它(__proto__)不是标准,而 Object.getPrototypeOf 函数才是查看对象原型的推荐方法。
共享原型
JavaScript 中原型的真正神奇之处是多个对象如何维持对同一个原型对象的引用。例如,如果我们创建了这样的两个数组:
var myArray = [1, 2];
var yourArray = [4, 5, 6];
那么这两个数组将共享同一个原型对象,而下面的代码计算结果为 true:
Object.getPrototypeOf(myArray) ===Object.getPrototypeOf(yourArray);
如果我们引用两个数组对象上的 push 方法,JavaScript 会去寻找原型上共享的 push 方法。
JavaScript 中的原型对象提供继承功能,同时也就实现了该方法实现的共享。原型也是链式的。换句话说,因为原型对象只是一个对象,所以一个原型对象可以维持到另一个原型对象的引用。当 JavaScript 查找像 push 方法这样的成员时,它会循着原型引用链检查每一个对象,直到找到该成员,或者抵达原型链的末端。原型链为继承和共享开辟了一条灵活的途径。
有关函数
JavaScript 中的函数也是对象。这样的表述带来了几个重要的结果,而我们并不会在本文中涉及所有的事项。这其中,能将一个函数赋值给一个变量,并且将一个函数作为参数传递给另一个函数的能力构成了现代 JavaScript 编程表达的基本范式。
我们需要关注的是,函数本身就是对象,因此函数可以有自身的方法,属性,并且引用一个原型对象。让我们来讨论下面的代码的含义。
// 这将返回 true:
typeof (Array) === "function"
// 这样的表达式也是:
Object.getPrototypeOf(Array) ===Object.getPrototypeOf(function () { })
// 这样的表达式同样:
Array.prototype != null
代码中的第一行证明, JavaScript 中的数组是函数。稍后我们将看到如何调用 Array 函数创建一个新的数组对象。下一行代码,证明了 Array 对象使用与任何其他函数对象相同的原型,就像我们看到数组对象间共享相同的原型一样。最后一行代码证明了 Array 函数都有一个 prototype 属性,而这个 prototype 属性指向一个有效的对象。这个 prototype 属性十分重要。
JavaScript 中的每一个函数对象都有 prototype属性。千万不要混淆这个 prototype 属性的 __proto__ 属性。他们用途并不相同,也不是指向同一个对象。
// 返回 true
Object.getPrototypeOf(Array) !=Array.prototype
Array.__proto__ 提供的是 数组原型 – 请把它当作 Array 函数所继承的对象。
而 Array.protoype,提供的的是所有数组的原型对象。也就是说,它提供的是像 myArray 这样数组对象的原型对象,也包含了所有数组将会继承的方法。我们可以写一些代码来证明这个事实。
// true
Array.prototype ==Object.getPrototypeOf(myArray)
// 也是 true
Array.prototype ==Object.getPrototypeOf(yourArray);
基于所知道的知识,请想象创建一个新的对象,并让新对象表现地像数组的过程。一种方法是使用下面的代码。
// 创建一个新的空对象
var o = {};
// 继承自同一个原型,一个数组对象
o.__proto__ = Array.prototype;
// 现在我们可以调用数组的任何方法...
o.push(3);
虽然这段代码很有趣,也能工作,可问题在于,并不是每一个 JavaScript 环境都支持可写的 __proto__ 对象属性。幸运的是,JavaScript 确实有一个创建对象内建的标准机制,只需要一个操作符,就可以创建新对象,并且设置新对象的 __proto__ 引用– 那就是“new”操作符。
var o = new Array();
o.push(3);
JavaScript 中的 new 操作符有三个基本任务。首先,它创建新的空对象。接下来,它将设置新对象的 __proto__ 属性,以匹配所调用函数的原型属性。最后,操作符调用函数,将新对象作为“this”引用传递。如果要扩展最后两行代码,就会变成如下情况:
var o = {};
o.__proto__ = Array.prototype;
Array.call(o);
o.push(3);
函数的 call 方法允许你在调用函数的情况下在函数内部指定“this”所引用的对象。当然,函数的作者在这种情况下需要实现这样的函数。一旦作者创建了这样的函数,就可以将其称之为构造函数。
构造函数
构造函数和普通的函数一样,但是具有以下两个特殊性质。通常构造函数的首字母是大写的(让识别构造函数变得更容易)。构造函数通常要和 new 操作符结合,用来构造新对象。Array 就是一个构造函数的例子。Array 函数需要和 new 操作符一起使用,而且 Array 的首字母是大写的。JavaScript 将 Array 作为内置函数包括在内,而任何人都可以写出自己的构造函数。事实上,我们最后可以为先前创建的点对象编写出构造函数。
var Point = function (x, y) {
this.x = x;
this.y = y;
this.add = function (otherPoint) {
this.x += otherPoint.x;
this.y += otherPoint.y;
}
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);
在上面的代码中,我们使用了 new 操作符和 Point 函数来构造点对象,这个对象带有 x 属性和 y 属性和一个 add 方法。
现在的问题是我们的每个点对象中仍然有单独的 add 方法。使用我们学到的原型和继承的知识,我们更希望将点对象的 add 方法从每个点实例中转移到 Point.prototype 中。要达到继承 add 方法的效果,我们所需要做的,就是修改 Point.prototype 对象。
var Point = function (x, y) {
this.x = x;
this.y = y;
}
Point.prototype.add = function (otherPoint){
this.x += otherPoint.x;
this.y += otherPoint.y;
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);