/ JavaScript

JavaScript 对象模型

JavaScript 的对象模型是基于原型的,与C++、Java 中基于类的对象模型有着很大的区别,特别是在对象属性与方法的继承机制上。

基于类的对象模型中,两个最重要的概念是类(class)与实例(instance):

  • class 定义了类中的所有属性与方法,可以看做该类所有实例的一个集合。
  • instance 是 class 的一个实例,该实例所具有的属性与方法由所属的类严格决定,不多不少。

而基于原型的对象模型中,没有 class 与 instance 的概念,所有东西都是 object。object 可以分为用作“模板”的 prototypical object,以及基于这些 prototypical object 使用 new 关键字创建的其他对象。对象的属性既可以在创建时指定,也可以在运行时指定。任何对象都可以作为其他对象的原型,让其他对象来“共享”该对象的属性。

1.JavaScript 的对象模型

类定义

基于类的对象模型中,需要使用 class 关键字显式地进行类的定义,并提供构造函数等方法(即使代码里没有写,编译器也会自动加上默认的构造函数)。

而 JavaScript 中构造函数并不与特定的类绑定,而只是为某个对象初始化一个特定的属性集。任何函数都可以作为构造函数。创建对象的时候,使用 new 关键字来调用某个函数即可。

子类(subclass)与继承

基于类的对象模型中,定义子类有着特定的语法,子类继承了父类的所有属性,并可以增加自己特有的属性。JavaScript 则可以将 prototypical object 与任意构造函数相关联,来达到继承的效果。(见后面的例子)

属性的增加与删除

在基于类的对象系统中,实例的属性是在编译时决定的,不能对实例的属性加以修改。

对于 JavaScript 中的 object,可以在运行时对其属性进行添加和删除。

2.一个例子

创建继承树

// 父类
function Employee() {
	this.name = '';
	this.dept = 'general';
}

// 儿子类
function Manager() {
	this.reports = [];
}
Manager.prototype = new Employee;

// 儿子类
function WorkerBee() {
	this.projects = [];
}
WorkerBee.prototype = new Employee;

// 孙子类
function SalesPerson() {
	this.dept = 'sales';
	this.quota = 100;
}
SalesPerson.prototype = new WorkerBee; // 他们也说“勤劳的小蜜蜂”??

// 孙子类
function Engineer() {
	this.dept = 'engineering';
	this.machine = '';
}
Engineer.prototype = new WorkerBee;

这段代码创建了一个继承树:

  • Employee
    • Manager
    • WorkBee
      • Saleperson
      • Engineer

