JavaScript高级程序设计:第六章

第六章

面向对象的程序设计

一、理解对象

1.属性类型:

ECMAScript中有两种属性:数据属性和访问器属性。

(1)数据属性:

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个可以描述其行为的特性:

[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。

[[Enumerable]]:表示能否通过for-in循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true。

[[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true。

[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。

(2)访问器属性:

         访问器属性不包含数据值;它们包含一对getter和setter函数。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性:

[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true。

[[Enumerable]]:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true。

[[Get]]:在读取属性时调用的函数。默认值为undefined。

[[Set]]:在写入属性时调用的函数。默认值为undefined。

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。在不支持Object。defineProperty()方法的浏览器中不能修改[[Configurable]]和[[Enumerable]]。

2.定义多个属性

         由于为对象定义多个属性的可能性很大,ECMAscript5又定义了一个Object。defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象。第二个对象的属性与第一个对象中要添加或修改的属性一一对应。例如:

var book={};

Object.defineProperties(book,{

         _year:{

                  writable:true,

                  value:2004

         },

         edition:{

                  writable:true,

                  value:1

         },

         year:{

                  get:function(){

                          return this._year;

                  },

                  set:function(newValue){

                          if(newValue>2004){

                                   this._year=newValue;

                                   this.edition+=newValue-2004;

                          }

                  }

         }

});

         以上代码在book对象上定义了两个数据属性(_year和edition)和一个访问器属性(year)。最终的对象与上一节中定义的对象相同。唯一的区别是这里的属性都是在同一时间里创建的。

3.读取属性的特性

使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable和value。例如:

var book={};

Object.defineProperties(book,{

         _year:{

                  value:2004

         },

         edition:{

                  value:1

         },

         year:{

                  get:function(){

                          return this._year;

                  },

                  set:function(newValue){

                          if(newValue>2004){

                                   this._year=newValue;

                                   this.edition+=newValue-2004;

                          }

                  }

         }

});

var descriptor=Object.getOwnPropertyDescriptor(book,"_year");

alert(descriptor.value);             //2004

alert(descriptor.configurable);       //false

 

二、创建对象

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 0;

}

var person1=createPerson("Nicholas",29,"Software Engineer");

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

 

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

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,如下所示:

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

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

1)将构造函数当作函数

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

//当作构造函数使用

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

person.sayName();   

//作为普通函数调用

Person("Greg",27,"Doctor");       //添加到windows

window.sayName();              //"Greg"

//在另一对象的作用域中调用

var o=new Object();

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

o.sayName();                   //"Kristen"

 

2)构造函数的问题

         使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

3.原型模式

         我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它包含的属性和方法。

(1)理解原型对象:

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

         创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来。当调用一个构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象。

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

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

例如:

function Person(){

}

         Person.prototype.name="Nicholas";

         Person.prototype.age=29;

         Person.prototype.job="Software Engineer";

         Person.prototype.sayName=function(){

                  alert(this.name);

         };

var person1=new Person();

var person2=new Person();

person1.name="Greg";

alert(person1.name);              //"Greg"——来自实例

alert(person2.name);              //"Nicholas"——来自原型

在这个例子中,person1的name属性被一个新值“Greg”屏蔽了。而person2没有新值覆盖所以仍旧是到原型对象中获取name属性的值“Nicholas”。

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。例如:

function Person(){

}

         Person.prototype.name="Nicholas";

         Person.prototype.age=29;

         Person.prototype.job="Software Engineer";

         Person.prototype.sayName=function(){

                  alert(this.name);

         };

var person1=new Person();

var person2=new Person();

person1.name="Greg";

alert(person1.name);              //"Greg"——来自实例

alert(person2.name);              //"Nicholas"——来自原型

delete person1.name;

alert(person1.name);           //“Nicholas”——来自原型

         在这个修改后的例子中,我们使用delete操作符删除了person1.name,之前保存的“Greg”值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中name属性的链接。因此接下来再调用person1.name时,返回的就是原型中name属性的值了。

(2)原型与in操作符:

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

function Person(){

}

         Person.prototype.name="Nicholas";

         Person.prototype.age=29;

         Person.prototype.job="Software Engineer";

         Person.prototype.sayName=function(){

                  alert(this.name);

         };

var person1=new Person();

var person2=new Person();

 

alert(person1.hasOwnProperty("name"));       //false

alert("name" in person1);                    //true

 

person1.name="Greg";

alert(person1.name);                         //"Greg"——来自实例

alert(person1.hasOwnProperty("name"));       //true

alert("name" in person1);                    //true

 

alert(person2.name);                         //"Nicholas"——来自原型

alert(person2.hasOwnProperty("name"));       //false

alert("name" in person1);                    //true

 

delete person1.name;

alert(person1.name);                         //"Nicholas"——来自原型

alert(person1.hasOwnProperty("name"));       //false

alert("name" in person1);                    //true

         在以上代码执行过程中,name要么是直接在对象上访问到的,要么是通过原型访问到的。因此调用“name”in person1始终都返回true,无论该属性存在于实例中还是存在于原型中。由于in操作符只要通过对象能够访问到的属性就返回true,hasOwnProperty()只在属性存在于实例中时,才返回true,因此只要in操作符返回true而hasOwnProperty()返回false,就可以确定属性时原型中的属性。

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

(3)更简单的原型语法

         使用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示:

function Person(){

}

Person.prototype={

         name:"Nicholas",

         age:29,

         job:"Software Engineer",

         sayName:function(){

                  alert(this.name);

         }

}

(4)原型的动态性

         由于在原型的查找值的过程中是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。如下面的例子:

var friend =new Person();

Person.prototype.sayHi=function(){

         alert("Hi");

};

friend.sayHi();          //"hi"(没有问题!)

         以上代码先创建了Person的一个实例,并将其保存在friend中。然后,下一条语句在Person。prototype中添加了一个方法sayHi()。即使friend实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。当我们调用friend.sayHi()时,首先会在实例中搜索名为sayHi的属性,在没找到的情况下,会继续搜索原型。

注意:实例中的指针仅指向原型,而不指向构造函数。

看下面的例子:

function Person(){

}

var friend=new Person();

Person.prototype={

         constructor:Person,

         name:"Nicholas",

         age:29,

         job:"Software Engineer",

         sayName:function(){

                  alert(this.name);

         }

};

friend.sayName();                //error

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

         (5)原生对象的原型

         原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型都在其构造函数的原型上定义了方法。例如,在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法,如下所示。

alert(typeof Array.prototype.sort);                //“function”

alert(typeof String.prototype.substring);           //“function”

 

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

String.prototype.startsWith=function(text){

         return this.indexOf(text)==0;

};

var msg=”Hello world!”;

alert(msg.startsWith(“Hello”));     //true

这里新定义的startsWith()方法会在传入的文本位于一个字符串开始返还true。既然方法都被添加给了String.prototype,那么当前环境中的所有字符串就都可以调用它。由于msg是字符串,而且后台会调用String基本包装函数创建这个字符串,因此通过msg就可以调用startsWith()方法。

(6)原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。

4.动态原型模式

动态原型模式能够通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。看一个例子:

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”,29,“Software Engineer”);

friend.sayName();

         这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改。

5.寄生构造函数模式

通常,在前述的几种模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回创新的对象;但从表面上看,这个函数又很像是典型的构造函数。下面是一个例子:

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”,29,“Software Engineer”);

