js继承
对象的属性
本节将讨论对象如何从原型链中的其它对象中继承属性,以及在运行时添加属性的相关细节。
继承属性
假设您通过如下语句创建一个mark
对象作为 WorkerBee
的实例:
var mark = new WorkerBee;
当 JavaScript 执行 new
操作符时,它会先创建一个普通对象,并将这个普通对象中的 [[prototype]] 指向 WorkerBee.prototype
,然后再把这个普通对象设置为执行 WorkerBee
构造函数时 this
的值。该普通对象的 [[Prototype]] 决定其用于检索属性的原型链。当构造函数执行完成后,所有的属性都被设置完毕,JavaScript 返回之前创建的对象,通过赋值语句将它的引用赋值给变量 mark
。
这个过程不会显式的将 mark
所继承的原型链中的属性作为本地属性存放在 mark
对象中。当访问属性时,JavaScript 将首先检查对象自身中是否存在该属性,如果有,则返回该属性的值。如果不存在,JavaScript会检查原型链(使用内置的 [[Prototype]] )。如果原型链中的某个对象包含该属性,则返回这个属性的值。如果遍历整条原型链都没有找到该属性,JavaScript 则认为对象中不存在该属性,返回一个 undefined
。这样,mark
对象中将具有如下的属性和对应的值:
mark.name = ""; mark.dept = "general"; mark.projects = [];
mark
对象从 mark.__proto__
中保存的原型对象里继承了 name
和 dept
属性。并由 WorkerBee
构造函数为 projects
属性设置了本地值。 这就是 JavaScript 中的属性和属性值的继承。这个过程的一些微妙之处将在 Property inheritance revisited 中进一步讨论。
由于这些构造器不支持为实例设置特定的值,所以这些属性值仅仅是创建自 WorkerBee
的所有对象所共享的默认值。当然这些属性的值是可以修改的,所以您可以为 mark
指定特定的信息,如下所示:
mark.name = "Doe, Mark"; mark.dept = "admin"; mark.projects = ["navigator"];
1.必须显式地设置原型才能确保动态的继承
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } Engineer.prototype = new WorkerBee; var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau"); Employee.prototype.specialty = "none";
现在 jane
对象的 specialty
属性为 "none" 了
2.继承的另一种途径是使用call()
/ apply()
方法。下面的方式都是等价的:
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } function Engineer (name, projs, mach) { WorkerBee.call(this, name, "engineering", projs); this.machine = mach || ""; }
再谈属性的继承
前面的小节中描述了 JavaScript 构造器和原型如何提供层级结构和继承的实现。本节中对之前未讨论的一些细节进行阐述。
本地值和继承值
在访问一个对象的属性时,JavaScript 将执行下面的步骤:
- 检查对象自身是否存在。如果存在,返回值。
- 如果本地值不存在,检查原型链(通过
__proto__
属性)。 - 如果原型链中的某个对象具有指定属性,则返回值。
- 如果这样的属性不存在,则对象没有该属性,返回 undefined。
以上步骤的结果依赖于你是如何定义的。最早的例子中有如下定义:
function Employee () { this.name = ""; this.dept = "general"; } function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee;
基于这些定义,假定通过如下的语句创建 WorkerBee
的实例 amy
:
var amy = new WorkerBee;
则 amy
对象将具有一个本地属性 projects
。name
和 dept
则不是 amy
对象的本地属性,而是从 amy
对象的 __proto__
属性获得的。因此,amy
将具有如下的属性值:
amy.name == ""; amy.dept == "general"; amy.projects == [];
现在,假设修改了与 Employee
的相关联原型中的 name
属性的值:
Employee.prototype.name = "Unknown"
乍一看,你可能觉得新的值会传播给所有 Employee
的实例。然而,并非如此。
在创建 Employee
对象的任意实例时,该实例的 name
属性将获得一个本地值(空的字符串)。这就意味着在创建一个新的 Employee
对象作为 WorkerBee
的原型时,WorkerBee.prototype
的 name
属性将具有一个本地值。因此,当 JavaScript 查找 amy
对象(WorkerBee
的实例)的 name
属性时,JavaScript 将找到 WorkerBee.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";
在这种情况下,amy
的 name
属性将为 "Unknown"。
正如这些例子所示,如果希望对象的属性具有默认值,并且希望在运行时修改这些默认值,应该在对象的原型中设置这些属性,而不是在构造器函数中。
判断实例的关系
JavaScript 的属性查找机制首先在对象自身的属性中查找,如果指定的属性名称没有找到,将在对象的特殊属性 __proto__
中查找。这个过程是递归的;被称为“在原型链中查找”。
特殊的 __proto__
属性是在构建对象时设置的;设置为构造器的 prototype
属性的值。所以表达式 new Foo()
将创建一个对象,其 __proto__ ==
。因而,修改 Foo.prototype
Foo.prototype
的属性,将改变所有通过 new Foo()
创建的对象的属性的查找。
每个对象都有一个 __proto__
对象属性(除了 Object);每个函数都有一个
prototype
对象属性。因此,通过“原型继承”,对象与其它对象之间形成关系。通过比较对象的 __proto__
属性和函数的 prototype
属性可以检测对象的继承关系。JavaScript 提供了便捷方法:instanceof
操作符可以用来将一个对象和一个函数做检测,如果对象继承自函数的原型,则该操作符返回真。例如:
var f = new Foo(); var isTrue = (f instanceof Foo);
创建 Engineer
对象如下:
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;
基于此,可以写出一个如下所示的 instanceOf
函数:
function instanceOf(object, constructor) { while (object != null) { if (object == constructor.prototype) return true; if (typeof object == 'xml') { return constructor.prototype == XML.prototype; } object = object.__proto__; } return false; }
Note: 在上面的实现中,检查对象的类型是否为 "xml" 的目的在于解决新近版本的 JavaScript 中表达 XML 对象的特异之处。如果您想了解其中琐碎细节,可以参考 bug 634150。
使用上面定义的 instanceOf 函数,这些表达式为真:
instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)
但如下表达式为假:
instanceOf (chris, SalesPerson)
构造器中的全局信息
在创建构造器时,在构造器中设置全局信息要小心。例如,假设希望为每一个雇员分配一个唯一标识。可能会为 Employee
使用如下定义:
var idCounter = 1; function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; this.id = idCounter++; }
基于该定义,在创建新的 Employee
时,构造器为其分配了序列中的下一个标识符。然后递增全局的标识符计数器。因此,如果,如果随后的语句如下,则 victoria.id
为 1 而 harry.id
为 2:
var victoria = new Employee("Pigbert, Victoria", "pubs") var harry = new Employee("Tschopik, Harry", "sales")
乍一看似乎没问题。但是,无论什么目的,在每一次创建 Employee
对象时,idCounter
都将被递增一次。如果创建本章中所描述的整个 Employee
层级结构,每次设置原型的时候,Employee
构造器都将被调用一次。假设有如下代码:
var idCounter = 1; function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; this.id = idCounter++; } function Manager (name, dept, reports) {...} Manager.prototype = new Employee; function WorkerBee (name, dept, projs) {...} WorkerBee.prototype = new Employee; function Engineer (name, projs, mach) {...} Engineer.prototype = new WorkerBee; function SalesPerson (name, projs, quota) {...} SalesPerson.prototype = new WorkerBee; var mac = new Engineer("Wood, Mac");
还可以进一步假设上面省略掉的定义中包含 base
属性而且调用了原型链中高于它们的构造器。即便在现在这个情况下,在 mac
对象创建时,mac.id
为 5。
依赖于应用程序,计数器额外的递增可能有问题,也可能没问题。如果确实需要准确的计数器,则以下构造器可以作为一个可行的方案:
function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; if (name) this.id = idCounter++; }
在用作原型而创建新的 Employee
实例时,不会指定参数。使用这个构造器定义,如果不指定参数,构造器不会指定标识符,也不会递增计数器。而如果想让 Employee
分配到标识符,则必需为雇员指定姓名。在这个例子中,mac.id
将为 1。
或者,您可以创建一个 Employee 的原型对象的副本以分配给 WorkerBee:
WorkerBee.prototype = Object.create(Employee.prototype); // instead of WorkerBee.prototype = new Employee
没有多重继承
某些面向对象语言支持多重继承。也就是说,对象可以从无关的多个父对象中继承属性和属性值。JavaScript 不支持多重继承。
JavaScript 属性值的继承是在运行时通过检索对象的原型链来实现的。因为对象只有一个原型与之关联,所以 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")
进一步假设使用本章前面所属的 WorkerBee
的定义。此时 dennis
对象具有如下属性:
dennis.name == "Doe, Dennis" dennis.dept == "engineering" dennis.projects == ["collabra"] dennis.machine == "hugo" dennis.hobby == "scuba"
dennis
确实从 Hobbyist
构造器中获得了 hobby
属性。但是,假设添加了一个属性到 Hobbyist
构造器的原型:
Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
dennis
对象不会继承这个新属性。