面向对象的语言有一个标志,那就是他们都有类的概念,而通过类可以创建多个具有相同属性和方法的对象。ECMAScript的对象没有类的概念,因此它的对象也与基于类的语言对象有所不同。
早期JavaScript开发人员创建类的方法是创建一个Object的实例,然后为它添加属性和方法,如下:
var person = new Object(); person.name = "Amy"; person.age = 25; person.job = "Student"; person.sayName = function(){ alert(this.name); }; person.sayName(); //"Amy"
这种方法的明显缺点:使用同一个接口创建很多对象,会产生大量的重复代码,为解决这个问题,工厂模式诞生。
1. 工厂模式
用函数来封装以特定接口创建对象的细节,如下:
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("Amy","25","Student"); var person2 = createPerson("Joy","29","Engineer"); person1.sayName(); //"Amy" person2.sayName(); //"Joy"
函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的person对象,可以无数次的调用这个函数。工厂模式虽然解决了创建多个相似对象的问题,但是却没有解决对象识别问题,于是有一个新模式出现了。
2.构造函数模式
使用构造函数模式将前面例子重写如下 :
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person("Amy","25","Student"); var person2 = new Person("Joy","29","Engineer"); person1.sayName(); //"Amy" person2.sayName(); //"Joy"
Person()和createPerson()的区别在于:
- 没有显示地创建对象;
- 直接将属性和方法赋给了this对象;
- 没有return语句。
要创建Person的新实例,就要用关键字new,通过new操作符来调用的任何函数,都可以作为构造函数。person1和person2都有一个constructor(构造函数)属性,该属性指向Person,检测对象类型,还可以用instanceof操作符,如下:
alert(person1.constructor == Person); //true alert(person2.constructor == Person); //true alert(person1 instanceof Person); //true alert(person1 instanceof Object); //true alert(person2 instanceof Person); //true alert(person2 instanceof Object); //true
person1和person2之所以同时是Object的实例,是因为所有对象均继承与Object。
缺点:每个方法都要在每个实例上重新创建一遍。所以,有一种方法是说把函数定义转移到构造函数外部,代码如下:
function Person(name ,age,job){ this.name=name; this.age=age; this.job=job; this.sayName=sayName; } function sayName(){ alert(this.name); } var person1=new Person("Amy",25,"Software Engineer"); var person2=new Person("Joy",29,"Doctor"); person1.sayName(); //"Amy" person2.sayName(); //"Joy"把sayName()函数的定义转移到构造函数外部,成为全局的函数,构造函数内部把sayName赋为为全局的sayName。这样sayName是一个指向外部函数的指针,因此person1和person2就共享了 全局的sayName函数。但是这会有个问题:全局作用域的函数只能被某个对象调用,这名不副实,会造成对全局环境的污染;更糟糕的是如果对象要定义很多方法,就要定义多少个全局函数, 那这个自定义引用类型就丝毫 没有封装性可言了。这个问题可以通过原型模式解决。
3.原型模式
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个对象,他的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype就是通过调用构造函数而创建的那个对象的原型对象。所有对象实例共享它所包含的属性和方法。如下例子:
function Person(){} Person.prototype.name = "Amy"; Person.prototype.age = 25; Person.prototype.job = "student"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.sayName(); //"Amy" person2.sayName(); //"Amy"我们将sayName()方法和所有属性直接添加到了Person的prototype属性中,构造函数变成了空函数,即便如此也可以通过调用构造函数来创建一个新对象,而且还会具有相同的属性和方法,与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。 原型模式最大的问题是由其共享的本性所导致的,代码如下:
function Person(){ } Person.prototype={ name:"Amy", age:22, job:"Software Engineer", friends:["firend1","friend2"], sayName:function(){ alert(this.name); } }; var person1=new Person(); var person2=new Person(); alert(person1.friends); //friend1,friend2 alert(person2.friends); //friend1,friend2 alert(person1.friends==person2.friends); //true person1.friends.push("friend3"); alert(person1.friends); //friend1,friend2,friend3 alert(person2.friends); //friend1,friend2,friend3 alert(person1.friends==person2.friends); //true
实例一般都要有属于自己的全部属性,所以很少有人单独使用原型模式。
4.组合使用构造函数模式和原型模式
构造函数模式用于自定义实例属性,原型模式用于定义方法和共享属性。这样可以最大限度的节省内存,这种混合模式还支持向构造函数传递参数,可谓集两种模式之长,目前使用最广,代码如下:
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.friends=["firend1","friend2"]; } Person.prototype={ constructor:Person, sayName:function(){ alert(this.name); } } var person1=new Person("Amy",22,"Software Engineer"); var person2=new Person("Joy",25,"Doctor"); alert(person1.friends); //friend1,friend2 alert(person2.friends); //friend1,friend2 alert(person1.friends==person2.friends); //false person1.friends.push("friend3"); alert(person1.friends); //friend1,friend2,friend3 alert(person2.friends); //friend1,friend2
5.动态原型模式
动态原型模式是把所有信息都封装到构造函数中,即通过构造函数必要时初始化原型,在构造函数中同时使用了构造函数和原型,这就成了动态原型模式。真正用的时候要通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
if(typeof this.sayName!="function"){
alert("初始化原型");//只执行一次
Person.prototype.sayName=function(){
alert(this.name);
};
}
}
var person1=new Person("Amy",22,"Software Engineer");
person1.sayName();
var person2=new Person("Joy",25,"Doctor");
person2.sayName();
if部分只有当sayName方法不存在的时候才会添加到原型中,这段代码只会在初次调用构造函数时执行。此后原型已经完成初始化,不需要再继续修改。使用动态原型时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
6.寄生构造函数模式
寄生模式其实就是把工厂模式封装在构造函数模式里,即创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,从表面看,这个函数又很像典型的构造函数。
function Person(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 person = new Person("Amy",22,"Software Engineer"); person.sayName(); //"Amy"
除了使用new操作符使得该函数成为构造函数外,这个模式和工厂模式一模一样。构造函数在不返回值的情况下,默认返回新对象实例。返回的对象与构造函数或者与构造函数的原型属性之间没有任何关系,也就是说,构造函数返回的对象与在构造函数外创建的对象没有什么不同,所以不能用instanceof来确定对象类型。一般不使用这种模式。
7.稳妥构造函数模式
道格拉斯·克罗克福德发明了JavaScript中的稳妥对象这个概念,稳妥对象指的是没有公共属性,而且方法也不引用this的对象。稳妥对象最适合在一些安全的环境中,或者在防止数据被其他应用程序改动时使用。
稳妥构造函数遵循与构造函数类似的模式,但有两点不同:
- 新创建对象的实例方法不引用this
- 不使用new操作符调用构造函数
function Person(name,age,job){ //创建要返回的对象 var o=new Object(); //可以在这里定义私有变量和函数 //添加方法 o.sayName=function(){ alert(name); }; //返回对象 return o; } var person=new Person("Amy",22,"Software Engineer"); person.sayName(); //"Amy" alert(person.name);//undefined alert(person.age);//undefined以这种模式创建的对象中,person是一个稳妥对象,除了使用sayName()方法外,没有其他方法访问name的值,age和job类似。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。