6.2创建对象

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

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程,考虑到在es中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面的例子所示。

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 Enginner");

var person2=createPerson("Greg",27,"Doctor");

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(及怎么知道一个对象的类型)。随着js的发展,一个新模式出现了。


构造函数模式

es中的构造函数可以用来创建特定类型的对象。像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 Enginner");
      var person2=new Person("Greg",27,"Doctor");

在这个例子中,Person()函数取代了createPerson()函数。我们注意到,Person()中的代码除了与createPerson()中相同部分以外,还存在以下不同之处:

没有显示的创建对象;

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

没有return语句。

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

(1)创建一个新对象;

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

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

(4)返回新对象。

在前面例子的最后,person1和person2分别保存着Person的一个不同实例。这两个对象都有一个constructor(构造函数)属性,改属性指向Person,如下所示。

person1.constructor==Person   //true

person2.constructor==Person  //true

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

person1 instaceof Object ;  person1 instaceof Person;  person2 instaceof Object;  person2 instaceof Person;

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

1.将构造函数当做函数

构造函数与其他函数的唯一区别,就是在调用他们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造的特殊语法。任何函数,只要通过new操作符来调用,那么他就可以作为构造函数,而任何函数,不通过new操作符来调用,那么他跟普通函数也不会有什么两样。例如前面例子定义的Perosn()函数可以通过下列任何一种方式来调用。

var person=new Person("Nicholas",29,"Sofrware Engineer");

person.sayName();

Person("Greg",27,"Doctor");

window.sayName();

//在里一个对象中调用

var o=new Object();

Person.call(o,"Kristen",25,"Nurse");

o.sayName() //Kristen

2.构造函数的问题

构造函数模式虽然好用,但也不是没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。不要忘了,es中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也可以这样定义。

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(perosn1.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("Nicholas",29,"Soft");

var person2=new Person("Greg",27,"Doctor");

在这个例子中,我们把sayName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName属性设置成等于全局的sayName函数。这样一来,由于sayName包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName函数。缺点是封装性差。

原型模式

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

function Person(){
this.name='zx'
}
Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.job="Software";
Person.prototype.sayName=function(){
alert(this.name);
}
var person1=new Person();
person1.sayName();
var person2=new Person();
person2.sayName();

在此,我们将sayName()方法和所有属性直接添加到了Person的prototype属性中,构造函数变成了空函数。即使如此,也任然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数你模式不同的是,新对象的这些属性和方法是有所有实例共享的。换句话说。person1和person2访问的都是同一组属性和同一个sayName()函数。要理解原型模式的工作原理,必须先理解ES中原型对象的性质。

1.理解原型对象

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

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数函数擦混构建一个新实例后,该实例内部将包含一个指针(内部属性),指向构造函数的原型对象。es5中管这个指针叫[[Prototype]],也可以写成__proto__;要明确真正重要一点就是,这个连接存在于实例与构造函数的原型之间,而不是存在于实例与构造函数之间。

Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系。在此,Person.prototype指向了原型对象,而Person.prototype.constructor有指回了Person。

原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。Person的每个实例---person1和person2都包含一个内部属性,该属性仅仅指向Person.prototype;换句话说,它们与构造函数没有直接的关系。此外,要注意,虽然这两个实例都不包含属性和方法,但我们却可以调用person1sayName().这是通过查找对象属性的过程来实现的。

虽然在所有实现中都无法访问到[[prototype]],但可以通过isPrototypeof()方法来确定对象之间是否存在这种关系。

这里我们用原型对象的isPrototypeOf()方法测试了person1和peroson2,因为他们内部都有一个指向Prosion.prototype的指针,因此都返回了true。

es5中有一个方法叫getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。例如:

Object.getPrototypeOf(person1)==Person.prototype;

Object.getPrototypeOf(person.name);  //Nicholas

这里的第一行代码只是确定Object.getPrototypeOf()返回对象实际就是这个对象的原型。第二行代码取得了原型对象中的name。使用Object.getProtopeOf()可以方便取得一个对象的原型,而这也利用原型实现继承的情况下是非常重要的。

每当代码读取某个帝乡的某个属性时,都会执行一次搜索,目标是具有给定的属性。首先收索从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。


虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性,来看下面的例子。

function Person(){
Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.job="Software Enginner";
Person.prototype.SayName=function(){
alert(this.name);
}
}
var person1=new Person();
person1.name="Greg";

通过hasOwnProperty()可以判断一个属性是来至于原型还是自身


2.原型与in操作符

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

3更简单的原型语法

function Person(){}
Person.prototype={
name:'Nicholas',
age:29,
job:'Software',
sayName:function(){
alert(this.name);
}
}

在上面的代码中,我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor属性不在指向Person了。前面曾经介绍过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在这里使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor(指向Object构造函数),不在指向Person函数。此时,尽管instanceof操作符还能返回正确结果,但通过constructor已经无法确定对象的类型了。

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

function Person(){


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

var friend = new Person();

4.原型的动态性

如果是重写整个原型对象,那么情况就不一样了,我们知道,调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数和最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。

function Person(){};



Person.prototype={
constructor:Person,
name:"Nicholas",
sayName:function(){
alert(this.name);
}
};
var friend=new Person();

在这个例子中,我们先创建了Person的一个实例,然后又重新了其原型对象。然后在调用feirend,sayName()时发生错误,因为friend指向的原型中不包含以该名字命名的属性。

5.原声对象的原型。

原型模式的重要性不仅仅体现在创建自定义类型方面,就连所有原声的引用类型,都是采用这种模式创建的。所有原声。所有原生引用类型(object,Array,String,等等)都在其构造函数的原型上定义了方法。

console.log(typeof Array.prototype.sort);

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义的原型一样修改原生对象的原型,因此可以随时添加方法。下面的代码就给基本包装类型String添加一个名为startWith()的方法。

String.prototype.startsWidth=function (text){
return this.indexOf(text)==0;
}
var msg="hello world";
msg.startsWidth("hello");


6.原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,所有实例在默认情况都取得相同的属性值。原型模式最大的问题石油气共享本性所导致的。

原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟通过在实例山刚添加一个同名属性,结义隐藏原型中的对应属性然后对于包含引用类型值属性,问题就比较突出了。


function Person(){}
Person.prototype={
constructor:Person,
name:"Nicholas",
friend:["shelby","Court"],
sayName:function(){
alert(this.name);
}
}


person1 =new Person();
person2 =new Person();
person1.friend.push("Van");
person2.friend.push("ok");
person1.friends === person2.friends //true

在此,Person.prototype对象有一个名为friends的属性,该属性包含一个字符串数组。然后,创建了Person的两个实例。接着,修改了person.friends引用的数组。




























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值