原创

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 这个方法,定义一个。

正文到此结束
本文目录