你的位置:首页 > Java教程

[Java教程]高程6.2创建对象


虽然Object构造函数或对象字面量都可以用来创建单个对象,但这个方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码.为解决这个问题,人们开始使用工厂模式的一种变体.

6.2.1工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种抽象了创建具体对象的过程.考虑到在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("Nicholas",29,"Software Engineer");  console.log(person1);//Object {name: "Nicholas", age: 29, job: "Software Engineer"}  var person2=createPerson("Greg",27,"Doctor");  console.log(person2);//Object {name: "Greg", age: 27, job: "Doctor"}

函数createPerson()能够根据接受的参数来构建一具包含所有必要信息的Person对象.可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象.

工厂模式虽然解决了创建多个相似对象的问题,但湍解决对象识别的问题(即怎样知道一个对象的类型).

6.2.2构造函数模式

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("Nicholas",29,"Software Engineer");  console.log(person1);//Person {name: "Nicholas", age: 29, job: "Software Engineer"}  var person2=new Person("Greg",27,"Doctor");  console.log(person2);//Person {name: "Greg", age: 27, job: "Doctor"}

在这个例子中,Person()取代了createPerson(),代码除了与第一个例子中的相同部分外,还存在以下不同的部分:

1 没有显式地创建对象;

2 直接将属性和方法赋给了this对象;

3 没有return语句.

构造函数始终都应该以一个大写字母开关,而非构造函数则应该以一个小写字母开头.构造函数本身也是函数,只不过可以用来创建对象而已.

要创建Person的新实例,必须使用new操作符.以这种方式调用构造函数实际上会经历以下4个步骤:

1.创建一个新的对象;

2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象);

3.执行构造函数中的代码(为这个新对象添加属性);

4.返回新对象.

在上面例子的最后,person1和person2分别保存着Person的一个不同的实例.这两个对象都有一个constructor(构造函数)属性,该属性指向Person.

 

alert(person1.constructor==Person);//true  alert(person2.constructor==Person);//true

对象的constructor属性最初是用来标识对象类型的.但是,提到检测对象类型,还是instanceof操作符更可靠一些.我们在这个例子中创建的所有对象既是Object的实例,同时也是Person的实例,这一点通过instanceof操作符可以得到验证.

alert(person1 instanceof Object);//true  alert(person1 instanceof Person);//true  alert(person2 instanceof Object);//true  alert(person2 instanceof Person);//true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方.在这个例子中,person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object.

以这种方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中的.

1.将构造函数当作函数

构造函数与其他函数的唯一区别就在于调用它们的方式不同.任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样.

例如,前面例子中定义 Person()函数可以通过下列任何一种方式调用.

//当作构造函数使用  var person=new Person("Nicholas",29,"Software Engineer");  person.sayName();//Nicholas  //作为普通函数调用  Person("Greg",27,"Doctor");//添加到window  window.sayName();//Greg  //在另一个对象的作用域中调用   var o=new Object();  Person.call(o,"Kristen",25,"Nurse");  o.sayName();//Kristen

这个例子中的前两行代码展示了构造函数的典型用法,即使用new操作符来创建一个新对象.接下来两行代码展示了不使用new操作符调用Person()会出现什么结果:属性和方法都被添加给window对象了.

当在全局作用域中调用一个函数时,this对象问题指向Global对象(在浏览器中就是window对象).因此,在调用完函数后,可以通过window对象来调用sayName()方法,并且返回了"Greg".最后,也可以使用call()(或者apply())在某个特殊对象的作用域中调用Person()函数.这里是在对象o的作用域中调用的,因此调用后o就拥有了所有属性和sayName()方法.

2.构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍.在前面的例子中,person1和person2都有一个名叫sayName()的方法,但那两个方法不是同一个Function的实例.ECMAScript中的函数是对象,因为每定义一个函数,也就是实例化了一个对象.

从逻辑角度来讲,此时的构造函数也可以这样定义

