Higanbana

创建对象(一)——工厂模式和构造函数模式

对象

我们常听到一句话:“在javascript中,一切皆是对象”。那么对象是什么呢?ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值,对象或者函数”。也就是说对象是一组没有特定顺序的值,它的每个属性或者方法都有一个名字,每个名字都映射到一个值。

创建对象有很多种方法,最原始的方法是这样的:

1
2
3
4
5
6
7
8
9
10
var person = {
name: "wanghan",
age: "20",
getName: function() {
console.log(this.name);
}
}
console.log(person.age); //20 访问属性的方式一
console.log(person['age']); //20 访问属性的方式二
person.getName(); //wanghan

也可以这样写:

1
2
3
4
5
6
var person = { };
person.name= "wanghan",
person.age= "20",
person.getName= function() {
console.log(this.name);
}

虽然这些方式都可以简单的创建一个对象,但是它们都有明显的弊端。假如说我们要创建一组相似的对象,这些对象拥有相同的属性名,只是属性值各不相同,我们用上面的办法就需要重复很多的代码,这显然是不合理的。还好我们有更机智的办法。

工厂模式

对于上面创建多个相似对象的问题,我们可以用一个函数来封装它所有的属性,这样我们只要给函数传入不同的参数(属性值),就能轻松创建出一个对象。就像工厂里加工产品一样,只要有一个模子,我们就可以复制出来无数个产品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function person(a,b) {
var o = {}; //这个对象就相当于模子
o.name = a;
o.age = b;
o.getName=function () {
console.log(this.name);
};
return o; //函数被调用时就会返回这个对象
}
var fun = person("wanghan",20);
fun.getName(); //wanghan
console.log(fun.age); //20
console.log(fun instanceof Object); //true
console.log(fun instanceof person); //false

简单来说,使用工厂模式创建对象的过程就是,在函数内创建一个对象,赋予属性及方法后再将对象返回。但是工厂模式创建的实例类型全都是Object,却不能识别到底是哪种对象类型。instanceof 用于判断一个变量是否是某个对象的实例,从上例最后两行代码就能看出,工厂模式就像暗箱操作,实例不知道自己是被谁创造的。但是好在“构造函数模式”可以解决这个问题。

构造函数模式

我们用构造函数重写上面的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
function Person(a,b) {
this.name = a;
this.age = b;
this.getName = function () {
console.log(this.name);
};
}
var fun = new Person("wanghan",20)
fun.getName(); //wanghan
console.log(fun.age); //20
console.log(fun instanceof Object); //true
console.log(fun instanceof Person); //true

最后两行代码可以看出,在这个模式中,可以验证fun是构造函数Person的实例,说明了构造函数可以将它的实例标识为一种特定的类型,这正是胜于工厂模式的地方。
我们仔细观察构造函数与工厂模式创造的函数的不同之处:

  • 没有显式地创建对象;
  • 直接将属性和方法赋值给了this对象;
  • 没有return语句;
  • 函数首字母大写(为了区别于普通函数);
  • 调用函数时用到new操作符。

new操作符

构造函数跟其他函数的唯一区别,就在于调用它们的方式不同,任何函数,只要通过new操作符来调用,那它就可以作为构造函数。通过new调用函数时会经历以下步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋值给了新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象

构造函数的问题

构造函数模式虽然好用,但也并非没有缺点,它的主要问题就是每个方法要在每个实例上创建一遍。在上面的栗子中,实例fun中创建了一个getName方法,但是如果我们再实例化一个对象,会再次创建一个getName方法,并且这两个同名函数是不相等的。

1
2
3
4
5
6
7
8
9
10
function Person(a,b) {
this.name = a;
this.age = b;
this.getName = function () {
console.log(this.name);
};
}
var fun = new Person("wanghan",20);
var fun2 = new Person("wanghan",20);
console.log(fun.getName == fun2.getName); //false

这些getName方法实现的功能是完全一样的,但是由于分别属于不同的实例,就不得不为每个getName分配空间,这显然是不合理的,那要怎么样才能让所有的实例都访问同一个getName方法呢,这就要用到原型模式了。关于原型,且听下回分解。