【翻译】JavaScript中的原型对象prototype

在 JavaScript 中,每定义一个对象的时候,对象都会带有一些预定义好的属性,其中之一便是 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() 的时候,它将会返回 window 对象,因为其是在全局范围定义的,没有被实例化,所以 this 返回全局对象。

console.log(myObject() === window); // true

__proto__

在 JavaScript 中,当定义或实例化任何一个对象的时候,都会为其附加一个隐藏属性 __proto__ ,但是千万不要直接访问 __proto__ 属性,因为有些浏览器并不支持直接访问它。并且,__proto__prototype 也不是一回事。

当创建 myObject 函数的时候,我们定义了一个类型对象 Function

console.log(typeof myObject); // function

Function 是 JavaScript 中的预定义对象,并且其拥有自己的属性(如 lengthargument )和方法(如 callapply )。同样,它也拥有自己的原型对象以及隐藏属性 __proto__ ,这就意味着,在 JavaScript 中,可能会有如下形式的代码:

Function.prototype = {
    arguments: null,
    length: 0,
    call: function(){
        // secret code
    },
    apply: function(){
        // secret code
    }
    ...
}

当然,肯定没有这么简单,这只是说明原型链是如何工作的。

当定义 myObject 函数,并且只为其添加参数 name ,但是却可以访问它的 length 或其它方法

console.log(myObject.length); // 1 (being the amount of available arguments)

这是为什么呢?当我们定义 myObject 函数的时候,它就创建了一个 __proto__ 属性并且将值设置为 Function.prototype 。所以,当我们访问 myObject.length 的时候,它先会在 myObject 中查找 length ,没有找到,然后继续沿着 __proto__ 原型链向上查找,最后再 Function 中找到并返回它。

你可能想知道为什么 length 为 1 却不为 0 ,或者是其他任何数字。这是因为 myObject 事实上是 Function 的一个实例。

console.log(myObject instanceof Function); // true
console.log(myObject === Function); // false

当创建对象的实例时,__proto__ 属性被更新为指向构造函数的原型,在这种情况下,原型是 Function

console.log(myObject.__proto__ === Function.prototype) // true

另外,当创建一个新的 Function 对象时,构造函数中的本地代码 Function 将计算参数的数量,并相应地更新 this.length,在这种情况下,它是 1。

如果我们使用 new 关键字创建的新实例 myObject 的时候,新对象的 __proto__ 将赋值为 myObject.prototype ,因为现在的构造函数为 myObject ,而非 Function

var myInstance = new myObject(“foo”);
console.log(myInstance.__proto__ === myObject.prototype); // true

新对象除了能访问 Function.prototype 中继承下来的 callapply 外,还能访问从 myObject 中继承下来的 getName 方法:

console.log(myInstance.getName()); // foo

var mySecondInstance = new myObject('bar');

console.log(mySecondInstance.getName()); // bar
console.log(myInstance.getName()); // foo

其实这相当于把原型对象当做一个蓝本,然后可以根据这个蓝本创建 N 个新的对象。

为什么要使用原型

比如说,我们正在开发一个画布游戏,需要有几个(可能几百)对象。每个对象都需要自己的属性,如 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 等等其他可能需要的方法。这种方式当然不可取,因为浏览器可能会为每一个对象创建内存分配而运行缓慢或者是直接崩溃。

那么,如何使用 prototype 呢?

为了使程序运行更快,我们可以重新定义原型属性 GameObject,每一个 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 方法的定义,从而大大节省了应用程序中的内存。

当我们调用该draw方法时,它将引用完全相同的函数(即它们所共有的函数)。

var GameLoop = function() {
    for(gameObject in arrayOfGameObjects) {
        gameObject.draw();
    }
};

更新当前的原型对象

你可能用过类似的 JavaScript 库,如 Prototype ,它便用的是如下方式。

可以看下这个简单示例:

String.prototype.trim = function() {
    return this.replace(/^\s+|\s+$/g, ‘’);
};

然后,就可以这样使用这个方法

" foo bar   ".trim(); // "foo bar"

当然,这样写还是有一些小缺点,在低版本的浏览器中,使用该方法是没有问题的,但是在比较新的浏览器中的 JavaScript 引擎已经实现了trim 方法,那么我们自己写的就会覆盖掉本身已经实现的,所以在自己定义 trim 方法之前,可以做一个检测。

if(!String.prototype.trim) {
    String.prototype.trim = function() {
        return this.replace(/^\s+|\s+$/g, ‘’);
    };
}

参考文章:Prototypes in JavaScript

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注