JavaScript 的原型对象 Prototype
当您在JavaScript中定义函数时,它会附带一些预定义的属性; 其中之一是虚幻的原型。 在本文中,我将详细说明它是什么,以及为什么要在项目中使用它。
1. 什么是Prototype?
原型对象 prototype最初是一个空对象,可以添加成员 - 就??像其他对象一样。
var myObject = function( name ) {
this.name = name;
return this;
};
console.log(typeof myObject.prototype); // object
myObject.prototype.getName = function() {
return this.name;
};
上面的代码创建了一个函数,然后赋值给 myObject。如果我调用 myObject(),它将返回 window 对象。因为它是在全局作用域内定义的,而且它还没有被实例化,所以 this 直接指向全局对象:
console.log(myObject() === window); // true
2. 原型链
avaScript 中定义或实例化任何一个对象的时候,它都会被附加一个名为 proto 的隐藏属性,原型链正是依靠这个属性才得以形成。但是千万别直接访问 proto 属性,因为有些浏览器并不支持直接访问它。
另外proto 和 对象的 prototype 属性也不是一回事,它们各自有各自的用途。而且,他们是携手工作的!
怎么理解呢?其实,当我们创建 myObject 函数时,实际上是创建了一个 Function 类型的对象:
console.log(typeof myObject); // function
这里要说明一下,Function 是 JavaScript 中预定义的一个对象,所以它也有自己预定义的属性(如 length 和 arguments)和方法(如 call 和 apply),当然也有 proto,以此实现原型链。也就是说,JavaScript 引擎内可能有类似如下的代码片段:
Function.prototype = {
arguments: null,
length: 0,
call: function() {
// secret code
},
apply: function(){
// secret code
},
...
};
事实上,JavaScript 引擎代码不可能这样简单,这里只是描述一下原型链是如何工作的。
我们定义了一个函数 myObject,它还有一个参数 name,但是并没有给它任何其它属性,例如 length 或者其它方法,如 call。那么下面这段代码为啥能正常执行呢?
console.log(myObject.length); // 结果:1,是参数的个数
这是因为我们定义 myObject 时,同时也给它定义了一个 proto 属性,并赋值为 Function.prototype(参考前面的代码片段),所以我们能够像访问其它属性一样访问 myObject.length,即使我们并没有定义这个属性,因为它会顺着proto 原型链往上去找 length,最终在 Function 里面找到了。
那为什么找到的 length 属性的值是 1,而不是 0 呢,是什么时候给它赋值的呢?由于 myObject 是 Function 的一个实例:
console.log(myObject instanceof Function); // true
console.log(myObject === Function); // false
当实例化一个对象的时候,对象的 proto 属性会被赋值为其构造者的原型对象,在本示例中就是 Function,此时构造器回去计算参数的个数,改变 length 的值。
console.log(myObject.__proto__ === Function.prototype); // true
而当我们用 new 关键字创建一个新的实例时,新对象的 proto 将会被赋值为 myObject.prototype,因为现在的构造函数为 myObject,而非 Function。
var myInstance = new myObject('foo');
console.log(myInstance.__proto__ === myObject.prototype); // true
新对象除了能访问 Function.prototype 中继承下来的 call 和 apply 外,还能访问从 myObject 中继承下来的 getName 方法:
console.log(myInstance.getName()); // foo
var mySecondInstance = new myObject('bar');
console.log(mySecondInstance.getName()); // bar
console.log(myInstance.getName()); // foo
其实这相当于把原型对象当做一个蓝本,然后可以根据这个蓝本创建 N 个新的对象。
3. 为什么使用Prototype更好?
比方说,我们正在开发一个canvas游戏,同时在屏幕上需要几个(可能数百个)对象。每个对象都需要自己的属性,如x和y坐标,宽度,高度等等。
我们可能需要这么做
var GameObject1 = {
x: Math.floor((Math.random() * myCanvasWidth) + 1),
y: Math.floor((Math.random() * myCanvasHeight) + 1),
width: 10,
height: 10,
draw: function(){
myCanvasContext.fillRect(this.x, this.y, this.width, this.height);
}
...
};
var GameObject2 = {
x: Math.floor((Math.random() * myCanvasWidth) + 1),
y: Math.floor((Math.random() * myCanvasHeight) + 1),
width: 10,
height: 10,
draw: function(){
myCanvasContext.fillRect(this.x, this.y, this.width, this.height);
}
//... do this 98 more times ...
这将创建内存中的所有这些对象 - 所有这些对象都使用单独的绘制和任何其他可能需要的方法定义。这当然是不理想的,因为JavaScript会消耗浏览器内存,并使其运行非常缓慢,甚至停止响应。
虽然有时候可能不会有100个对象,但是仍然很致命的是,它将需要查找一百个不同的对象,而不仅仅是单个Prototype。
4. 如何使用Prototype
为了使应用程序运行得更快(并遵循最佳实践),我们可以(重新)定义GameObject的Prototype原型属性; GameObject的每个实例都将引用GameObject.prototype中的方法,就像它们是自己的方法一样。
// define the GameObject constructor function
var GameObject = function(width, height) {
this.x = Math.floor((Math.random() * myCanvasWidth) + 1);
this.y = Math.floor((Math.random() * myCanvasHeight) + 1);
this.width = width;
this.height = height;
return this;
};
// (re)define the GameObject prototype object
GameObject.prototype = {
x: 0,
y: 0,
width: 5,
width: 5,
draw: function() {
myCanvasContext.fillRect(this.x, this.y, this.width, this.height);
}
};
然后我们可以把GameObject实例化100次
var x = 100,
arrayOfGameObjects = [];
do {
arrayOfGameObjects.push(new GameObject(10, 10));
} while(x--);
现在我们有一个100个GameObjects的数组,它们都共享了draw方法的相同Prototype和定义,它大大地节省了应用程序中的内存。
当我们调用draw方法的时候,它将会指向一个相同的Function
var GameLoop = function() {
for(gameObject in arrayOfGameObjects) {
gameObject.draw();
}
};
5. Prototype 是一个动态的对象
对象的原型是一个动态的对象,这意味着,如果在创建了所有的GameObject实例之后,我们决定,而不是绘制一个矩形,我们要绘制一个圆,我们可以相应地更新我们的GameObject.prototype.draw方法。
GameObject.prototype.draw = function() {
myCanvasContext.arc(this.x, this.y, this.width, 0, Math.PI*2, true);
}
而现在,所有以前的GameObject和任何未来的实例都会画一个圆。
6. Prototype 的典型示例
用过 jQuery 或者 Prototype 库的朋友可能知道,这些库中通常都会有 trim 这个方法。
示例
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, '');
};
trim 用法
foo bar '.trim(); // 'foo bar'
但是这样做又有一个缺点,因为比较新版本的浏览器中的 JavaScript 引擎在 String 对象中本身就提供了 trim 方法, 那么我们自己定义的 trim 就会覆写它自带的 trim。其实,我们在定义 trim 方法之前,可以做个简单的检测,看是否需要自己添加这个方法:
if(!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, '');
};
}
检查一下,如不存在 trim 这个方法,定义一个。