对象模型的细节

JavaScript是基于原型的语言,它只有对象。原型对象可以作为一个模板,新对象可以从中获得原始的属性。任何对象都可以在创建对象时或在运行时指定其自身的属性。此外,任何对象都可以关联另一个对象的原型,从而允许第二个对象共享第一个对象的属性。

定义类

在基于类的语言中,需要专门的类的定义来定义类。在定义类时,允许定义被称为构造器的特殊方法来创建该类的实例。在构造器方法中,可以指定实例的属性的初始值并做一些其他的操作。可以通过使用new操作符来创建对象的实例。

JavaScript没有专门的类定义。可以通过定义构造函数的方式来创建一系列有着特定初始值和方法的对象。任何JavaScript函数都可以被用作构造函数,可以使用new操作符来创建一个新对象。

子类和继承

在基于类的语言中,可以通过类定义来创建类的层次结构,在类的定义中,可以指定新类是已经存在类的子类,子类继承父类的所有属性,并且可以添加新属性和修改继承的属性。JavaScript通过将构造函数与原型对象相关联的方式来实现继承。

添加和移除属性

在基于类的语言中,通常在编译时创建一个类,然后在编译时或运行时实例化该类的实例。定义类后,无法更改类的数量或属性类型。但在JavaScript中可以在运行时添加或修改任何对象的属性,如果为一个对象添加了一个属性,而这个对象有作为其他对象的原型,则以该对象为原型的所有其他对象也将获得该属性。

基于类的对象和基于原型的对象差异

基于类的对象 基于原型的对象
类和实例是不同的事物 所有对象均为实例
通过类定义来定义类,通过构造函数来实例化类 通过构造函数来定义和创建一组对象
通过new操作符来创建单个对象 相同
通过类定义来定义现存类的子类,从而构建对象的层级结构 指定一个对象作为原型并且与构造函数一起构建对象的层级结构
遵循类链继承属性 遵循原型链继承属性
类定义指定类的所有实例的所有属性,无法在运行时动态添加属性 构造函数或原型指定实例的初始属性集,允许动态地向单个的对象或者整个对象集中添加或移除属性

继承层级结构示例

employee

  • Employee具有name属性(默认为空字符串)和dept属性(默认为general)
  • ManagerEmployee的子类,它添加了reports属性(默认为空数组,以Employee对象数组作为他的值)
  • WokerBeeEmployee的子类,它它添加了projects属性(默认为空数组,以字符串数组作为它的值)
  • SalesPersonWorkBee的子类,它添加了quota属性(默认为100),重载了dept属性值为sales,表明所有的销售人员都同属于同一部门
  • Engineer基于WorkerBee,它添加了machine属性(默认为空字符串),重载了dept属性为engineering
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 定义Employee对象
function Employee(name, dept) {
this.name = name === undefined ? '' : name;
this.dept = dept === undefined ? 'general' : dept;
}
// 定义Manager对象
function Manager() {
Employee.call(this);
this.reports = [];
}
Manager.prototype = Object.create(Employee.prototype);
// 定义WorkerBee对象
function WorkerBree() {
Employee.call(this);
this.projects = [];
}
WorkerBree.prototype = Object.create(Employee.prototype);
// 定义SalesPerson对象
function SalesPerson() {
WorkerBee.call(this);
this.quota = 100;
this.dept = 'sales';
}
SalesPerson.prototype = Object.create(WorkerBree.prototype);
// 定义Engineer对象
function Engineer() {
WorkerBee.call(this);
this.machine = '';
this.dept = 'engineering';
}
Engineer.prototype = Object.create(WorkerBree.prototype);

对象的属性

继承属性
1
2
3
4
5
6
// 对象实例化
var mark = new WorkerBee;

mark.name = "";
mkrk.dept = "general";
mark.projects = [];

当JavaScript执行new操作符时,它会先创建一个普通对象,并将这个普通对象中的[[prototype]]指向WorkBee.prototype,然后再把这个普通对象设置为执行WorkBee构造函数时的this值。该普通对象的[[prototype]]决定其用于检索属性的原型链。当构造函数执行完成后,所有的属性都被设置完毕,JavaScript返回之前创建的对象,通过赋值语句将他的引用赋值给变量mark

这个过程不会显示的将mark所继承的原型链中的属性作为本地属性存放在mark对象中。当访问属性时,JavaScript将首先检查对象自身中是否存在该属性,有则返回该属性的值。如果不存在,则会检查原型链(使用内置的prototype属性)。如果原型链中的某个对象包含该属性,则返回这个属性的值。如果遍历整个原型链都没有找到该属性,则返回undefined

