一、工厂模式
鉴于ECMAScript无法创建类,所以开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。举栗:
function createPerson(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ console.log(this.name); } return o; } var person1 = createPerson("Andy",28,"Actor"); var person2 = createPerson("Lily",22,"Nurse");
函数createPerson()能够根据接收的参数来构建一个包含所有必要信息的Person对象。可以无数次调用这个函数,每次它都会返回一个包含三个属性一个方法的对象。
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
二、构造函数
举栗:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ console.log(this.name); } } var person1 = new Person("Andy",28,"Actor"); var person2 = new Person("Lily",22,"Nurse");
Person()和createPerson()之间的不同:
a.没有显式地创建对象;
b.直接将属性和方法赋给了this;
c.没有return语句;
d.函数名第一个字母大写,主要为了区别于其他函数。
要创建Person的新实例,必须使用new操作符。用这种方式调用构造函数会经历以下4个步骤:
a.创建一个新对象;
b.将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
c.执行构造函数中的代码(为这个新对象添加属性);
d.返回新对象。
例子的最后,person1和person2分别保存这个Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person。验证如下:
console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true
对象的constructor属性最初是用来标识对象类型的,但是用instanceof检测对象类型更好一些。上面的例子中创建的所有对象既是Object的实例,也是Person的实例。验证如下:
console.log(person1 instanceof Object); //true console.log(person1 instanceof Person); //true console.log(person2 instanceof Object); //true console.log(person2 instanceof Person); //true
构造函数模式胜过工厂模式的地方在于:创建自定义的构造函数可以将它的实例标识为一种特定的类型。
1.将构造函数当作函数
构造函数与其他函数的唯一区别,在于调用它们的方式不同。构造函数也是函数,所以不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它都可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数没什么差别。举栗:
//当作构造函数使用 var person = new Person("Andy",28,"Actor"); person.sayName(); //"Andy" //作为普通函数调用 Person("Lily",22,"Doctor") //添加到window window.sayName(); //"Lily" //在另一个对象的作用域中调用 var o = new Object(); Person.call(o,"Joe",25,"Nurse"); o.sayName(); //"Joe"
以上前两行代码是构造函数的典型用法,就是用new操作符创建一个新的对象。
中间两行代码不用new操作符调用Person对象,属性和方法都被添加给了全局对象window。
最后两行代码使用call()(或者apply())在某个特殊对象的作用域中调用Person()函数,那么对象o就拥有了所有的属性和方法。
2.构造函数的问题
构造函数主要问题:每个方法都要在每个实例上重新创建一遍。
前面的栗子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。
ECMAScript中的函数是对象,因此每定义一个函数,就是实例化了一个对象。
从以上逻辑角度,此时的构造函数也可以这样定义。
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = new Function("console.log(this.name)"); //与声明函数在逻辑上是等价的 }
从这个角度看构造函数,更容易明白每个Person实例都包含一个不同的Function实例的本质。以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,证明如下:
console.log(person1.sayName == person2.sayName); //false
创建两个完成同样任务的Function实例没有必要,我们可以通过this对象,把函数绑定到特定对象上面。举栗:
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName(){ console.log(this.name); } var person1 = new Person("Andy",28,"Actor"); var person2 = new Person("Lily",22,"Nurse");
上面的栗子,我们把方法sayName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName属性设置成全局的sayName函数。因为sayName包含的是一个指向函数的指针,所以person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。
但是问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,所以这让全局作用域有点儿名不副实。再者,如果对象需要定义很多方法,那么就要定义很多个全局函数,所以我们这个就谈不上封装性了。
参考资料
《javascript高级程序设计(第3版)》第6章 面向对象的程序设计