隐藏诸多细节的构造函数


如果你有以下创建对象的需求:

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

var p1 = {
    name     : 'Justin', 
    age      : 35,
    toString : toString
};

var p2 = {
    name     : 'Monica', 
    age      : 32,
    toString : toString
};

var p3 = {
    name     : 'Irene', 
    age      : 2,
    toString : toString
};

console.log(p1.toString());  // [Justin,35] 
console.log(p2.toString());  // [Monica,32] 
console.log(p3.toString());  // [Irene,2]

这些对象在创建时,具有相同的特性名称,只不过特性值不同,其实你如下定义Person函数:

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

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

var p1 = new Person('Justin', 35);
var p2 = new Person('Monica', 32);
var p3 = new Person('Irene', 2);

接着如下调用Person,就可以有相同的效果:

var p1 = new Person('Justin', 35);
var p2 = new Person('Monica', 32);
var p3 = new Person('Irene', 2);

console.log(p1.toString());  // [Justin,35] 
console.log(p2.toString());  // [Monica,32] 
console.log(p3.toString());  // [Irene,2]

Person这样的函数,接在new之后使用时,俗称为构造函数(Constructor),通常对从类为基础的语言过来的人,也会说这就像是一个类(Class),不过这只是比拟,实际上与它并不是类。

实际上使用new运算符后接上一个函数时,一部份是在作以下的动作:

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

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

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

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

这也说明了,为什么使用new接上函数,返回的对象会有nameage,因为Person中,this参考的就是p所参考的对象,所以在this上新增特性,就相当于在p所参考对象上新增特性。

说是一部份作了这些动作,不过还有别的细节,像是原型继承以及constructor特性的指定等,不然的话,你其实大可以如下定义就好了:

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

function person(name, age) {
    return {
        name     : name,
        age      : age,
        toString : toString
    };
}

var p = person('Justin', 35);

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

原型继承会在另一篇文件中说明,稍后则就会看到constructor的说明。

一个函数作为构造函数使用时,基本上无需编写return,如果构造函数有返回值,那返回值就会被当作new XXX(...)的结果。例如:

function Nobody()  {
}

function Person(name, age)  {
    return [];
}

var n = new Nobody();
var p = new Person();

console.log(n instanceof Nobody);  // true
console.log(p instanceof Person);  // false
console.log(p instanceof Array);   // true

instanceof可用来测试对象是否由经由某个构造函数new出来,由于实际上Person中定义了return []new Person()返回的是[],因此instanceof测试结果并不是Person构造的实例。

每个透过new构造的对象,都可以使用constructor特性,参考至当初构造它的函数,这是因为函数本身的prototype上会有个constructor,指向函数本身。例如:

function Person() {}
var p = new Person();
console.log(p.constructor === Person);                  // true
console.log(Person.prototype.constructor === Person);   // true

虽然这可以作为判断对象类型的参考依据之一,不过要注意的是,constructor是可以修改的,因而并不可靠,instanceof也不是使用constructor来判断对象是否为某构造函数的实例,而是根据对象的原型对象,这之后会有另一篇文章来探讨。

由于透过构造函数所创建的对象,所有的特性都是直接新增在对象上,也因此可以直接透过 . 运算符加以访问。例如:

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

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

console.log(p.name);  // Justin
console.log(p.age);   // 35

对熟悉面向对象私有(private)特性的人来说,可能觉得这不安全,这相当于在面向对象观念中,每个类成员都是公开成员的意味。JavaScript 本身并没有支持面向对象私用特性的语法,如果你想模拟,则可以如下:

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

    this.age = age;
}

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

console.log(p.name);       // undefined
console.log(p.getName());  // Justin
console.log(p.age);        // 35

以上假设的是,name不可以被设定,但可以透过getName来获取,之所以会有这样的效果,其实就是 Closure 的作用。上例中,在对象上新增了getName特性,参考至一个函数,该函数形成 Closure 绑定了参数name,参数也就是局部变量,并非对象上的特性,所以无法透过.运算符获取,因此模拟了私用特性。

由于 Closure 绑定的是变量本身,所以也可以如下,在设定值(或获取值)时予以保护:

function Account() {
    var balance = 0;

    this.getBalance = function() {
        return balance;
    };

    this.setBalance = function(money) {
        if(money < 0) {
            throw new Error('can\'t set negative balance.');
        }
        balance = money;
    };
}

var acct = new Account();

console.log(acct.getBalance());   // 0

acct.setBalance(1000);
console.log(acct.getBalance());   // 1000

acct.setBalance(-1000);           // Error: can't set negative balance

构造函数还有一些细节需要了解,这会在下一篇文件中继续讨论。


展开阅读全文