你的浏览器不支持canvas

Love You Ten Thousand Years

JavaScript的对象模型

Date: Author: M/J

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。转载请注明来自小可嗒嗒的博客

JavaScript是一种基于原型的面向对象的语言,而不是基于类的。

基于类的面向对象的语言,是构建在两个不同实体的概念之上的:类和实例

基于原型的语言不存在这种区别:只有对象。基于原型的语言具有所谓原型对象rototypical object的概念。原型对象可以作为一个模板,新对象可以从中获得原始的属性。任何对象都可以指定其自身的属性,既可以是创建时也可以在运行时创建。而且,任何对象都可以作为另一个对象的原型prototype,从而允许后者共享前者的属性。

定义

在基于类的语言中,需要专门的类定义符class definition定义类。

JavaScript也遵循类似的模型,但却不同于基于类的语言。在JavaScript中你只需要定义构造函数来创建具有一组特定的初始属性和属性值的对象。任何JavaScript函数都可以用作构造器。 也可以使用new操作符和构造函数来创建一个新对象。

子类与继承

基于类的语言是通过对类的定义中构建类的层级结构的。

JavaScript通过将构造器函数与原型对象相关联的方式来实现继承。

添加和移除属性

基于类的语言中,通常在编译时创建类,然后在编译时或者运行时对类的实例进行实例化。一旦定义了类,无法对类的属性进行更改。

JavaScript中,允许运行时添加或者移除任何对象的属性。如果您为一个对象中添加了一个属性,而这个对象又作为其它对象的原型,则以该对象作为原型的所有其它对象也将获得该属性。


例子

employee

  • Employee具有name属性(默认值为空的字符串)和dept属性(默认值为general
  • ManagerEmployee的子类。它添加了reports属性(默认值为空的数组,以Employee对象数组作为它的值)
  • WorkerBeeEmployee的子类。它添加了projects属性(默认值为空的数组,以字符串数组作为它的值
  • SalesPersonWorkerBee的子类。它添加了quota属性(其值默认为100)。它还重载了dept属性值为sales,表明所有的销售人员都属于同一部门
  • Engineer基于WorkerBee。它添加了machine属性(其值默认为空的字符串)同时重载了dept属性值为engineering

实现

Creating the hierarchy

Employee的定义

Manager 和 WorkerBee的定义

ManagerWorkerBee的定义表示在如何指定继承链中上一层对象,在JavaScript中,添加一个原型实例作为构造器函数prototype属性的值,而这一动作可以在构造器函数定义后的任意时刻执行。

Engineer 和 SalesPerson的定义

在对EngineerSalesPerson定义时,创建了继承自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";

更灵活的构造器

如何实现构造器函数在创建新的实例时指定属性值。

more flexible constructors

使用一种设置默认值的特殊惯用方法:

this.name = name || "";

注意: 由上面的定义,您无法为继承属性指定初始值。如果想在JavaScript中为继承的属性指定初始值,您需要在构造器函数中添加更多的代码。

Specifying properties in a constructor

下面是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的值。
  • 因为baseEnginee的一个方法,在调用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对象不会继承这个新属性。


对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。