ECMAScript支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建或增强,因此具有动态性而非严格定义的实体。在没有类的情况下,可以采用下列模式创建对象。
对象的创建
工厂模式
工厂模式是软件工程里面一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下代码所示:
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("Tom",28,"软件工程师");var person2=createPerson("XXX",23,"系统架构师");
函数createPerson()能够根据接受的参数来创建一个包含所有必要信息的Person对象。可以无数吃的调用该方法,而每次都会返回一个包含3个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型),随着Javascript的发展,又一个新模式出现了。
构造函数模式
众所周知,ECMAScript的构造函数可以用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时就会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而自定义对象类型的属性和方法。将上面的例子用构造函数的方式重写如下所示:
function Person(name,age,job){this.name=name;this.age=age;this.job=job;this.sayName=function(){alert(this.name);};}var person1=new Person("Tom",28,"软件工程师");var person2=new Person("XXX",23,"系统架构师");
在这个例子中,Person()函数取代了createPerson()函数。我们注意到,Person()中的代码除了与createPerson()相同部分外,还存在以下不同之处:
- 没有显示的创建对象;
- 直接将属性和方法赋予this对象;
- 没有return语句。
此外,还应注意到Person()首字母大写了(这是Javascript构造函数约定俗成的写法,以区别于普通的函数)。使用构造函数时,务必要加上new操作符,否则构造函数与普通函数并没什么区别(换句话说,任何普通函数前面加上new操作符都可以是构造函数)。
创建自定义的构造函数意味着将来可以将它的实际类型标识为一种特定的类型,这恰恰解决了工厂模式的问题。但是构造函数仍然存在以下问题:
- 主要问题就是每个方法都要在每个示例上重新创建一遍。在上面的例子中person1和person2都有一个名为sayName的方法,但是两个方法不是同一个Function的实例;
- 固然我们可以将方法放在构造函数的外面,但是这样一来就使得全局作用域定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实;如果对象需要定义很多方法,那么就要定义很多全局函数,这也就失去了封装的应用类型的意义了。
原型模式
我们创建的每一个函数都有一个prototype(原型)属性(构造函数的一个属性),该属性指向一个对象. 而这个对象将作为该构造函数所创建的所有实例的基引用(base reference), 可以把对象的基引用想像成一个自动创建的隐藏属性. 当访问对象的一个属性时, 首先查找对象本身, 找到则返回; 若不, 则查找基引用指向的对象的属性(如果还找不到实际上还会沿着原型链向上查找, 直至到根). 只要没有被覆盖的话, 对象原型的属性就能在所有的实例中找到.使用原型对象的好处就是可以让所有特定类型实例共享其中定义的属性和方法。示例代码如下所示:
// prototype默认为new Object(); 为了方便, 记为p_objfunction Person(name) {this.name = name;}// 为 p_obj 增加 sayName 属性Person.prototype.sayName = function(){alert(this.name);}var john = new Person("John"); // john 的 base reference指向p_objvar eric = new Person("Eric"); // eric 的 base reference也是指向p_obj// 注意sayName代码中的this将指向实例化后的对象(this绑定)john.sayName(); // john对象本身没有sayName属性, 于是访问原型对象p_obj的sayName属性eric.sayName(); // 访问同一个原型对象p_obj的sayName属性var tmp = Person.prototype;tmp.boss = "David";// 于这个运行点, p_obj已经被修改// 根据上述属性访问流程, 新的修改(boss属性)能反映到所有的实例, 包括已经创建和即将创建的alert("John's boss is " + john.boss);alert("Eric's boss is " + eric.boss);// hisCar和sayCar属性将增加到john对象而不是p_obj对象..john.hisCar = "Audi";john.sayCar = function(){alert(this.name + " has a car of " + this.hisCar);}john.sayCar();// ..因此下一句将错误, 因为eric对象本身和原型p_obj都没有sayName属性/* eric.sayCar(); */
除了上述方式直接定义Prototype外,还可以使用字面量创建原型,但是要注意使用字面量定义的原型属性和方法不能再以字面量的方式重写,一旦重写了原型,原来的原型中定义的所有属性和方法都将被清除(当然用普通的方式是没有问题的),如下所示:
//使用字面量方式创建原型function User(name,age){//构造方法this.name = name;//属性this.age = age;}User.prototype = {addr : '湖北武汉',show : function(){alert(this.name+'|'+this.age+'|'+this.addr);}};//重写了原型User.prototype = {other : '暂时没有说明……',show : function(){alert(this.addr);}};var user1 = new User('ZXC',22);//创建实例var user2 = new User('CXZ',21);user1.show();//返回 undefineduser2.show();