当你在JavaScript中定义一个函数的同时也会生成一些内置的属性;其中之一就是原型(prototype).在这片文章中,我会详细解释什么是原型,以及为什么你应该在你的项目中使用它。
什么是原型?
原型属性被初始化为一个空的对象,并且可以向其中添加成员 - 就像你给其他对象添加的一样。
var myObject = function(name){
this.name = name;
return this;
};
console.log(typeof myObject.prototype); // object
myObject.prototype.getName = function(){
return this.name;
};
在上面这个代码片段中,我们创建了一个函数,但是如果你调用myObject(),它会简单的返回window对象,因为它被定义在了全局作用域中。因此当它没有被实例化时,this就会返回全局对象。
console.log(myObject() === window); // true
隐秘的连接
每个JavaScript对象都拥有一个“隐秘的”属性
在我们继续前,我想论述一下让prototype之所以能这样运作的“隐秘”连接。
当一个JavaScript对象被定义或者初始化时,一个叫_proto_
的“隐秘”属性就被添加到了上面;原型链就是这样被访问到的。但是,因为并不是所有的浏览器都支持,所以尝试去访问_proto_
并不是一个好主意。
请不要将对象中_proto_
属性和prototype
对象搞混淆了,因为它们是两个完全独立的属性;也就是说,它们俩是分别协作的。第一次接触它们可能会让人困惑,但是进行这种区分是很重要的!那这到底意味着什么?请让我接着解释。当我们创建myObject
函数时,我们定义了一个类型为Function
的对象。
console.log(typeof myObject); // function
对于那些不知道的朋友,Fuction
是一个内置的JavaScript对象,正因如此,它有一些自己的属性(e.g. length and arguments)
和函数(e.g. call and apply)
。并且,是的,它也有自己的prototype对象和一个“隐秘的”_proto_
连接。这意味着在JavaScript执行引擎中的某个地方,存在着类似下面几行的代码。
Function.prototype = {
arguments: null,
length: 0,
call: function(){
// secret code
},
apply: function(){
// secret code
}
...
}
事实上,那些代码可能不会如此的简单;这只不过是去说明原型链是怎样运作的。
现在我们已经将myObject
定义为了一个函数,并且给了它一个参数,name
;但是我们还没有给它设置任何属性,比如说length
或者别的方法,比如说call
,那为什么下面的代码可以正常运行呢?
console.log(myObject.length); // 1 (being the amount of available arguments)
这是因为,当我们定义了myObject
的时候,它创建了一个_proto_
属性,并且将它的值设置为了Function.prototype(在上面的代码中说明了)。所以,当我们访问myObject.length
时,它会去寻找myObject
对象的length
属性,但是没有找到它;于是它就沿着通过_proto_link
这个链条,找到了这个属性然后返回了它。
你可能在想为什么length
会被设置为1
而不是0
- 或者其他任何的数字呢?这是因为myObject
事实上是Function
的一个实例。
console.log(myObject instanceof Function); // true
console.log(myObject === Function); // false
另外,当你创建一个新的Function
对象时,Functino
构造器中内置的一些代码会对参数进行计数,并且依据它去更新this.length
。也就是说,在这个例子中它是1
。
但是如果,我们用关键字new
创建了一个myObject
的新实例,_proto_
将会指向myObject.prototype
因为myObject
就是我们新实例的构造器。
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
正如你能想到的,这可以让我们非常的方便地设计对象,同时根据需要创建足够多的实例。- 也就是我下一个话题要说的!
为什么用prototype更好
假如说,我们要开发一个canvas游戏,这个游戏需要很多(也有可能很多很多)对象。每个对象都需要它们自己的属性,比如说x
和y
坐标,width
和height
,还有其他的属性。
我们可能会像下面这样写:
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);
}
...
};
…像这样又写了98遍…
这样做将会在内存中创建所有这些对象 - 函数全部都是独立声明的,比如说draw
和其他任何所需要的方法。这当然不是我们想要的,因为这样会使浏览器给JavaScript的内存膨胀以至于让它运行得非常慢… 或者干脆直接停止响应。
然而如果只有100个对象,那这也不会发生。不过这样做仍旧造成了不小的性能损失,因为它需要去寻找100个不同的对象,而不是简单的一个prototype
对象。
怎样使用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);
}
};
我们可以接着这样初始化GameObjcet
对象100次
var x = 100,
arrayOfGameObjects = [];
do {
arrayOfGameObjects.push(new GameObject(10, 10));
} while(x--);
现在我们有了一个包含100个GameObjects
实例的数组,它们都共享一个同样的prototype
以及对draw
方法的定义,这样我们就大大地节约了应用对内存的使用。
当我们调用draw
方法时,它会准确地引用同样的方法。
var GameLoop = function() {
for(gameObject in arrayOfGameObjects) {
gameObject.draw();
}
};
prototype是一个动态的对象
每个对象的prototype都是一个动态的对象。简单地说,这意味着如果当我们创建了GameObject
实例后,我们决定不画矩形了,却而代之的是一个圆,我们可以相应地更新我们的GameObject.prototype.draw
方法
GameObject.prototype.draw = function() {
myCanvasContext.arc(this.x, this.y, this.width, 0, Math.PI*2, true);
}
那么现在,所有之前的GameObject
实例和任何今后的实例都会画圆。
更新内置对象的prototype
是的,这是完全可能的。你可能对JavaScript库很熟悉,比如说Prototype,正是利用了这种方法。
让我们来看一个简单的小例子:
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, ‘’);
};
我们现在可以通过任何字符串使用这个方法:
“ foo bar “.trim(); // “foo bar”
然而,这样做有一个不太好的地方。举个例子,你可能会在你的应用中这么做;然而,在未来的一或者两年里,浏览器可能会实现或者更新JavaScript里String.prototype
的trim
。那样就将会使你定义的trim
覆盖掉内置的trim
!为了避免这样的情况发生,我们可以在定义函数前添加一个简单的检查代码。
if(!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, ‘’);
};
}
现在,如果内置方法中已经存在trim
,那么将会使用内置的trim
方法。
根据经验法则,通常会避免对内置对象进行拓展。但是,如果我们需要,任何法则都是可以被打破的
小结
希望这篇文章能展现出些许JavaScript的核心内容 – 也就是prototype。快去创造更加高效的应用吧!
如果你有任何关于prototype的问题,请给我留言,我会竭尽所能回答它们。
原文
http://code.tutsplus.com/tutorials/prototypes-in-javascript-what-you-need-to-know–net-24949
如有翻译错误,请给我留言,谢谢。