ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”
创建自定义对象的最简单方式就是创建一个Object的实例,然后再为它添加属性和方法。
ECMA262第5版在定义只有内部采用的特性(attribute)时,描述了属性(property)的各种特征。
ECMAScript中有两种属性:数据属性和访问器属性。
1.数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有两个描述其行为的特性。
(1)[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,是否修改属性的特性,或者能否把属性修改为访问器属性。
(2)[[Enumerable]]:表示能否通过for-in循环返回属性。
(3)[[Writable]]:表示能否修改属性的值。
(4)[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。
要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value。设置其中的一个或多个值,可以修改对应的特性值。
2.访问器属性
访问器属性不包含数据值;它们包含一对儿getter和setter函数(不过,这两个都不是必需的)。
(1)[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。
(2)[[Enumerable]]:表示能否通过for-in循环返回属性。
(3)[[Get]]:在读取属性时调用的函数。默认值为undefined。
(4)[[Set]]:在写入属性时调用的函数。默认值为undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
由于为对象定义多个属性的可能性很大,ECMAScript5又定义了一个Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。
使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configuragble、enumerable、writable和value。
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。
对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象类型,还是instanceof操作符要更可靠一些。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
1.理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象,那么这个方法就返回true。
ECMAScript5新增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。使用delete操作符则可完全删除实例属性,从而让我们能够重新访问原型中的属性。
使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。只在给定属性存在于对象实例中时,才会返回true;
2.原型与in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
3.更简单的原型语法
用一个包含所有属性和方法的对象字面量来重新整个对象。
4.原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例中反映出来——即使是先创建了实例后修改原型也是如此。
5.原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
6.原型对象的问题
原型模式最大的问题是由其共享的本性所导致的。
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者防止数据被其他应用程序(如Mashup程序)改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一时新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。
许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
给原型添加方法的代码一定要放在替换原型的语句之后。
在通过原型链来实现继承时,不能使用字面量创建原型方法。因为这样做就会重写原型链。
原型链实现继承最主要的问题来自包含引用类型值的原型。第二个问题是在创建子类型的实例时,不能向超类型的构造函数传递参数。
借用构造函数(constructor straling)技术(有时候也叫做伪造对象或经典继承)。其基本思想即:在子类型构造函数的内部调用超类型构造函数。
在子类中使用call()或apply()调用父类构造函数。
借用构造函数的问题:仅仅借用构造函数,那么方法都在构造函数中定义,因此函数复用就无从谈起了。
组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
ECMAScript5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
寄生式继承:创建一个仅用于封装过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(subType,superType)
{
var prototype=object(superType.prototype);
prototype.constructor=subType;
subType.prototype=prototype;
}
JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型来实现的。