Javascript面向对象(七)——类模式
在面向对象编程里,类是一个为创建对象的可扩展编程代码模板。提供了状态的初始值(成员变量),实现行为(成员函数或方法)。——维基百科
在Javascript中有特定语法结构以及关键字class,但学习之前,我们应该考虑术语“类”是面向对象编程理论。其定义是引用上面内容,且和语言无关。
即使不用class关键字,在Javascript也有几种众所周知的类编程模型。这里我们首先讨论它们。
类结构将在下一章描述,但在Javascript中它是个“语法糖”,我们马上要学习模型中一个的扩展。
函数类模式
根据类的定义,下面代码的构造函数可以视作类:
function User(name) {
this.sayHi = function() {
alert(name);
};
}
let user = new User("John");
user.sayHi(); // John
它遵循定义的所有部分:
- 是创建对象的编程代码模板(使用new调用)
- 提供了状态初始值(name,从参数提供)
- 提供了方法(sayHi)
这被称为函数类模型。局部变量和内嵌函数在User里面,没有赋值给this,对内可见,但是不能被外部代码访问。
所以我们能容易增加内部函数和变量,如calAge()
:
function User(name, birthday) {
// only visible from other methods inside User
function calcAge() {
new Date().getFullYear() - birthday.getFullYear();
}
this.sayHi = function() {
alert(name + ', age:' + calcAge());
};
}
let user = new User("John", new Date(2000,0,1));
user.sayHi(); // John
上面代码中,name、birthday和函数calcAge()在内部,是对象私有的,仅仅在对象内部可见。
另外方面,sayHi是外部的,公共方法。外部代码可以访问,如创建的对象user可以访问它。
这种方式能够让外部访问的助手方法隐藏实现细节,仅赋值this的方法成为外部可见的。
工厂类模式
我们能创建类根本不使用new关键字,如下代码:
function User(name, birthday) {
// only visible from other methods inside User
function calcAge() {
new Date().getFullYear() - birthday.getFullYear();
}
return {
sayHi() {
alert(name + ', age:' + calcAge());
}
};
}
let user = User("John", new Date(2000,0,1));
user.sayHi(); // John
我们能看到,函数User返回一个对象,带有公共的方法。这种方式唯一好处就是可以省略new关键字,直接写 let user = User(...)
,代替写let user = new User(...)
,除此以外,几乎和函数类模式完全一样。
基于原型类模式
基于原型类模式是最重要、最好的方式。实践中函数类模式、工厂类模式很少使用。很快我们会说明原因。
这里我们用原型方式重新写上面代码:
function User(name, birthday) {
this._name = name;
this._birthday = birthday;
}
User.prototype._calcAge = function() {
return new Date().getFullYear() - this._birthday.getFullYear();
};
User.prototype.sayHi = function() {
alert(this._name + ', age:' + this._calcAge());
};
let user = new User("John", new Date(2000,0,1));
user.sayHi(); // John
代码结构解释如下:
- 构造函数User仅初始化当前对象状态。
- 方法通过原型增加:User.prototype.
我们看到,方法在词法上不在function User
内部,他们不共享一个词法环境。如果我们声明变量在function User
内部,那么他们对方法不可见。
所以,有一个广泛的共识是:内部属性和方法通常使用“”开头,如_name
、_calcAge()
。技术,这仅是一个约定,外部代码仍然能访问他们,但几乎所有的开发者认同“”的意思,外部代码尽力不触碰下划线开头的属性和方法。
这里是原型模式优于函数类模式之处:
- 在函数模式中,每个对象有自己所有方法的拷贝,示例中的拷贝是:
this.sayHi = function(){...}
,以及其他构造函数中的方法。 - 在原型模式中,所有方法在
User.prototype
中,为所有对象共享,对象仅存储数据。
所以,原型模式是更有效的。
不仅如此,原型允许我们使用有效的方式实现继承。内置Javascript对象都使用原型。也有特定的语法机构提供漂亮的语法方式实现“类”,后续详解。
基于原型的类继承
我们有两个基于原型的类Rabbit:
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.jump = function() {
alert(this.name + ' jumps!');
};
let rabbit = new Rabbit("My rabbit");
和Animal类:
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
alert(this.name + ' eats.');
};
let animal = new Animal("My animal");
现在他们完全独立。
但我们想让Rabbit继承Animal,换句话说,rabbit应该基于animals,能够访问Animal的方法,并可以扩展实现自己的方法。
Javascript语言中原型意味什么?
现在rabbit对象的方法在Rabbit.prototype中,我们想如果访问rabbit对象的某个方法不在Rabbit.prototype中,使用Animal.prototype作为后备。
所以原型链应该是rabbit –> Rabbit.prototype –>Animal.prototype. 如图所示:
代码实现:
// Same Animal as before
function Animal(name) {
this.name = name;
}
// All animals can eat, right?
Animal.prototype.eat = function() {
alert(this.name + ' eats.');
};
// Same Rabbit as before
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.jump = function() {
alert(this.name + ' jumps!');
};
// setup the inheritance chain
Rabbit.prototype.__proto__ = Animal.prototype; // (*)
let rabbit = new Rabbit("White Rabbit");
rabbit.eat(); // rabbits can eat too
rabbit.jump();
(*)行设置了原型链,所以rabbit首先在Rabbit.prototype中搜索方法,然后是Animal.prototype,搜到完成,如果Animal.prototype中仍没有搜到,会继续在Object.prototype中搜索,因为Animal.prototype是一般的普通对象,所以继承自Object。
完整图示如下:
总结
属于“类”来自面向对象编程,在Javascript中,通常意味着函数类模式或原型模式。原型模式更强大且高效,所以推荐使用。
根据原型模式规范:
- 方法存储在Class.prototype中。
- 原型继承自其他原型。
下一章,我们讨论class关键字和其结构,是提供了更简洁的方式实现原型模式,并且还有其他优势。
本文参考链接:https://blog.csdn.net/neweastsun/article/details/70551592