Employee构造函数为mark对象分配了name和dept的属性值。WorkerBee的构造函数分配了project属性值。这就是JavaScript中的属性和属性值继承。

在JavaScript中,可以在运行时为任何对象添加属性。

1
2
3
4
5
// 添加特定于某个对象的属性
mark.bonus = 3000;

// 向构造函数的原型对象添加新的属性,该属性将添加到从这个原型中继承属性的所有对象中
Employee.prototype.specialty = "none";
本地属性和继承属性

在访问一个对象的属性时,JavaScript将进行以下操作:

  1. 检查对象自身是否存在。如果存在,返回值。
  2. 如果本地值不存在,检查原型链
  3. 如果原型链中的某个对象具有指定属性,则返回值。
  4. 如果这样的属性不存在,则没有该属性,返回undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Employee() {
this.name = '';
this.dept = 'general';
}
function WorkerBee() {
this.projects = [];
}
WorkerBee.prototype = new Employee;

// 创建WorkerBee的实例amy
let amy = new WorkerBee;
amy.name == ''; // 继承属性
amy.dept == 'general'; // 继承属性
amy.projects = []; // 本地属性

判断实例的关系

JavaScript中的属性查找在对象自身的属性内查找,如果未找到属性名称,则在特殊对象属性__proto__中查找。这个过程是递归的,这被称为在原型链中查找。

特殊的__proto__属性是在构建对象中设置的。设置为构造器的prototype属性的值。所以表达式new Foo()将创建一个对象。其__proto__ == Foo.prototype。因而,修改Foo.prototype的属性,将改变所有通过new Foo()创建的对象的属性的查找。

每个对象都有一个__proto__对象属性(Object除外)。每个函数都有一个prototype对象属性。因此,通过原型继承对象就可以与其他对象之间形成关系。通过比较对象的__proto__属性和prototype属性可以检测对象的继承关系。JavaScript提供了便捷的方法:instanceof操作符可以用来将一个对象和一个函数做检测,如果对象继承自函数的原型则返回true:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Foo() {}
var f = new Foo();
console.log(f instanceof Foo); // true


// 使用上面的Engineer的例子
var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");
console.log(chris.__proto__ == Engineer.prototype);
console.log(chris.__proto__.__proto__ == WorkerBee.prototype);
console.log(chris.__proto__.__proto__.__proto__ == Employee.prototype);
console.log(chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype);
console.log(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__;
}
reurn false;
}
instanceOf (chris, Engineer); // true
instanceOf (chris, WorkerBee); // true
instanceOf (chris, Employee); // true
instanceOf (chris, Object); // true

构造器中的全局属性

在创建构造器时,在构造器中修改全局属性要慎重:

1
2
3
4
5
6
7
8
9
10
11
12
var idCounter = 1;
function Employee(name, dept) {
this.name = name || '';
this.dept = dept || 'general';
this.id = idCounter++;
}
var victoria = new Employee('victoria', 'public'); // victoria.id = 1
var harry = new Employee('harry', 'sales'); // harry.id = 2

// 在每一次创建Employee对象时,idCounter都将被递增一次
function Manager() {}
Manager.prototypr = new Employee(); // 此时idCounter也将被递增一次

没有多重继承

某些面向对象语言支持多重继承(Python),对象可以从无关的父对象中继承属性和属性值。JavaScript不支持多重继承。JavaScript的属性继承是在运行时通过检索对象的原型链来实现的,因为对象只有一个原型与之关联,所以JavaScript无法动态的从多个原型链中继承。

在JavaScript中,可以在构造函数中调用多个其他的构造函数,造成了多重继承的假象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Hobbist(hobby) {
this.hobby = hobby || 'code';
}
function Engineer(name, projs, mach, hobby) {
this.base1 = WorkerBee;
this.base(name, 'engineering', projs);
this.base2 = Hobbist;
this.base2(hobby);
this.mach = mach || '';
}
Engineer.prototype = new WorkerBee;
var dennis = new Engineer('Dennis', ['test'], 'hugo');

dennis.name == 'Dennis';
dennis.dept == 'engineering';
dennis.peojects == ['test'];
dennis.machine == 'hugo';
dennis.hobby == 'codde';

Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
dennis.equipment == null; // 不会继承

本文参考资料

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • © 2019-2021 musi

请我喝杯咖啡吧~

支付宝
微信