首页 > 代码库 > Javascript面向对象的程序设计
Javascript面向对象的程序设计
面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。
每个对象都是基于一个引用类型创建的,这个引用类型可以是之前的原生类型,也可以是开发人员定义的类型。
1、理解对象
①属性类型:ECMAScript中有两种属性:数据属性和访问器属性。
(1)数据属性:数据属性包含一个数据值的位置。在这个位置可以读取和写入值,数据户型有4个描述其行为的特性:
数据属性有4个描述其行为的特性。
[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值为true
[[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true
[[Writable]]:表示能否修改属性的值。默认值为true
[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置,默认为undefined
要修改属性默认的特性,要使用ECMAScript5的Object.defineProperty()方法。这个方法要接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value。设置其中的一个或多个值,可以修改对应的特性值。
(2)访问器属性:访问器属性不包含数据值;它们包含一对儿getter和setter函数。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性:
[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值为true
[[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true
[[Get]]:在读取属性时调用的函数,默认值为undefined
[[Set]]:在写入属性时调用的函数,默认值为undefined
访问器属性不能直接定义,必须使用Object.defineProperty()来定义
②定义多个属性
由于为对象定义多个属性的可能性很大,ECMAScript5定义了一个Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一 一对应。
③读取属性的特性
ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,可以是访问器属性,也可以是数据属性。
在JavaScript中,可以针对任何对象——包括DOM和BOM对象,使用Object.getOwnPropertyDescriptor()方法。
2、创建对象
虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。
①工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节
此函数能够接受的参数来构建一个包含所有必要信息的person对象。可以数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。随着JavaScript的发展,一个新的模式又出现了。
②构造函数模式:
与工厂模式的区别:(1)没有显示地创建对象(2)直接将属性和方法赋给了this对象(3)没有return语句。
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。
这个例子中,person1和person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person:
对象的constructor属性最初是用来标识对象类型的。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方。在这个例子中,person1和person2之所以同时是Object的实例,是因为所有对象均继承自Object。
(1)将构造函数当做函数:构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。
(2)构造函数的问题:每个方法都要在每个实例上重新创建一遍。可以通过把函数定义转移到构造函数外部来解决这个问题
新问题就是在全局作用域中定义的函数实际上只能被某个对象调用。更无法接受的是如果对象需要定义很多方法,那么就要定义很多个全局函数。原型模式可以解决。
③原型模式:
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
(1)理解原型对象:
多个对象实例共享原型所保存的属性和方法的基本原理:当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到具有给定名字的属性,则返回该属性的值。如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果找到,则返回该属性的值。
原型最初只包含constructor属性,而该属性也是共享的,因此可以通过对象实例访问
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。即添加这个属性会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接,不过,使用delete操作符则完全可以删除实例属性,从而让我们能够重新访问原型中的属性。
使用hasOwnProperty()方法可以检测一个属性是存在实例中,还是存在原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例时,才会返回true
只有当person1重写name属性后才会返回true,因为只有这个时候name才是一个实例属性,而非原型属性。
(2)原型与in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。
单独使用:in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中
同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中
由于in操作符只要通过对象能够访问到属性就返回true,hasOwnProperty()只在属性存在于实例中返回true,因此只要in操作符返回true,而hasOwnProperty()返回false,就可以确定属性是原型中的属性
name属性先存在于原型中,因此hasPrototypeProperty()返回true。当在实例中重写name属性后,该属性就存在于实例中了,因此hasPrototypeProperty()返回false。
在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环返回。
要取得对象上的所有可枚举的实例属性,可以使用ECMAScript5的object.keys()方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
如果是通过Person实例调用,则Object.keys()返回的数组只包含“name”和“age”两个实例属性。
如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法
注意结果中包含了不可枚举的constructor属性。Object.keys()方法和Object.getOwnPropertyNames()方法都可以用来代替for-in循环。
(3)更简单的原型语法
用一个包含所有属性和方法的对象字面量来重写整个原型对象
我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象,最终结果相同,但是有一个例外:construction属性不再指向Person了。constructor属性变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。
(4)原型的动态性。
(5)原生对象的原型:
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String等等)都在其构造函数的原型上定义了方法。
④组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。这种混成模式还支持向构造函数传递参数。
这是用来定义引用类型的一种默认模式
⑤动态原型模式
这段代码只会在初次调用构造函数时才会执行,此后,原型已经完成初始化,不需要再做什么修改了。这里对原型所做的修改,能够立即在所有实例中得到反映。
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
⑥寄生构造函数模式
在前述几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,但从表面上看,这个函数又很像是典型的构造函数。
3、继承:ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
①原型链:ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。给原型添加方法的代码一定要放在替换原型的语句之后。
原型链的问题:(1)包含引用类型值的原型。在通过原型来实现继承时,原型实际上回变成另一个类型的实例。(2)在创建子类型的实例时,不能向超类型的构造函数中传递参数。实践中很少会单独使用原型链。
②借用构造函数:在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术。基本思想就是子类型构造函数的内部调用超类型构造函数。
③组合继承:也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。
④原型式继承:
在object()函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
ECMAScript5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个是用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
⑤寄生式继承
寄生式继承时与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
⑥寄生组合式继承:组合继承无论是在什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次实在子类型构造函数内部。
子类型最终都会包含超类型对象的全部实例属性。
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。思路就是:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非就是超类型原型的一个副本而已。
Javascript面向对象的程序设计