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
的属性有name
和dept
(默认值为'general'
)Manager
是Employee
的子类,拥有一个额外的reports
属性WorkBee
是Employee
的另一个子类,额外增加了projects
属性SalesPerson
、Engineer
都是WorkBee
的子类,分别给继承自Empolyee
的dept
属性赋了不同的默认值,各自也都定义了额外的属性。
下面写一些代码来定义一些对象:
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
就同时具有了 Hobbist
和 WorkerBee
的属性。不加注意的话可能会误以为 JavaScript 支持多个“基类”。事实上,JavaScript 是不支持的。因为 Engineer
的 prototype
属性仍然还是 new WorkerBee
。
为了验证这个说法,可以在上述代码的最后加上 Hobbyist.prototype.equipment = ['mask', 'fins', 'regulator', 'bcd'];
,运行代码可以发现,dennis
并不会继承 Hobbyist
的 equipment
属性。
4.小结
基于原型的对象系统意味着 JavaScript 中并没有专门的“类”的概念,而是将一部分对象作为原型(函数也是对象),来派生其他的对象。属性继承与初始化则需要通过原型链的方式来进行。同时,可以运行时添加属性,也给代码带来了更大的灵活性。
另外插一句,JavaScript 基于原型的对象模型给代码的自动补全(Auto complete)造成了一定的麻烦。SublimeText 似乎只提示当前打开的文档中已有的字符串,使用不太熟悉的 API 时很不方便。而 WebStorm 和 Brackets 在 JavaScript 的自动补全能覆盖所有的属性和方法。
参考
- Details of the object model - MDN
- ECMA 6.0 引入了
class
、extends
等关键字,未来版本的 JavaScript 对象模型可能会有变化。具体内容可以读一下《ECMAScript 6入门》。