面向对象的程序设计

对象:无序属性的集合,其属性可以包含基本值、对象或者函数。无非就是一组名值对

一、理解对象

ECMA-262 第五版在定义只有内部才用的特性时,描述了属性的各种特征。定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。

有两种内部属性:数据属性和访问器属性。

数据属性:configurable、enumerable、writable 和 value。

访问器属性:configurable、enumerable、get 和 set。

要修改属性默认的特性:Object.defineProperty() 方法

定义多个属性:Object.defineProperties() 方法

读取属性的特性:Object.getOwnPropertyDescriptor() 方法

二、创建对象

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复的代码。为解决这个问题,人们开始使用工厂模式的一种变体。

1. 工厂模式

面向对象的程序设计_第1张图片

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2. 构造函数模式

构造函数可用来创建特定类型的对象。像 Object 和 Array 这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性的方法。例如,可以使用构造函数模式将前面的例子重写如下。

面向对象的程序设计_第2张图片

按照惯例,构造函数始终都应该以一个大写字母开头。构造函数本身也是函数,只不过可以用来创建对象而已。

要创建 Person 的新实例,必须使用 new 操作符。

在前面的例子的最后,person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都有一个 constructor(构造函数)属性,该属性指向 Person,如下所示

面向对象的程序设计_第3张图片

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1 和person2 都有一个名为 sayName() 的方法,但那两个方法不是同一个 Function 的实例。不要忘了——ECMAScript 中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。不同实例上的同名函数是不相等的。

通过把函数定义转移到构造函数外部来解决这个问题

面向对象的程序设计_第4张图片

这样做确实解决来两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

3. 原型模式

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

面向对象的程序设计_第5张图片

理解原型对象

在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性是一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor 指向 Person。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。这个指针叫 [[Prototype]]。

虽然在脚本中没有标准的方式访问这个属性,但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性 _proto_ ;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

虽然在所有实现中都无法访问到 [[Prototype]],但可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系。从本质上讲,如果 [[Prototype]] 指向调用 isPrototypeOf() 方法的对象(Person.prototype),那么这个方法就返回 true。

ECMAScript5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回 [[Prototype]] 的值。

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标时具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。这正是多个对象实例共享原型所保存的属性和方法的基本原理。

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中属性。

面向对象的程序设计_第6张图片

使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在对象实例中时,才会返回 true。

面向对象的程序设计_第7张图片

原型与 in 操作符

有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。

面向对象的程序设计_第8张图片

在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中即包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在 for-in 循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。

要取得对象上所有可枚举的实例属性,可以使用 ECMAScript5 的 Object.keys() 方法。这个方法接收一个对象作为参数,放回一个包含所有可枚举属性的字符串数组。

面向对象的程序设计_第9张图片

如果你想要得到所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames() 方法

面向对象的程序设计_第10张图片

结果中包含了不可枚举的 constructor 属性。Object.keys() 和 Object.getOwnPropertyNames()  方法都可以用来替代 for-in 循环。

更简单的原型语法

为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象

面向对象的程序设计_第11张图片

最终结果相同,但有一个例外:constructor 属性不再指向 Person 了。前面曾经介绍过,每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。

可以像下面这样特意将它设置回适当的值。

面向对象的程序设计_第12张图片

原型对象的问题

原型模式的最大问题是由其共享的本性所导致的。对于那些包含基本值的属性倒也说得过去,然而,对于包含引用类型值的属性来说,问题就比较突出了。

面向对象的程序设计_第13张图片

实例一般都是要有属于自己的全部属性的。

4. 组合使用构造函数模式和原型模式

创建自定义类型的最常见的方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

面向对象的程序设计_第14张图片

5. 动态原型模式

面向对象的程序设计_第15张图片

6. 寄生构造函数模式

通常在前述几种模式都不适用的情况下,可以使用寄生构造函数模式。

面向对象的程序设计_第16张图片

除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样。构造函数在不返回值的情况下,默认会返回新对象实例。而通过构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。建议在可以使用其他模式的情况下,不要使用这种模式。

7. 稳妥构造函数模式

不使用 this 和 new,适合在安全的环境中使用。

三、继承

1. 原型链

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

那么,假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系仍然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。

实现原型链有一种基本模式,其代码大致如下。

面向对象的程序设计_第17张图片

最终结果:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。

通过实现原型链,本质上扩展了前面介绍的原型搜索机制。当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继续的情况下,搜索过程就得以沿着原型链继续向上。在找不到属性或方法的情况下,搜索过程总要一环一环地前行到原型链末端才会停下来。

我们知道所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是 Object 的实例。

原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。

原型链的问题

最主要的问题来自包含引用类型值的原型。在通过原型实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。第二个问题是,在创建子类型的实例时,不能向超类型的构造函数中传递参数。

面向对象的程序设计_第18张图片

2. 借用构造函数

在子类型构造函数的内部调用超类型构造函数。

面向对象的程序设计_第19张图片

传递参数

面向对象的程序设计_第20张图片

借用构造函数的问题,方法都存在构造函数中定义,函数复用就无从谈起了。

3. 组合继承

指得是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。是 JavaScript 中最常用的继承模式。

面向对象的程序设计_第21张图片

4. 原型式继承

面向对象的程序设计_第22张图片

ECMAScript5 通过新增 Object.create() 方法规范化链原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create() 与上图的 Object() 方法的行为相同。

面向对象的程序设计_第23张图片

其本质是执行对给定对象的浅复制。

5. 寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。基于某个或某些信息创建一个对象,然后增强对象,最后返回对象。

面向对象的程序设计_第24张图片

6. 寄生组合式继承

前面说过,组合继承式 JavaScript 最常用的继承模式;不过它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次式在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

面向对象的程序设计_第25张图片

寄生组合式继承模式

面向对象的程序设计_第26张图片

普遍认为寄生组合式继承是引用类型最理想的继承模式

你可能感兴趣的:(面向对象的程序设计)