javascript创建对象的几种模式

这篇文章将逐个介绍Javascrip创建对象的几种模式:工厂模式,构造函数模式,原型模式,动态原型模式,寄生构造函数模式,稳妥构造函数模式。

工厂模式

function createPerson( name, age, job ) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        alert(this.name);
    }

    return o;
}

var person1 = createPerson("Nicholas", 29, "software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

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

var person1 = new Person("Nicholas", 29, "software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"

和工厂模式相比,构造函数模式的不同之处在于:

1.没有显式地创建对象;

2.直接将属性和方法赋给了this对象;

3.没有return语句。

要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下四个步骤:

1.创建一个新对象;

2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象);

3.执行构造函数中的代码(为这个新对象添加属性);

4.返回新对象。

在这里《javascript高级程序设计(第二版)》中有这么一段话:

在前面例子的最后,person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person.

其实这里说的并不够准确,person1和person2这两个实例中并没有constructor属性,而是Person对象的原型中有这个属性,person1和person2中有的是_proto_属性,person1和person2只是可以通过原型链找到constructor属性,这在后面的原型模式中会提到。

这里要说的无非就是构造函数模式解决了工厂模式不能解决对象识别问题。

alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true

构造函数的问题

使用构造函数的主要问题就是,每个方法都要在每个实例上重新创建一遍。

我们可以这样:

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

function sayName() {
    alert(this.name);
}

这样的确解决了两个函数做同一件事情的问题,但新的问题又来了:在全局作用域中定义函数实际上只能被某个对象引用,这让全局作用域有点名不副实。而让人更无法接受的是:如果对象需要定义多个方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

原型模式

使用原型的好处是可以让所有对象实例共享它所包含的属性和方法。

function Person(){

}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
}

var person1 = new Person();
person1.sayName(); //"Nicholas"

var person2 = new Person();
person2.sayName(); //"Nicholas"

alert(person1.sayName == person2.sayName); //true

理解原型

无论在什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性。在默认情况下,所有prototype属性都会获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。

原型对象的问题

看下面的代码:

function Person(){

}

Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job: "software Engineer",
    friends: ["Shelby","Court"],
    sayName: function() {
        alert(this.name);
    }
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true

由于friends数组存在于Person.prototype而非person1中,所以对person1中friends的修改也会通过person2.friends反映出来。假如我们的初衷就是这样在所有实例中共享一个数组,那么对这个结果自无话可说。可是,实例一般都是要有属于自己的全部属性。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。

组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。

function Person( name, age, job ) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby","Court"];
}

Person.prototype = {
    constructor: Person,
    sayName: function(){
        alert(this.name);
    }
}

var person1 = new Person("Nicholas", 29, "software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");

alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

这种构造函数与原型混成模式,是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。

动态原型模式

function Person( name, age, job ) {
    //属性
    this.name = name;
    this.age = age;
    this.job = job;

    //方法
    if( typeof this.sayName != "function" ) {
        Person.prototype.sayName = function() {
            alert(this.name);
        }
    }
}

var person = new Person("Nicholas", 29, "software Engineer");
person.sayName();

这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。

寄生构造函数模式

function SpecialArray() {
    //创建数组
    var values = new Array();

    //添加值
    values.push.apply(values,arguments);

    //添加新方法
    values.toPipedString = function(){
        return this.join("|");
    };

    //返回数组
    return values;
}

var colors = new SpecialArray("red","blue","green");
alert(colors.toPipedString()); //"red|blue|green"

除了使用new操作符并把使用的包装函数叫做构造函数模式外,这个模式跟工厂模式一模一样。构造函数再不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加return语句,可以重写调用构造函数时返回的值。

关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型之间没有关系;不能依赖instanceof操作符来确定对象的类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

Douglas Crockford发明了Javascript中稳妥对象概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。

function Person( name, age, job ) {
    //创建要返回的对象
    var o = new Object();

    //可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function() {
        alert(name);
    };

    //返回对象
    return o;
}

var person = Person("Nicholas", 29, "software Engineer");
Person.sayName(); //"Nicholas"

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没什么关系, 因此instanceof操作符对这种对象没有意义。

参考资料:《javascript高级程序设计(第二版)》p116-131.


blog comments powered by Disqus