JavaScript设计模式:工厂模式及构造函数模式

创建对象

通过 Object 构造函数或者对象字面量都可以用来创建单个对象,但这些方法有着明显的缺点:使用同一个接口创建很多个对象,会产生大量的重复代码。为解决这样的问题,工厂模式和构造函数模式应运而生。

工厂模式

工厂模式是一种在软件工程领域内被广泛使用的一种设计模式,这种模式抽象了创建具体对象的过程。JS在 ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装以特定的接口创建对象的细节,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createPerson(name, age, job) {
const obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayPerson = function(){
console.log("Name:"+this.name+" Age:"+this.age+" Job:"+this.job);
};
return obj;
}
const person1 = createPerson("Arrow", 35, "hero");
const person2 = createPerson("Flash", 25, "superHero");

person1.sayName(); // Name:Arrow Age:35 Job:hero
person2.sayName(); // Name:Flash Age:25 Job:superHero

函数 createPerson() 能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性和一个方法的对象。工厂模式解决了创建多个相似对象的问题,但却没有解决对象识别的问题(怎样知道一个对象的类型)。

构造函数模式

可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法,将前面的例子重写成构造函的模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Person = function (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayPerson = function () {
console.log("Name:"+this.name+" Age:"+this.age+" Job:"+this.job);
};
};
const person1 = new Person("Arrow", 35, "Hero");
const person2 = new Person("Flash", 25, "SuperHero");

person1.sayPerson(); // Name:Arrow Age:35 Job:hero
person2.sayPerson(); // Name:Flash Age:25 Job:superHero

上述的例子中,Person() 函数取代了 createPerson() 函数。Person() 中的代码与 createPerson() 中的相同的部分外,还存在一下的不同之处:

  • 没有显示的创建对象;
  • 直接将属性和方法赋给了 this 对象;
  • 没有 return 语句。

要创建 Person 的新实例,必须要使用 new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
1、创建一个新对象;
2、将构造函数的作用域赋给新对象(因此 this 指向这个新对象);
3、执行构造函数中的代码(为这个新对象添加属性);
4、返回新对象。
在上述的例子中,person1和 person2分别保存着 Person的一个不同的实例。在这两个实例对象中都有一个 constructor(构造函数)属性,该属性指向 Person,如下代码所示:

1
2
console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

对象的constructor属性一开始是为了标识对象类型的。但是,提到类型检测时发现,通过 instanceof来检测更为可靠一点。本例子中创建的所有对象即是 Object的实例,同时也是 Person的实例,这一点可以通过 instanceof操作符来验证。

1
2
3
4
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true

创造自定义的构造函数意味着可以将他的实例标识为一种特定的类型,这也正是构造函数模式胜过工厂模式的地方。

将构造函数当作普通的函数

构造函数与普通函数的唯一区别就是其调用的方式不同。不过,构造函数也是函数,不存在定义构造函数的特殊语法。任何函数,只要其通过 new操作符来调用,那他就可以作为构造函数;同样,任何函数如果不通过 new操作符来调用,那么他和普通函数就没什么区别。例如,前面的例子可以通过以下的任一方式来调用。

1
2
3
4
5
6
7
8
9
10
//当作构造函数来调用
const person1 = new Person("Arrow", 35, "Hero");
person1.sayPerson(); //Name:Arrow Age:35 Job:Hero
//作为普通函数来调用
Person("SuperGirl", 22, "femaleHero");
window.sayPerson(); //Name:SuperGirl Age:22 Job:femaleHero
//在另一个对象的作用域中调用
const o = new Object();
Person.call(o, "superMan", 28, "manHero");
o.sayPerson(); //Name:superMan Age:28 Job:manHero

构造函数存在的问题

使用构造函数的主要问题,就是每个方法都需要在每个实例上重新创建一遍。在前面的例子中,person1和 person2都用一个名为 sayPerson()的方法,但两个方法并不是同一个 Function的实例。然而,创建两个完成同样任务的 Function实例的确没有必要;何况有 this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,可以通过把函数定义转移到构造函数外部来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayPerson = sayPerson;
}
function sayPerson(){
console.log("Name:"+this.name+" Age:"+this.age+" Job:"+this.job);
}
const person1 = new Person("Arrow", 35, "Hero");
const person2 = new Person("Flash", 25, "SuperHero");

在上述的实例中,把sayPerson()函数的定义转移到了构造函数的外部。在构造函数的内部,将sayPerson的属性设置成了全局的 sayPerson()函数。由于 sayPerson包含的是一个指向函数的指针,因此 person1和 person2对象就共享了在全局作用域中定义的同一个sayPerson()函数。这样做的确解决了函数共享的问题,但出现了新的问题:如果对象需要定义许多方法,那么就要定义很多个全局函数,于是自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过原型模式来解决。