函数 prototype 特性


在〈隐藏诸多细节的构造函数〉中看过一个例子:

function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.toString = toString;
}

这可以解决重复创建函数实例的问题,但在全局范围(对象)上多了个toString名称,虽然可以如下避免这个问题:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.toString = function() {
        return '[' + this.name + ', ' + this.age + ']';
    };
}

Person函数中使用了函数字面量创建了函数实例,并指定给toString特性,不过每次调用构造函数时,都会创建一次函数实例。

如果你知道函数在定义时,都有个prototype特性,则可以如下:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p1 = new Person('Justin', 35);
var p2 = new Person('Momor', 32);

console.log(p1.toString());   // [Justin, 35]
console.log(p2.toString());   // [Momor, 32]

使用new关键字时,JavaScript 会先创建一个空对象,接着设定对象的原型为函数的prototype特性所参考的对象,然后调用构造函数并将所创建的空对象设为this

JavaScript 在寻找特性名称时,会先在实例上找寻有无特性,以上例而言,p1上会有nameage特性,所以可以直接获取对应的值。如果对象上没有该特性,会到对象的原型上去寻找,以上例而言,p1上没有toString特性,所以会到p1的原型上寻找,而p1的原型对象此时也就是Person.prototype参考的对象,这个对象上有toString特性,所以可以 找到toString所参考的函数并执行。

如果使用 ECMAScript 5,可以透过Object.getPrototypeOf来获取实例的原型对象。例如:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p = new Person('Justin', 35);

console.log(Person.prototype === Object.getPrototypeOf(p));   // true

Node.js、Nashorn 中,对象都有个「非标准」特性__proto__,许多浏览器也支持这个特性,可以设定或获取实例创建时被设定的原型对象,然而在 ECMAScript 6 规范,__proto__被加入了附录,规范浏览器中必须支持这个特性,然而其他环境中不一定,一般的建议是避免使用这个特性。

然而,若要用模拟的方式来说明new Person('Justin', 35)时作了什么事,大概像是这样:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p = {};
p.__proto__ = Person.prototype;
Person.call(p, 'Justin', 35);

console.log(p.toString());         // [Justin,35]
console.log(p instanceof Person);  // true

要注意的是,只有在查找特性,而对象上不具该特性时才会使用原型,如果你对对象设定某个特性,是直接在对象上设定了特性,而不是对原型设定了特性。例如:

function Some() {}
Some.prototype.data = 10;

var s = new Some();
console.log(s.data);                 // 10

s.data = 20;
console.log(s.data);                 // 20
console.log(Some.prototype.data);    // 10

在上例中可以看到,你对s参考的对象设定了data特性,但并不影响Some.prototype.data的值。

你可以在任何时间点对函数的prototype新增特性,由于原型查找的机制,透过函数而构造的所有实例,都可以找到该特性,即使实例创建之后,特性才被添加到原型中。例如:

function Some() {}

var s = new Some();
console.log(s.data);       // undefined

Some.prototype.data = 10;
console.log(s.data);       // 10

先前在谈构造函数时有提过,每个透过new构造的对象,都会有个constructor特性,参考至当初构造它的函数。事实上,每个函数实例创建时,都会在函数实例上以空对象创建prototype,然后在空对象上设定constructor特性,也因此每个new构造的对象,都可以找到constructor特性。例如:

function Some() {}
console.log(Some.prototype.constructor);  // [Function: Some]

每个函数实例,其prototype特性默认参考至Object的实例,根据原型寻找原则,查找特性时若prototype上找不到,由于prototypeObject实例,也就是prototype的原型对象默认是参考至Object.prototype,所以又会到Object.prototype上寻找,如果找到就使用,如果没有找到就是undefined,这就是 JavaScript 的原型链寻找特性机制。

例如:

Object.prototype.xyz = 10;

function Some() {}

var s = new Some();
console.log(s.xyz); // 10

console.log(Object.getPrototypeOf(s) === Some.prototype);          // true

var protoOfS = Object.getPrototypeOf(s);
console.log(Object.getPrototypeOf(protoOfS) === Object.prototype); // true

实例的原型对象,默认就是构造函数的prototype参考的对象。虽然Some实例或Some.prototype都没有定义xyz,但根据原型链查找,最后在Object.prototype可以找到xyz(并不建议在Object.prototype上添加特性,因为这会影响所有JavaScript 的实例,这边只是为了示范原型链查找)。

你也可以使用isPrototypeOf来确定对象是否为另一对象的原型。例如:

console.log(Array.prototype.isPrototypeOf([]));              // true
console.log(Function.prototype.isPrototypeOf(Array));         // true
console.log(Object.prototype.isPrototypeOf(Array.prototype)); // true

for in在枚举对象特性时,会循着原型链一路找出所有可枚举特性。

如果要创建一个实例,想要令其循着某个原型链查找,例如,想要创建一个类似数组的对象,但要其可循着Array原型链查找,以利用Array定义的特性,若使用非标准__proto__特性的话,可以如下:

var arrayLike = {
    '0' : 10,
    '1' : 20,
    '2' : 30,
    length : 3
};

arrayLike.__proto__ = [];

arrayLike.map(function(elem) {
              return elem * 10
          })
          .forEach(function(elem) {
              console.log(elem);
          });

ES5 或更早前,__proto__终究是非标准特性,然而,即使是 ECMAScript 5 也只有提供Object.getPrototypeOf,没有可设置原型对象的方式,在《JavaScript: The Good Parts》的〈3.5. Prototype〉中提出可自定义一个Object.beget函数来解决这个问题:

Object.beget = function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};

var arrayLike = Object.beget(Array.prototype);
arrayLike[0] = 10;
arrayLike[1] = 20;
arrayLike[2] = 30;
arrayLike.length = 3;

arrayLike.map(function(elem) {
              return elem * 10
          })
          .forEach(function(elem) {
              console.log(elem);
          });

ECMAScript 5 中包括了一个Object.create函数,可达到相同的目的:

var arrayLike = Object.create(Array.prototype, {
    '0'    : {value : 10},
    '1'    : {value : 20},
    '2'    : {value : 30},
    length : {value : 3}
});

arrayLike.map(function(elem) {
              return elem * 10
          })
          .forEach(function(elem) {
              console.log(elem);
          });

Object.create第一个参数接受原型对象,第二个参数接受描述器(Descriptor),其内部大致是做了以下这些事(Ben Newman 写的范例):

Object.create = function(proto, props) {
    var ctor = function(ps) {
        if(ps) {
            Object.defineProperties( this, ps );
        }
    };
    ctor.prototype = proto;
    return new ctor(props);
};

因此,作为一个有趣的练习,先前有个范例使用了p.__proto__ = Person.prototype这段代码,以下将之改为使用Object.create

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p = Object.create(Person.prototype);
Person.call(p, 'Justin', 35);

console.log(p.toString());         // [Justin,35]
console.log(p instanceof Person);  // true




展开阅读全文