JavaScript面向对象的程序设计

面向对象的程序设计

本文是原理解释,实例请参考:http://www.jb51.net/article/53823.htm

1、理解对象
1.1、属性类型
        ECMA-262第五版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特性。ECMA-262定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接使用它们。为了表示特性是内布值,该规范把它们放在了两对儿方括号中,例如[[Enumeralbe]]。尽管ECMA-262第三部的定义有些不同,但本书只参考第五版的描述。
        ECMA-262中有两种属性:数据属性和访问器属性。
        1.1.1、数据属性
                #[[Configurable]]:
                #[[Enumerable]]:
                #[[Writable]]:
                #[[Value]]:
                要修改属性默认的特性,必须使用ECMAScript 5的Object.defineProperty()方法。这个方法接受三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是configurable、enumerable、writable和value。设置其中的一个或多个值,可以修改对应的特性值。
        1.1.2、访问器属性
                访问器属性不包含数据值;它们包含一对儿getter和setter函数(不过,这两个函数都不是必需的).
                #[[ Configurable]]:
                # [[Enumerable]]:
                # [[Get]]:
                # [[Set]]:         
                访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
                在不支持Object.defineProperty()方法的浏览器中不能修改[[Configurable]][[Enumerable]]

1.2、定义多个属性

        ECMAScript 5又定义了一个Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

1.3、读取属性的特性
        使用ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接受两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性、这个对象的属性有configurable、enumerable、writable和value。
2、创建对象
2.1、工厂模式
         http://blog.chinaunix.net/uid-26672038-id-4011524.html
          可以看到工厂模式的实现方法非常简单,解决了创建多个相似对象的问题,但是工厂模式却无从识别对象的类型,因为全部都是Object,不像Date、Array等,因此出现了构造函数模式。
2 .2、构造函数模式
         以这种方式创建函数,每个对象实例都包含一个不同的Function实例,函数实例会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。
        然而,创建多个完成相同任务的Function实例的确没有必要;况且有this对象在,根本不用在执行大麦前不过可以通过把函数定义转移到构造函数外部来解决这个问题。在构造函数内部,我们将sayName属性设置成等于全局的sayName函数。这样一来,由于sayName包含的是一个指向函数的指针,因此,多个对象就共享了在全局作用域总定义的同一个sayName函数。这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用。这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义跟多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
        好在,这些问题可以通过使用原型模式来解决。
2.3、原型模式
        我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。
        prototype(原型)属性。
        2.3.1理解原型对象
                 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
                创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性_proto_;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。         
                 虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法返回true。
                ECMAScript 5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。支持这个方法的浏览器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。
                虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们再实例中增加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。通过使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

        使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true。
        2.3.2原型与in操作符
                有两种方式使用in操作符:单独使用和for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
                在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记的属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在IE8及更早的版本中例外。
                要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
                如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。
                Object.keys()和Object.getOwnPropertyNames()方法都可以用来替代for-in循环。支持这两个方法的浏览器有IE9+、Safari 5+、Firefox 4+、Opera 12+和Chrome。
        2.3.3更简单的原型语法
                为减少不必要输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。如下面的例子:
                function Person(){}
                Person.prototype={name:"Tom",age:19;sayName:function(){alert(this.name);}};
                在上面的例子中,我们将Person.prototype设置为等于一个对象字面量形式创建的新对象。最终结果相同,但有个一例外:constructor属性不再指向Person了。前面曾经介绍过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在这里使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了。
                function Person(){}
                Person.prototype={constructor:Person,name:"Tom",age:19,sayName:function(){alert(this.name);}};
                以上代码特意包含了一个constructor属性,并将它的值设置为Person,从而确保了通过该属性能够访问到适当的值。
                注意,以这中方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的。因此如果使用兼容ECMAScript 5的JavaScript引擎,可以试一试Object.defineProperty().
                function Person(){}
                Person.prototype={name:"Tom",age:19;sayName:function(){alert(this.name);}};
                Object.defineProperty(Person.prototype,"constructor",{enumerable:false,value:Person});
        2.3.4原型的动态性
                由于在原型中查找值得过程是一次搜索,因此我们队原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
                尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。
                重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
        2.3.5原生对象的原型
                原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
                 通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。
        2.3.6原型的问题
            原型的最大问题是由其共享的本性所导致的。
            共享对包含基本值类型的属性还说的过去,但是对于包含引用类型值的属性来说,问题就比较突出了。
2.4、组合使用构造函数模式和原型模式
         使用动态原型模式时,不能使用对象字面量 重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
2.5、动态原型模式
2.6、寄生构造函数模式
         关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。
2.7、稳妥构造函数模式
         所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的属性。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不适用new操作符调用构造函数。
        function Person(name,age,job){
              var o = new  Object();
              o.sayName=function(){
                   alert(name);
              };
              return o;
        }
        var friend = Person("Tom",29,"Software Engineer");
        friend.sayName();
        这样,实例变量person中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方法可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。
        与寄生构造函数模式类似,适用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。
3、继承
        OO语言支持两种类型的继承,接口继承和实现继承。接口继承只方法签名,而实现继承则继承实际的方法。
        js只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
2.1、原型链
         构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
        假如我们让原型对象等于另一个类型的实例,结果会怎样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中,也包含着一个指向另一个构造函数的指针。
        2.1.1、别忘记默认的原型
        2.1.2、确定原型和实例的关系
                子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
                在通过使用原型链实现继承时,不能使用对象字面量创建原型方法。
        2.1.3、谨慎地使用方法
        原型链的问题:
             1)、最主要的问题来自包含引用类型值的原型。
             2)、在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题。实践中很少会单独使用原型链。
2.2、借用构造函数
         如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。
2.3、组合继承
         指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是,使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
        instanceof和isPrototypeOf也能够用于识别基于组合继承创建的对象。
2.4、原型式继承
         借助原型可以基于已有的对象创建新对象,同时还不比因此创建自定义类型。
        ECMAScript 5通过新增Object.create()方法规范化了原型式继承。这个方法接受两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和object()的方法行为相同。Object.create()的第二个参数,与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
        在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
2.5、寄生式继承
         寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
        在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。任何能够返回新对象的函数都适用于此模式;能够返回新对象的函数在此模式中不是必需的。
        使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
2.6、寄生组合式继承
         组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
        所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不比为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
        开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。因为,寄生组合式继承可以做出高效率的应用:只调用一次超类型构造函数,并且避免了在子类型原型上创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof和isPropertiesOf()。
                function Person(){}
                Person.prototype={name:"Tom",age:19;sayName:function(){alert(this.name);}};
                function Person(){}
                Person.prototype={name:"Tom",age:19;sayName:function(){alert(this.name);}};
                function Person(){}
                Person.prototype={name:"Tom",age:19;sayName:function(){alert(this.name);}};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值