佛曰:对象本无根,类型亦无形。本来无一物,何处惹尘埃!
理解对象
- 一切事物皆对象;
- 对象具有封装和继承特性;
创建对象
- 通过 object 创建
1 | var person = new object(); |
- 通过字面量创建
1 | var person = { |
属性类型
数据属性
Configurable
能否删除、能否修改属性特性、能否把属性修改为访问器属性 默认为 trueEnumerable
能否通过 for-in 循环返回属性 默认为 trueWritable
能否修改属性的值 默认为 trueValue
包含属性的值。取值时从这个位置读,写入值时保存新值在这个位置。 默认值为 undefined
访问器属性
Configurable
同上Enumerable
同上Get
在读取属性时调用的函数。 默认值 undefinedSet
在写入属性时调用的函数。默认值 undefined
工厂模式
1 | function createPerson(name, age, job) { |
无数次调用函数 createPerson 都能够根据参数返回包含三个属性的的 Person 对象。缺点:怎样知道一个对象的类型
构造函数模式
1 | function Person(name, age, job) { |
和工厂模式的不同之处:
- 没有显式地创建对象
- 直接将属性和方法赋给了 this 对象
- 没有 return 语句
- 函数的首字母大写
创建 Person 的新实例,必须使用的 new 操作符。
new 的过程有 4 个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(this 指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
每个实例都有一个 constructor(构造函数)属性,该属性指向 Person。
使用 instanceof 检测:
1 | alert(person1 instanceof Object) //true |
作为普通函数调用:
1 | // 作为普通函数调用 |
构造函数的缺点:方法不共享,每个实例都有自己的方法。
临时解决方案:(把方法放在对象外面)
1 | function Person(name, age, job) { |
原型模式
我们创建的每一个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。照字面意思理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。好处:可以让所有对象实例共享它所包含的属性和方法。
示例:
1 | function Person() {} |
理解原型对象
无论什么时候,只要创建一个新函数,就会根据一组特定规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象,在默认情况下所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。其实 Person.prototype.constructor 指向 Person。
创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性,其他方法都是从 object 继承而来的。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫Prototype
,这个属性没有标准的访问方式,在执行过程中是不可见的。浏览器中调试可以看见其实就是_proto_ 。
要明确的真正重要的一点是:这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
图示:
可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。如果Prototype
指向调用 isPrototypeOf()方法对象(Person.prototype),那么方法返回 true。
1 | alert(Person.prototype.isPrototypeOf(person1); //true |
ECMAScript5 新增方法 object.getPrototypeOf(),此方法返回Prototype
的值。
1 | alert(object.getPrototypeOf(person1) == Person.prototype) //true |
多个对象实例共享原型对象的属性和方法的原理:
每当对象读取某个对象的某个属性是,都会执行一次搜索,目标就是给定名字的属性,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值。如果没找到,则继续搜索指针指向的原型对象,原型对象中查找给定名字的属性,如果在原型对象中查找到给定名字的属性,则返回该属性的值。如果原型对象中也没找到给定名字的属性,最后返回 undefined。
原型最初只包含 constructor,而该属性也是共享的,因此可以通过对象实例访问。
虽然对象实例可以访问原型对象中的值,但是对象实例是不可以重写原型对象中的值,只可以‘屏蔽’原型对象中同名属性。
1 | function Person() {} |
使用 delete 可以完全删除实例属性,实例可以重新访问原型对象中属性。
1 | delete person1.name |
使用 hasOwnPrototype()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(从 object 继承来的)只在给定属性存在于对象实例中时,才会返回 true。
1 | alert(person1.hasOwnPrototype('name')) //false |
图示:
原型与 in 操作符
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。
在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true。(该属性无论存在实例中还是原型对象中)
1 | person1.name = 'Greg' |
同时使用 hasOwnPrototype()和 in 操作符,就可以确定该属性到底存在实例中,还是原型对象中。
1 | function hasPrototypePrototy(object, name) { |
使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,包括实例和原型对象中的属性。获取可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys()方法。这个方法接收一个对象参数,返回一个包含所有可枚举属性的字符串数组。
1 | function Person() {} |
如果想要得到所有实例属性,无论它是否可以枚举,都可以使用 Object.getOwnPropertyNames()方法。
1 | var keys = Object.getOwnPropertyNames(Person.prototype) |
结果中包含了可枚举的 constructor 属性。Object.keys()和 Object.getOwnPropertyNames()都可以替代 for-in 循环。
更简单原型语法
用包含所有属性和方法的对象字面量来重写整个原型对象:
1 | function Person() {} |
最终结果相同但是有一个例外,constructor 属性不再指向 Person 了,这种语法本质上完全重写了默认的 prototype 对象。因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 构造函数。尽管 instanceof 操作符还能返回正确的结果,但通过 constructor 已经无法确定对象类型了。
1 | var friend = new Person(); |
可以特意将 constructor 属性设置回适当的值:
1 | function Person() {} |
以这种方式重设 constructor 属性会导致它的Enumerable
特性被设置为 true。默认情况下,原生的 constructor 属性是不可枚举的。
ECMAScript5 的 JavaScript 引擎,可以用 Object.defineProperty()重置Enumerable
特性。
1 | Object.defineProperty(Person.prototype, 'constructor', { |
原型的动态性
由于在原型中查找值得过程是一次搜索,因此我们对原型对象所做的任何修改都能立即从实例上反应出来。先创建实例后修改原型也照样如此:
1 | var friend = new Person() |
我们调用 sayHi()时,首先去实例中搜索名为 sayHi 属性,找不到就会继续搜索原型对象。实例和原型对象之间的连接只是一个指针,而非一个副本,因此就可以在原型对象中找到新的 sayHi 属性,并返回保存在那里的函数。
如果我们重写了原型对象,那么情况就不一样了。我们知道,调用函数时会为函数添加一个指向原型对象的Prototype
指针(即_proto_),而把原型指向另一个对象就等于切断了构造函数与原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。
1 | function Person() {} |
这个例子中,我们先创建了 Person 的一个实例,然后又重写了其原型对象。然后在调用 friend.sayName()时发生了错误,因为 friend 指向的原型中不包含以该名字命名的属性。
从图上看,重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
原型对象的缺点
原型中的所有属性是被很多实例共享,这种共享对于函数非常合适。对于引用类型的属性就会导致多个实例共享同一个引用类型的属性,其中一个实例修改了这个属性,这个属性也会反映到其他实例中。实例一般都是要有属于自己的全部属性的。
1 | function Person() {} |
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。每一个实例都会有自己的一份实例副本,但同时共享着对方法的引用,节省内存,支持向构造函数传递参数;可谓是集两种模式之长。
1 | function Person(name, age, job) { |
动态原型模式
这个模式是把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
1 | function Person(name, age, job) { |
使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
寄生构造函数模式
1 | function Person(name, age, job) { |
Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。
继承
OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,所以 ECMAScript 只支持实现继承,而且其实现继承主要依靠原型链来实现的。
原型链
基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针constructor
,而实例都包含一个指向原型内部的指针。假如让原型对象等于另一个类型的实例,原型对象将包含一个指向另一个原型的指针,相应地,另一个原型也包含一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。
1 | function SuperType() { |
继承是通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。
所有引用类型默认都继承了 Object,而这个继承也是通过原型实现的。