function Person(name,age,job){    this.name=name;    this.age=age;    this.job=job;    this.sayName=new Function("alert(this.name)");//与声明函数在逻辑上是等价的  }

从这个角度上来看构造函数,更容易明白每个Person实例都包含一个不同的Function实例(以显示name属性)的本质.说明白些,以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的.因此,不同实例上的同名函数是不相等的.

alert(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(){    alert(this.name);  }  var person1=new Person("lili",28,"Engineer");  console.log(person1);//Person {name: "lili", age: 28, job: "Engineer"}  var person2=new Person("honghong",23,"Doctor");  console.log(person2)//Person {name: "honghong", age: 23, job: "Doctor"}

这个例子中,把sayName()函数定义转移到了构造函数的外部,而在构造函数内部,将sayName属性设置成等于全局的sayName函数.这样一样,由于sayName包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数.这个做确实解决了两个函数做同一件事的问题,不过新问题来了:在全局作用中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实.而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了.好在,这些问题可以通过使用原型模式来解决.

6.2.3原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法.如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象的原型对象.使用原型对象的好处是可以让所有实例共享它所包含的属性和方法.换名话说,不必在构造函数中定义对象的实例的信息,而是可以将这些信息直接添加到原型对象中.

function Person(){  }  Person.prototype.name="honghong";  Person.prototype.age=23;  Person.prototype.job="Doctor";  Person.prototype.sayName=function(){    alert(this.name);  };  var person1=new Person();  person1.sayName();//honghong  var person2=new Person();  person2.sayName();//honghong  alert(person1.sayName==person2.sayName);//true

我们将sayName()方法和所有属性直接添加到Person的prototype属性中,构造函数成了空函数.即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还 会具有相同的属性和方法.但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的.换句话说,person1和person2访问的都是同一组属性和同一个sayName()函数.

1.理解原型对象

无论什么时候,只要创建了一个新函数,应会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象.在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所有函数的指针.就拿前面的例子来说,Person.prototype.constructor指向Person.而通过恋之欲室构造函数,我们还可继续为原型对象添加其他属性和方法.

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来的.当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象.虽然在脚本中没有标准的方式访问[[Prototype]],但FF,Safari和Chrome在每个对象上都支持一个属性_proto_; 而在其他实现中,这个属性对脚本则是完全不可见的.这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间.

以前面使用Person构造函数和Person.prototype创建实例的代码为例,图6-1展示了各个对象之间的有关系.V_PC5IX1QP9F6KBIJ33K[XA

 

这里Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person.原型对象中除了包含constructor属性之外,还包括后来添加的其他属性.Person的每个实例都包含一个内部属性,该属性仅仅指向了Person.prototype,换句话说,它们与构造函数没有直接关系.

可以通过isPrototype()方法来确定对象之间是否存在这种关系.从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true.

alert(Person.prototype.isPrototypeOf(person1));//true  alert(Person.prototype.isPrototypeOf(person2));//true

ECMAScript 5增加了一个新方法,叫Object.getPrototype(),在所有支持的实现中,这个方法返回[[Prototype]]的值.

alert(Object.getPrototypeOf(person1)==Person.prototype);//true  alert(Object.getPrototypeOf(person1).name);//honghong

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性.搜索首先从对象的实例本身开始.如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查的具有给定名字的属性.如果在原型对象中找到这个属性,则返回该属性的值.这是多个对象实例共享原型所保存的属性和方法的基本原理.

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值.

实例中的属性将会屏蔽原型中同名的属性,添加这个属性会阻止我们访问原型中的那个属性,但不会修改那个属性.即使将这个属性设置为null,也只会在实例中设置这个属性,而不会改得其指向原型的连接.不过,使用delete操作符则可以安全删除实例属性,从而让我们能够重新访问原型中的属性.

使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中.这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例时,都会返回true.

function Person(){  }  Person.prototype.name="honghong";  Person.prototype.age=23;  Person.prototype.job="Doctor";  Person.prototype.sayName=function(){    alert(this.name);  };  var person1=new Person();  var person2=new Person();  alert(person1.hasOwnProperty("name"));//false  person1.name="lili";  alert(person1.name);//lili  alert(person1.hasOwnProperty("name"));//true  alert(person2.name);//honghong  alert(person2.hasOwnProperty("name"));//false  delete person1.name;  alert(person1.name);//honghong  alert(person1.hasOwnProperty("name"));//false

通过使用hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了.

调用person1.hasOwnProperty(“name”)时,只有当person1重写name属性后都会返回true,因为只有这个时候name才是一个实例属性,而非原型属性.

注意:ECMAScript 5的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法.

2.原型与in操作符

有两种方式使用in操作符:单独使用和在for-in循环中使用.在单独使用时,in操作符会在通过对象能够访问时给定属性时返回true,无论该属性存在于实例还是原型中.

function Person(){}Person.prototype.name="Nicholas";Person.prototype.age="28";Person.prototype.job="Doctor";Person.prototype.sayName=function(){  console.log(this.name);};var person1=new Person();var person2=new Person();console.log(person1.hasOwnProperty("name"));//falseconsole.log("name" in person1);//trueperson1.name="honghong";console.log(person1.name);//honghongconsole.log(person1.hasOwnProperty("name"));//trueconsole.log("name" in person1);//trueconsole.log(person2.name);//Nicholasconsole.log(person2.hasOwnProperty("name"));//falseconsole.log("name" in person2);//truedelete person1.name;console.log(person1.name);//Nicholasconsole.log(person1.hasOwnProperty("name"));//falseconsole.log("name" in person1);//true

在以上代码执行的整个过程中,name属性要么是直接在对象上访问到的,要么是通过原型访问到的.调用"name" in person1始终都返回true,无论该属性存在于实例中还是存在于原型中.同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中.

//自定义一个函数根据返回true或false可知该属性是存在于对象中还是存在于原型中
function hasPrototypeProperty(object,name){  return !object.hasOwnProperty(name)&&(name in object);}function Person(){}Person.prototype.name="Nicholas";Person.prototype.age="28";Person.prototype.job="Doctor";Person.prototype.sayName=function(){  console.log(this.name);};var person=new Person();console.log(hasPrototypeProperty(person,"name"));//trueperson.name="honghong";console.log(hasPrototypeProperty(person,"name"));//false

在使用for-in循环时,返回的是所有能够通过对象访问的 可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性.屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环返回.

要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5的Object.keys()方法.这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组.

function Person(){}Person.prototype.name="Nicholas";Person.prototype.age="28";Person.prototype.job="Doctor";Person.prototype.sayName=function(){  console.log(this.name);};var keys=Object.keys(Person.prototype);console.log(keys);//["name", "age", "job", "sayName"]var p1=new Person();p1.name="Rob";p1.age=31;var p1keys=Object.keys(p1);console.log(p1keys);//["name", "age"]

如果想得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法.

var keys=Object.getOwnPropertyNames(Person.prototype);console.log(keys);//["constructor", "name", "age", "job", "sayName"]

注意结果中包含了不可枚举的constructor属性.Object.keys()和Object.getOwnPropertyNames()方法都可以用来替代for-in循环.

3.更简单的原型语法

前面的例子中每添加一个属性和方法都要敲一遍Person.prototype.为了减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的的对象字面量来重写整个原型对象.如下面的例子

function Person(){}Person.prototype={  name:"Nicholas",  age:28,  job:"Doctor",  sayName:function(){    alert(this.name);  }}

上面我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象.最终结果相同,但有一个例外:constructor属性不再指向Person了.

var friend=new Person();console.log(friend instanceof Object);//trueconsole.log(friend instanceof Person);//trueconsole.log(friend.constructor==Person);//falseconsole.log(friend.constructor==Object);//true

在此,用instanceof操作符测试Object和Person仍然返回true,但constructor属性则等于Object而不等于Person了.如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值.

function Person(){}Person.prototype={  constructor:Person,  name:"Nicholas",  age:28,  job:"Doctor",  sayName:function(){    alert(this.name);  }}

注意,以这种方法重设constructor属性会导致它的[[Enumerable]]特性被设置为true.默认情况下,原生的constructor属性是不可枚举的,因此如果你使用兼容的ECMAScript 5的JavaScript引擎,可以试一试Object.defineProperty().

function Person(){}Person.prototype={  name:"Nicholas",  age:28,  job:"Doctor",  sayName:function(){    alert(this.name);  }}//重设构造函数,只适合用于ECMAScript兼容的浏览器Object.defineProperty(Person.prototype,"constructor",{  enumerable:false,  value:Person,})

4.原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来--即使是先创建了实例后修改原型也照样如此.

var friend=new Person();Person.prototype.sayHi=function(){  alert("hi");}friend.sayHi();//hi

实例与原型之间的松散连接关系.当我们调用person.sayHi()时,首先会在实例中搜索名为sayHi的属性,在没有找到的情况下,会继续搜索原型.因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回存在那里的函数.

调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系.记住:实例中的指针仅指向原型,而不指向构造函数.

function Person(){}var friend=new Person();Person.prototype={  constructor:Person,  name:"Nicholas",  age:28,  job:"Doctor",  sayName:function(){    alert(this.name);  }};friend.sayName();//Uncaught TypeError: friend.sayName is not a function

145WJA[3}}0K8`[E`GH2{$G

从图6-3可以看出,重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型.

5.原生对象的原型

所有原生引用类型(Object,Array,String等等)都在其构造函数的原型上定义了方法.例如在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法.

console.log(typeof Array.prototype.sort);//functionconsole.log(typeof String.prototype.substring);//function

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法.可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法.

下面的代码就给基本包装类型String添加了一个名为startWith()的方法.

String.prototype.startWith=function(text){  return this.indexOf(text)==0;};var msg="Hello world!";console.log(msg.startWith("Hello"));//true

这里新定义的startWith()方法会在传入的文本位于一个字符串开始时返回true.既然方法被添加给了String.prototype,那么当前环境中的所有字符串都可以调用它.

尽管可以这样做,不过我们不推荐在产品化的程度中修改原生对象的原型.如果因某个实现中缺少某个就去,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可以会导致命名冲突.而且,这样做也可能会意外地重写原生方法.

6.原型对象的问题

原型模式的缺点是:首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值.另外原型模式的最大问题是由其共享的本性所导致的.

原型中所有属性被很多实例共享,这种共享对于函数非常合适.对于包含引用类型值的属性来说,问题就比较突出了.

function Person(){}var friend=new Person();Person.prototype={  constructor:Person,  name:"Nicholas",  age:28,  job:"Doctor",  friends:["Shelby","Court"],  sayName:function(){    alert(this.name);  }};var person1=new Person();var person2=new Person();person1.friends.push("Van");console.log(person1.friends);//["Shelby", "Court", "Van"]console.log(person2.friends);//["Shelby", "Court", "Van"]console.log(person1.friends===person2.friends);//true

Person.prototype对象有一个名为friends的属性,该属性包含一个字符串数组.然后,创建了Person的两个实例,接着,修改了person1.friends引用的数组,向数组中添加了字符串.由于friends数组存在于Person.prototype而非person1中,所以刚刚提到的修改也会通过person2.friends(与person1.friends指向同一个数组)反映出来.实例一般都是要有属于自己的全部属性的,而这个问题正是我们很少看到有人单独使用原型模式的原因所在.

6.2.4 组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式.构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性.结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存.另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式这长.

function Person(name,age,job){  this.name=name;  this.age=age;  this.job=job;  this.friends=["Shelby","Court"];}Person.prototype={  constructor:Person,  sayName:function(){    alert(this.name);  }}var person1=new Person("Nicholas",28,"Doctor");var person2=new Person("Greg",23,"Teacher");person1.friends.push("Van");console.log(person1.friends);//["Shelby", "Court", "Van"]console.log(person2.friends);//["Shelby", "Court"]console.log(person1.frients===person2.friends);//falseconsole.log(person1.sayName===person2.sayName);//true

这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法.可以说,这是用来定义引用类型的一种默认模式.

6.2.5 动态原型模式

动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数的原型的优点.

换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型.

function Person(name,age,job){  this.name=name;  this.age=age;  this.job=job;  if(typeof this.sayName !="function"){    Person.prototype.sayName=function(){      alert(this.name);    };  }}var friend=new Person("Nicholas",28,"Doctor");friend.sayName();//Nicholas

对于采用这种模式创建的对象,还可以使用instranceof操作符确定它的类型.

使用动态原型模式,不能使用对象字面量重写原型.

6.2.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 friend=new Person("Nicholas",28,"Doctor");friend.sayName();//Nicholas

构造函数在不返回值的情况下,默认会返回新对象实例.而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值.

function SpecialArray(){  //创建数组  var values=new Array();  //添加值  values.push.apply(values,arguments);  //添加方法  values.toPipedString=function(){    return this.join("|");  };  //返回数组  return values;}var colors=new SpecialArray("red","blue","green");alert(colors.toPipedString());//red|blue|green

关于寄生构造函数模式,有一点要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同.为此,不能依赖instanceof操作符来确定对象类型.由于存在上述问题,建议在可以使用其他模式的情况下,不要使用这种模式.