JavaScript
是一种基于原型的面向对象的语言,而不是基于类的。
基于类的面向对象的语言,是构建在两个不同实体的概念之上的:类和实例
基于原型的语言不存在这种区别:只有对象。基于原型的语言具有所谓原型对象rototypical object
的概念。原型对象可以作为一个模板,新对象可以从中获得原始的属性。任何对象都可以指定其自身的属性,既可以是创建时也可以在运行时创建。而且,任何对象都可以作为另一个对象的原型prototype
,从而允许后者共享前者的属性。
定义
在基于类的语言中,需要专门的类定义符class definition
定义类。
JavaScript
也遵循类似的模型,但却不同于基于类的语言。在JavaScript
中你只需要定义构造函数来创建具有一组特定的初始属性和属性值的对象。任何JavaScript
函数都可以用作构造器。 也可以使用new
操作符和构造函数来创建一个新对象。
子类与继承
基于类的语言是通过对类的定义中构建类的层级结构的。
JavaScript
通过将构造器函数与原型对象相关联的方式来实现继承。
添加和移除属性
基于类的语言中,通常在编译时创建类,然后在编译时或者运行时对类的实例进行实例化。一旦定义了类,无法对类的属性进行更改。
在JavaScript
中,允许运行时添加或者移除任何对象的属性。如果您为一个对象中添加了一个属性,而这个对象又作为其它对象的原型,则以该对象作为原型的所有其它对象也将获得该属性。
例子
Employee
具有name
属性(默认值为空的字符串)和dept
属性(默认值为general
)Manager
是Employee
的子类。它添加了reports
属性(默认值为空的数组,以Employee
对象数组作为它的值)WorkerBee
是Employee
的子类。它添加了projects
属性(默认值为空的数组,以字符串数组作为它的值SalesPerson
是WorkerBee
的子类。它添加了quota
属性(其值默认为100
)。它还重载了dept
属性值为sales
,表明所有的销售人员都属于同一部门Engineer
基于WorkerBee
。它添加了machine
属性(其值默认为空的字符串)同时重载了dept
属性值为engineering
实现
Employee的定义
Manager 和 WorkerBee的定义
Manager
和WorkerBee
的定义表示在如何指定继承链中上一层对象,在JavaScript
中,添加一个原型实例作为构造器函数prototype
属性的值,而这一动作可以在构造器函数定义后的任意时刻执行。
Engineer 和 SalesPerson的定义
在对Engineer
和SalesPerson
定义时,创建了继承自WorkerBee
的对象,该对象会进而继承自Employee
。这些对象会具有在这个链之上的所有对象的属性。另外,它们在定义时,又重载了继承的dept
属性值,赋予新的属性值。
对象的属性
继承属性
我们创建一个WorkerBee
的实例:
var mark = new WorkerBee;
当JavaScript
发现new
操作符时,它会创建一个通用generic
对象,并将其作为关键字this
的值传递给WorkerBee
的构造器函数。该构造器函数显式地设置projects
属性的值,然后隐式地将其内部的__proto__
属性设置为WorkerBee.prototype
的值(属性的名称前后均有两个下划线)。__proto__
属性决定了用于返回属性值的原型链。一旦这些属性设置完成,JavaScript
返回新创建的对象,然后赋值语句会将变量mark
的值指向该对象。
prototype
是构造器函数的一个属性,而__proto__
是对象的(实例的)一个属性
这个过程不会显式的将mark
所继承的原型链中的属性值作为本地变量存放在mark
对象中。当请求属性的值时,JavaScript
将首先检查对象自身中是否存在属性的值,如果有,则返回该值。如果不存在,JavaScript
会通过__proto__
对原型链进行检查。如果原型链中的某个对象包含该属性的值,则返回这个值。如果没有找到该属性,JavaScript
则认为对象中不存在该属性。这样,mark
对象中将具有如下的属性和对应的值:
mark.name = "";
mark.dept = "general";
mark.projects = [];
添加属性
在JavaScript
中,您可以在运行时为任何对象添加属性,而不必受限于构造器函数提供的属性。添加特定于某个对象的属性,只需要为该对象指定一个属性值:
mark.bonus = 3000;
这样mark
对象就有了bonus
属性,而其它WorkerBee
则没有该属性。
如果向某个构造器函数的原型对象中添加新的属性,那么该属性将添加到从这个原型中继承属性的所有对象的中:
Employee.prototype.specialty = "none";
更灵活的构造器
如何实现构造器函数在创建新的实例时指定属性值。
使用一种设置默认值的特殊惯用方法:
this.name = name || "";
注意: 由上面的定义,您无法为继承属性指定初始值。如果想在
JavaScript
中为继承的属性指定初始值,您需要在构造器函数中添加更多的代码。
下面是Engineer
构造器的定义:
function Engineer (name, projs, mach) {
this.base = WorkerBee;
this.base(name, "engineering", projs);
this.machine = mach || "";
}
假设创建了一个新的Engineer
对象:
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
执行时,会有以下步骤:
new
操作符创建了一个新的通用对象,并将其__proto__
属性设置为Engineer.prototype
new
操作符将该新对象作为this
的值传递给Engineer
构造器- 构造器为该新对象创建了一个名为
base
的新属性,并指向WorkerBee
的构造器。这使得WorkerBee
构造器成为Engineer
对象的一个方法。 - 构造器调用
base
方法,将传递给该构造器的参数中的两个,作为参数传递给base
方法,同时还传递一个字符串参数"engineering"
。显式地在构造器中使用"engineering"
表明所有Engineer
对象继承的dept
属性具有相同的值,且该值重载了继承自Employee
的值。 - 因为
base
是Enginee
的一个方法,在调用base
时,JavaScript
将在步骤1中创建的对象绑定给this
关键字。这样,WorkerBee
函数接着将"Doe, Jane"
和"engineering"
参数传递给Employee
构造器函数。当从Employee
构造器函数返回时,WorkerBee
函数用剩下的参数设置projects
属性 - 当从
base
方法返回后,Engineer
构造器将对象的machine
属性初始化为"belau"
- 当从构造器返回时,
JavaScript
将新对象赋值给jane
变量
继承的另一种途径是使用call()/apply()
方法。
function Engineer (name, projs, mach) {
WorkerBee.call(this, name, "engineering", projs);
this.machine = mach || "";
}
这和上面的例子是等价的
再谈属性的继承
本地值与继承值
function Employee () {
this.name = "";
this.dept = "general";
}
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
var amy = new WorkBee;
Employee.prototype.name = "Unknown"
乍一看,可能觉得新的值Unknown
会传播给所有Employee
的实例。然而,并非如此。
在创建Employee
对象的任意实例时,该实例的name
属性将获得一个本地值(空的字符串)。因此,当JavaScript
查找amy
对象的name
属性时,JavaScript
将找到 orkerBee.prototype
中的本地值,也就不会继续在原型链中向上找到Employee.prototype
了。
如果想在运行时修改一个对象的属性值并且希望该值被所有该对象的后代所继承,您就不能在该对象的构造器函数中定义该属性。而应该将该属性添加到该对象所关联的原型中。
function Employee () {
this.dept = "general";
}
Employee.prototype.name = "";
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
var amy = new WorkerBee;
Employee.prototype.name = "Unknown";
判断实例的关系
每个对象都有一个__proto__
对象属性(除了Object
);每个函数都有一个prototype
对象属性。因此,通过原型继承(prototype inheritance)
,对象与其它对象之间形成关系。通过比较对象的__proto__
属性和函数的prototyp
属性可以检测对象的继承关系。JavaScript
提供了便捷方法:instanceof
操作符可以用来将一个对象和一个函数做检测,如果对象继承自函数的原型,则该操作符返回真。
var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");
对于该对象,以下所有语句均为真:
chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;
构造器的全局函数
没有多继承
某些面向对象语言支持多重继承。也就是说,对象可以从无关的多个父对象中继承属性和属性值。JavaScript
不支持多重继承。
在JavaScript
中,可以在构造器函数中调用多个其它的构造器函数。这一点造成了多重继承的假象。
function Hobbyist (hobby) {
this.hobby = hobby || "scuba";
}
function Engineer (name, projs, mach, hobby) {
this.base1 = WorkerBee;
this.base1(name, "engineering", projs);
this.base2 = Hobbyist;
this.base2(hobby);
this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
var dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")
dennis
确实从Hobbyist
构造器中获得了hobby
属性。但是,假设添加了一个属性到 Hobbyist`构造器的原型:
Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
dennis
对象不会继承这个新属性。