本章内容
- 理解对象属性
- 理解并创建对象
- 理解继承
什么是面向对象?
面向对象(Object Oriented, OO)是软件开发的一种思想,或者说是一种大局观从总体走向上把握软件的开发。与之有别的,还有面向过程。借鉴一个例子,来说明一下这两种思想的不同——面向过程是编年体,面向对象是纪传体。仔细品味一下,能发现面向过程针对的客体是物(事情的变化过程),而面向对象针对的客体是人(具有状态与行为的生命有机体)。 每种思想流派的出现都是为了适应并改变当时的环境的,对比面向过程和面向对象,我们发现后者更加符合人的逻辑,更加贴合人的思考问题的模式。所以,在编程语言的后续发展中,面向对象也融入了语言的设计理念当中了。 但只要面向对象这个模糊的概念,显然不利于进一步的发展,所以业界也讨论出了关于面向对象思想的三大要素:封装,继承和多态。
- 封装:类似于飞机的黑盒子,你能从里面得到处理后的数据,但是你不知道数据在里面是怎么被怎样处理的。
- 继承:待补充
- 多态:待补充
面向对象语言里面的类
面向对象语言有一个标志,那就是它们都有类的概念。而通过类可以创建任意个拥有相同属性和方法的对象,而通过类就可以实现对象的继承,例如Python中的类。但是JavaScript中并没有采用类继承这一方法,而是采用原型链这个较为冷门的方法来实现继承。因此ECMA-262中并没有类的概念,它的对象和基于类的语言中的对象也有所不同。
JS中对于对象的定义
ECMA-262中将对象定义为:”无序属性的集合,其属性可以包含基本值、对象或者函数。“
严格来讲。这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样,我们可以将ECMAScript的对象想象成散列表:无非是一组键值对(key-value),其中值可以是数据或者函数。
**每个对象都是基于一个引用类型创建的。**这个引用类型可以是原生类型(Obejct
,Array
等),也可以是开发人员定义的类型。
创建单个对象的两种模式
创建自定义对象的最简单方式就是创建一个Object
的实例,然后为它添加属性和方法。
var dog = new Object();
dog.name = "小白";
dog.color = "white";
dog.bark = function() {
alert("汪汪汪")
}
早期的JavaScript开发人员经常使用这个模式创建新对象。几年后,对象字面量成为创建这种对象的首选模式。上面的例子,用对象字面量语法可以写成这样:
var dog = {
name: "小白",
color: "white",
bark: function() {
alert("汪汪汪")
}
}
对象的两种属性(待完善)
ECMScript中有两种属性:数据属性和访问器属性,可以认为是为对象中的数据和方法的抽象。
数据属性有4个描述行为的特性,规定该数据是否能被delete删除,是否能被枚举,是否能被写入修改等。
同一访问器属性也有对应的特性,其中需要注意的是一堆getter和setter函数,这里我们要加深思考一下,当我们使用.
语法获取对象中的时,在对象内部发生了什么,为什么就恰好把我们看到的对应的value值取出来了。
创建大量对象的模式
虽然Object
构造函数或对象字面量都可以用来创建单个对象但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量重复的代码。为了解决这个问题,开发人员开始使用工厂模式的一种变体。
- 工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面的例子所示。
function createDog(name, color) {
var o = new Object();
o.name = name;
o.color = color;
o.bark = function() {
alert(this.name)
};
return o;
}
var dog1 = createDog("小白", "white");
var dog2 = createDog("小黄", ”yellow“);
函数createDog()能够根据接受的参数来构建一个包含所有必要信息的dog对象。可以无数次地调用这个函数,而每次它都会返回一个包含两个属性一个方法的对象。 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。随着JavaScript的发展,又一个新模式出现了。
- 构造函数模式
ECMAScript中的构造函数可用来创建特定类型的对象。像
Object
和Array
这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如,可以使用构造函数模式将前面的例子重写如下。
function Dog(name, color) {
this.name = name;
this.color = color;
this.bark = function() {
alert(this.name)
}
}
var dog1 = new Dog(''小白", "white");
var dog2 = new Dog("小黄", "yellow");
在这个例子中,Dog()函数取代了createDog()函数。我们注意到,Dog()中的代码除了与createDog()中相同的部分外,还存在以下不同之处:
1.没有显示地创建对象;
2. 直接将属性和方法赋给了this
对象;
3. 没有return
语句。
按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。 **注意:构造函数本身也是函数,只不过可以用来创建对象而已。**要通过构造函数创建实例,必须使用new
操作符。
构造函数的注意与问题
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new
操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new
操作符来调用,那它跟普通函数也不会有什么两样。
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,dog1和dog2都有一个名为bark()的方法,但那两个方法不是同一个Function
的实例。**不要忘了——ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。**从逻辑角度讲,此时的构造函数也可以这样定义。
function Dog(name, color) {
this.name = name;
this.color = color;
this.bark = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的
}
从这个角度上来看构造函数,更容易明白每个Dog实例都包含一个不同的Function
实例(以显示name属性)的本质。然而,创建两个完成同样任务的Function
实例的确没有必要;况且有this
对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
function Dog(name, color) {
this.name = name;
this.color = color;
this.bark = bark;
}
function bark() {
alert(this.name)
}
var dog1 = new Dog(''小白", "white");
var dog2 = new Dog("小黄", "yellow");
在这个例子中,我们把bark()函数的定义转移到了构造函数外部。而在构造函数内部,我们将bark属性设置成等于全局的bark函数。这样一来,由于bark包含的是一个指向函数的指针,因此dog1和dog2对象就共享了在全局作用域中定义的同一个bark()函数。 这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。
原型模式
我们创建的每个函数都有一个prototype
(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示。
function Dog() {
}
Dog.prototype,name = "小黄";
Dog.prototype.color = "white";
Dog.prototype.bark = function() {
alert(this.name)
}
var dog1 = new Dog();
dog1.bark(); // 小黄
var dog2 = new Dog();
dog2.bark(); // 小黄
alert(dog1.bark() == dog2.bark()) // true
在此,我们将bark()方法和所有属性直接添加到了Dog
的prototype
属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,dog1和dog2访问的都是同一组属性和同一个bark()函数。
要理解原型模式的工作原理,必须先理解ECMAScript中原型对象的性质。
原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor
(构造函数)属性,这个属性包含一个指向prototype
属性所在函数的指针。就拿前面的例子来说,Dog.prototype. constructor
指向Dog
。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor
属性;至于其他方法,则都是从Object
继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__
;而在其他实现中,这个属性对脚本则是完全不可见的。
要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
代码搜索属性及方法的过程
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
继承与原型链
继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。 ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。 简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。