类、原型和构造函数

ECMAScript没有类的概念,但我们可以模拟出“类”

对象通过原型继承

在JavaScript中,类的所有实例对象都是从同一个原型对象上继承属性,可以通过如下inherit()方法实现

1
2
3
4
5
6
7
8
9
10
11
12
// inherit()返回一个继承自原型对象p的属性的新对象
function inherit(p) {
if (p == null) throw TypeError;
if (Object.create)
return Object.create(p);
var t = typeof p;
if (t !== 'object' && t !== 'function')
throw TypeError;
function f() {};
f.prototype = p;
return new f();
}

举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
// obj1从Object.prototype继承对象的方法
var obj1 = { x: 1 };

// obj2继承obj1和Object.prototype
var obj2 = inherit(obj1);
obj2.y = 2;

console.log(obj2.x + obj2.y); // => 3

obj1.x++;
// 改变obj1.x,继承而来的obj2.x也会随之改变
console.log(obj2.x + obj2.y); // => 4

通常,类的实例还要进一步初始化,可以通过定义工厂函数创建和初始化类的实例对象

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
// 实现一个表示范围的类

// 通过工厂模式返回一个新的范围对象
function range(from, to) {
var r = inherit(range.methods);
r.from = from;
r.to = to;
return r;
}

// 这些方法可以被每个范围对象继承
range.methods = {
includes: function (x) {
return this.from <= x && x <= this.to;
},
foreach: function (f) {
for (var x = Math.ceil(this.from); x <= this.to; x++) {
f(x);
}
},
toString: function () {
return "(" + this.from + ", " + this.to + ")";
}
};

var r = range(1, 5);
r.includes(2) // => true
r.foreach(console.log) // => 1 2 3 4 5
r.toString() // => (1, 5)

构造函数

调用构造函数的一个重要特征——构造函数的prototype属性被用作新对象的原型

通过同一个构造函数创建的所有对象都继承自同一个对象,构造函数初始化这个新建的对象,并将这个对象用做其调用上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Range(from, to) {
this.from = from;
this.to = to;
}

Range.prototype = {
includes: function (x) {
return this.from <= x && x <= this.to;
},
foreach: function (f) {
for (var x = Math.ceil(this.from); x <= this.to; x++) {
f(x);
}
},
toString: function () {
return "(" + this.from + ", " + this.to + ")";
}
};

var r = new Range(1, 5);
r.includes(2) // => true
r.foreach(console.log) // => 1 2 3 4 5
r.toString() // => (1, 5)

构造函数和类的标识

原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是同一个类的实例

通常我们使用instanceof检测某个对象是否为类的实例,本质上也是检测构造函数的prototype属性是否出现在对象的原型链中的任何位置

1
2
r instanceof Range                  // => true
r instanceof Object // => true

constructor属性

所有JavaScript函数都有一个原型对象属性prototype,这个对象包含唯一一个不可枚举的属性constructor

1
2
3
var Func = function () {};
var obj = Func.prototype;
obj.constructor === Func; // => true

可以看出构造函数的原型中存在预先定义好的constructor属性,对象通常继承的constructor属性均指代其构造函数

1
2
3
var Func = function () {};
var obj = new Func();
obj.constructor === Func // => true

在上面定义的Range类中,新定义的prototype对象不含constructor属性

1
r.constructor === Range              // => false

可以通过手动添加来修正

1
2
3
4
Range.prototype = {
constructor: Range,
// code...
};

也可以使用预定义的prototype对象,依次添加方法

1
2
3
4
5
6
7
8
9
10
11
Range.prototype.includes = function (x) {
return this.from <= x && x <= this.to;
};
Range.prototype.foreach = function (f) {
for (var x = Math.ceil(this.from); x <= this.to; x++) {
f(x);
}
};
Range.prototype.toString = function () {
return "(" + this.from + ", " + this.to + ")";
};

JavaScript中Java式类的模拟

JavaScript中的类与三种不同的对象有关:

  • 构造函数对象

  • 原型对象

  • 实例对象

在JavaScript中定义一个类有以下三步:

  1. 定义一个构造函数,并设置初始化新对象的实例属性

  2. 给构造函数的prototype对象定义实例的方法

  3. 给构造函数定义类字段和类属性

下面的例子展现了实现一个复数类的完整过程:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 构造函数为每个实例定义两个实例字段 r 和 i,分别表示实部和虚部
function Complex(real, imaginary) {
if (isNaN(real) || isNaN(imaginary))
throw new TypeError;
this.r = real;
this.i = imaginary;
}

/**
* 类的实例的方法,包括加、取模、相等判断
*/
Complex.prototype.add = function (complex) {
return new Complex(this.r + complex.r, this.i + complex.i);
}
Complex.prototype.mag = function () {
return Math.sqrt(this.r * this.r + this.i * this.i);
}
Complex.prototype.isEqual = function (complex) {
return complex != null &&
complex.constructor === Complex &&
this.r === complex.r && this.i === complex.i;
}
Complex.prototype.toString = function () {
return '{' + this.r + ', ' + this.i + '}';
}

/**
* 类字段和类方法直接定义为构造函数的属性
* 类的方法通常不使用 this,只对其参数操作
*/
Complex.ZERO = new Complex(0, 0);
Complex.ONE = new Complex(1, 0);
Complex.I = new Complex(0, 1);

// 将实例对象返回的字符串解析为 Complex 对象
Complex.parse = function (str) {
try {
var m = Complex._format.exec(str);
return new Complex(parseFloat(m[1]), parseFloat(m[2]));
} catch (e) {
throw new TypeError("Can't parse '" + str + "' as a complex");
}
};
Complex._format = /^\{([^,]+),([^}]+)\}$/;

var c = new Complex(2, 3);
var d = new Complex(c.i, c.r);
console.log(c.toString(), d.toString()); // => {2, 3} {3, 2}
var e = Complex.parse('{1, 2}')
console.log(e.add(c).toString()) // => {3, 5}

JavaScript基于原型的继承机制是动态的,如果创建对象之后原型的属性发生改变,也会影响继承这个原型的所有实例对象

可以通过给原型对象添加新方法扩充JavaScript类

1
2
3
4
5
Complex.prototype.conj = function () {
return new Complex(this.r, -this.i);
}

console.log(c.conj().toString()) // => {2, -3}