其中

  • Employee 的属性有 namedept(默认值为 'general'
  • ManagerEmployee 的子类,拥有一个额外的 reports 属性
  • WorkBeeEmployee 的另一个子类,额外增加了 projects 属性
    • SalesPersonEngineer 都是 WorkBee 的子类,分别给继承自 Empolyeedept 属性赋了不同的默认值,各自也都定义了额外的属性。

下面写一些代码来定义一些对象:

var lei_bu_si = new Empolyee;
// lei_bu_si.name 的值是 ''
// lei_bu_si.dept 的值是 'general'

var pr = new Manager;
// pr.name 的值是 ''
// pr.dept 的值是 'general'
// pr.reports 的值是 []

var hard_working_guy = new WorkerBee;
// hard_working_guy.name 的值是 ''
// hard_working_guy.dept 的值是 'general'
// hard_working_guy.projects 的值是 []

var markert_guy = new SalesPerson;
// markert_guy.name 的值是 ''
// markert_guy.dept 的值是 'sales'
// markert_guy.projects 的值是 []
// markert_guy.quota 的值是 100

var full_stack_guy = new Engineer;
// full_stack_guy.name 的值是 ''
// full_stack_guy.dept 的值是 'sales'
// full_stack_guy.projects 的值是 []
// full_stack_guy.machine 的值是 '' //其实可以初始化成 '被老板强迫装了 Windows XP 的 MacBook Pro'

对象的属性操作

属性继承

var hard_working_guy = new WorkerBee; 为例,当 JavaScript 解析到 new 关键字时,创建一个新的通用对象,将该对象作为 this 关键字的值传递给相应的构造函数,并隐式地将内部的 __proto__ 属性的值设成了 WorkerBee.prototype。这些属性赋值完毕后,JavaScript 返回刚创建的 object,再将其赋给 hard_working_guy

这一行代码并没有给 hard_working_guy 的属性赋值。使用 __ptoto__ 属性沿着原型链(prototype chain)向上查找,直到上溯到给这个属性赋了默认值的原型为止。

具体到上面这个例子中,hard_working_guy 拥有三个属性:

  • name: ''
  • dept: 'general'
  • projects: ''

当然,在对象初始化之后,可以修改属性值:

hard_working_guy.name = '非不可抗力例行加班的码农';
hard_working_guy.dept = 'IT';
hard_working_guy.projects = '一些外包';

运行时添加属性

JavaScript 中,还可以在运行时为对象添加属性:

hard_working_guy.bonus = '¥ 30.0'; //当月奖金

这行代码给 hard_working_guy 添加了 bonus 属性,但其他利用 WorkerBee 创建的对象没有这个属性。

如果要为某个原型的所有对象添加属性,要用到原型的 prototype 属性:

Employee.prototype.specialty = 'none';

上面这行代码为每个 Employee 对象添加了 specialty 属性。

带参数的构造函数

上面例子中的构造函数不带任何参数,在定义构造函数时可以给定参数表。将上面的代码改成:

// 父类
function Employee(name, dept) {
	this.name = name || '';
	this.dept = dept || 'general';
}

// ...

// 另一个儿子类
function WorkerBee(projs) {
	this.projects = projs || [];
}
WorkerBee.prototype = new Employee;

// ...

// 孙子类
function Engineer(match) {
	this.dept = 'engineering';
	this.machine = match || '';
}

这里为几个构造函数添加了参数。另外,在子类的构造函数中,可以给父类的构造函数传递参数(沿着 prototype chain 向上传递):

// ...

// 另一个儿子类
function WorkerBee(name, dept, projs) {
	this.base = Employee;
	this.base(name, dept);
	this.projects = projs || [];
}
WorkerBee.prototype = new Employee;

// ... 

// 孙子类
function Engineer(name, projs, match) {
	this.base = WorkerBee;
	this.base(name, 'engineering', projs);
	this.machine = match || '';
}

完成这些定义之后,可以直接通过构造函数完成属性赋值:

var full_stack_guy = new Engineer('江宁区代码狗', ['OpenLayers', 'MapBox.js', 'jQuery'], '刷了 原生 Android 的 iPhone 7');

还有一种更为简洁的写法来代码上面添加 base 属性的代码,即调用 call() / apply() 函数:


// ...

// 孙子类
function Engineer(name, projs, match) {
	WorkerBee.call(this, name, 'engineering', projs);
	this.machine = match || '';
}

3. 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\")

上面这行代码另外定义了一个 Hobbyist 构造函数,并在 Engineer 的定义中增加了对 Hobbist 构造函数的调用。这样,dennis 就同时具有了 HobbistWorkerBee 的属性。不加注意的话可能会误以为 JavaScript 支持多个“基类”。事实上,JavaScript 是不支持的。因为 Engineerprototype 属性仍然还是 new WorkerBee

为了验证这个说法,可以在上述代码的最后加上 Hobbyist.prototype.equipment = ['mask', 'fins', 'regulator', 'bcd'];,运行代码可以发现,dennis 并不会继承 Hobbyistequipment 属性。

4.小结

基于原型的对象系统意味着 JavaScript 中并没有专门的“类”的概念,而是将一部分对象作为原型(函数也是对象),来派生其他的对象。属性继承与初始化则需要通过原型链的方式来进行。同时,可以运行时添加属性,也给代码带来了更大的灵活性。

另外插一句,JavaScript 基于原型的对象模型给代码的自动补全(Auto complete)造成了一定的麻烦。SublimeText 似乎只提示当前打开的文档中已有的字符串,使用不太熟悉的 API 时很不方便。而 WebStorm 和 Brackets 在 JavaScript 的自动补全能覆盖所有的属性和方法。

参考