friend.sayName(); //“Nicholas”

         在这个例子中,Person函数创建了一个对象,并以相应的属性和方法初始化对象,然后又返回了这个对象。除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

6.稳妥构造函数模式

         所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中,或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下:

function  Person(name,age,job){

         //创建要返回的对象

         var  o = new Object();

         //可以在这里定义私有变量和函数

         //添加方法

         o.sayName = function(){

                  alert(name);

         };

         //返回对象

         return  o;

}

三、继承

         1.原型链:

         ECMAscript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现原型链有一种基本模式,其代码大致如下:

function SuperType(){

         this.property = true;

}

SuperType.prototype.getSuperValue = function(){

         return this.property;

};

function SubType(){

         this.subproperty=false;

}

//继承了SuperType

SubType.prototype = new SuperType();

SubType.prototype.getSuperValue=function(){

         return this.subproperty;

};

var instance = new SubType();

alert(instance.getSuperValue());     //true

         以上代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和一个方法。它们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。

(1)确定原型和实例的关系:

         可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。以下几行代码说明了这一点:

alert(instance instanceof Object);           //true

alert(instance instanceof SuperType);        //true

alert(instance instanceof SubType)           //true

由于原型链的关系,我们可以说instance是Object、SuperType或SubType中任何一个类型的实例。因此,测试这三个构造函数的结果都返回了true。

第二种方法是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true,如下所示:

alert(Object.prototype.isPrototypeOf(instance));        //true

alert(SuperType.prototype.isPrototypeOf(instance));     //true

alert(SubType.prototype.isPrototypeOf(instance));       //true

 

(二)谨慎的定义方法

         子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。在通过原型链实现继承时,不能使用对象字面量创建原型方法。

(三)原型链的问题:

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值原型。

2.借用构造函数

         在解决原型中包含引用类型值所带来的问题的过程中,开发人员使用一种叫做借用构造函数的技术。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在将来新创建的对象上执行构造函数。

(1)相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。看下面这个例子。

function SuperType(name){

         this.name = name;

}

function SubType(){

         //继承了SuperType,同时还传递了参数

         SuperType.call(this,“Nicholas”);

         //实例属性

         this.age = 29;

}

var  instance = new SubType();

alert(instance.name);            //“Nicholas”;

alert(instance.age);              //29

以上代码中的SuperType只接受一个参数name,该参数会直接赋给一个属性。在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name属性。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。

(2)借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

3.组合继承

组合继承有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

4.原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数:

function  object(o){

         function  F(){ }

         F.prototype = o;

         return  new  F();

}

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。来看下面的例子。

var person=(

         name:"Nicholas",

         friends:["Shelby","Court","Van"]

         );

var anotherPerson=object(person);

anotherPerson.name="Greg";

anotherPerson.friends.push("Rob");

 

var yetAnotherPerson=object(person);

yetAnotherPerson.name="Linda";

yetAnotherPerson.friends.push("Barbie");

 

alert(person.friends);         //"Shelby,Court,Van,Rob,Barbie"

        

         这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给object()函数,然后再根据具体需求对得到的对象加以修改即可。在这个例子中,可以作为另一个对象基础的是person对象,于是我们把它传入到object()函数中,然后该函数就会返回一个新对象。这个新对象person作为原型,所以它的原型就包含一个基本类型值属性和一个引用类型值属性。这意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。

5.寄生式继承

         寄生式继承是与原型式继承紧密相关的一种思路,与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

posted @ 2016-01-05 08:57  -cyber  阅读(191)  评论(0编辑  收藏  举报