ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或函数”。
一、理解对象
创建自定义对象的最简单方式就是创建一个Object的实例,然后再为它添加属性和方法。
1 属性类型
1)数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。
- [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
- [[Enumerable]]:表示能否通过for-in循环返回属性。
- [[Writeable]]:表示能否修改属性的值。
- [[Value]]:包含这个属性的数据值。
要修改属性默认的特性,必须使用ECMAScript 5的Object.defineProperty()方法。这个方法接受三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value。
2)访问器属性
访问器属性不包含数据值:它们包含一对儿getter和setter函数。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。
- [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。
- [[Enumerable]]:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true。
- [[Get]]:在读取属性时调用的函数。
- [[Set]]:在写入属性时调用的函数。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
不一定非要同时指定getter和setter。只指定getter意味着属性是不能写,尝试写入属性会被忽略,在严格模式下,尝试写入只指定了getter函数的属性会抛出错误。类似地,只指定setter函数的属性也不能读,否则在非严格模式下会返回undefined,而在严格模式下会抛出错误。
2 定义多个属性
由于为对象定义多个属性的可能性很大,ECMAScript 5又定义了一个Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接受两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。
3 读取属性的特性
使用ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接受两个参数:属性所在的对象和读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configuration、enumerable、writable和value。
二、创建对象
1 工厂模式
function createPerson(name, age, job) { var o = { name: name, age: age, job: job, sayName: function() { alert(this.name); } }; return o; } var person = createPerson(‘Greg‘, 27, ‘Doctor‘);
2 构造函数模式
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } var person = new Person(‘Greg‘, 27, ‘Doctor‘);
与工厂模式不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了this对象;
- 没有return语句。
以这种方式调用构造函数实际上会经历以下4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象;
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
1)将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。
//当作构造函数使用 var person = new Person(‘Greg‘, 27, ‘Doctor‘); person.sayName();//"Grey" //作为普通函数调用 Person("Grey", 27, "Doctor"); window.sayName();//"Grey" //在另一个对象的作用域中调用 var o = new Object(); Person.call(o, "kristen", 25, "Nurse"); o.sayName();
2)构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
3 原型模式
每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
1)理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。
创建了自定义的构造函数以后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本则是完全不可见的。
虽然在所有实现中都无法访问到[[Prototype]],但可以通过isPrototype()方法来确定对象之间是否存在这种关系。
ECMAScript 5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在示例中添加了一个属性,而该属性会与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。
使用hasOwnProperty()方法可以检测一个属性是存在于示例中,还是存在在原型中。这个方法(不要忘了它是从Object继承而来的)只在给定属性存在于对象示例中时,才会返回true。
2)原型与in操作符
有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于示例中还是原型中。
在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中即包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的。
要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5的Object.keys()方法。这个方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()。
3)更简单的原型语法
function Person() { } Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function() { alert(this.name); } } //重设构造函数,只适用于ECMAScript 5兼容浏览器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person }
4)原型的动态性(重要)
调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数于最初原型之间的联系。
function Person() { } var friend = new Person(); Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName: function() { alert(this.name); } }; friend.sayName();
在这个例子中,我们先创建了Person的一个实例,然后又重写了其原型对象。然后在调用friend.sayName()时发生错误,因为friend指向的原型中不包含以该名字命名的属性。
5)原型对象的问题
原型对象的最大问题是由其共享的本性所导致的。
4 组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor: Person, sayName: function() { alert(this.name); } }
5 动态原型模式
动态原型模式把所有信息都封装在了构造函数中。
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); } } }
三、继承
1 原型链
原型链是实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.prototype; } function SubType() { this.subprototype = false; } //继承了SuperType SubType.prototype = new SubType();
1)确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true。
2)原型链的问题
原型链虽然很大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。
2 借用构造函数
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一个叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪类对象或经典继承)。这中技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { SuperType.call(this); }
1)传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
2)借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式中存在的问题——方法都在都在构造函数中定义,因此函数复用就无从谈起了。
3 组合继承
组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { alert(this); } function SubType(name, age) { //继承属性 SuperType.call(this, name); this.age = age; } //继承方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { alert(this.age); };
4 原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o) { function F() {} F.prototype = o; return new F(); }
在object()函数内部,先创建了一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个实例。从本质上讲,object()传入其中的对象执行了一次浅复制。