前言
class的出现使开发者们能够以更简洁的方式实现原型继承,相比较于ES6时代以前的prototype,其优雅的实现方式也使得代码更加方便阅读。
ES5时代的prototype
在ES5与其早期版本中并不存在类的概念,但是为了更好的简化代码、增强代码的复用性与可维护性,JavaScript开发者们选择通过一种变通的思路来实现类的特性——通过操作构造函数(function)与其原型(prototype),示例如下:
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function() {
console.log(this.name);
}
var xiaoming = new PersonType('xiaoming');
var xiaohong = new PersonType('xiaohong');
xiaoming.sayName(); // xiaoming
xiaohong.sayName(); // xiaohong
// instanceof用于测试比较的两个对象是否存在于同一条原型链上
console.log(xiaoming instanceof PersonType); // true
console.log(xiaohong instanceof PersonType); // true
如上述代码所示,构造函数PersonType通过new关键字调用后会生成一个PersonType的实例,每个实例都可以顺着原型链访问到sayName这一方法,类似这样的写法在大多数js的早期库中十分常见,如jQuery与zepto,毕竟这样的实现方案好处显而易见,当我需要在新增一个功能时,比方说新增一个sayAge的功能,我只需要在PersonType.prototype上再挂上一个sayAge的方法便可,这大大降低了代码后期的维护难度,只是这样的写法看着总是不那么的美观,如果能将其只用一个对象就实现出来那就更好了。
ES6的class
class的出现使我们能够更加方便的生成示例,尽管其内部的所有功能都可以使用prototype来实现(这也是其被称为语法糖的原因),但是因其方便的特性在第一时间便俘获开发者的心,毕竟避免无意义无价值的重复劳动是程序员的天性。
首先让我们使用class的方式重新实现一下刚才的代码
class personClass {
// 等价于personType构造函数
constructor(name) {
this.name = name;
}
// 等价于personType.prototype.sayName
sayName() {
console.log(this.name);
}
}
let xiaoming = new PersonType('xiaoming');
let xiaohong = new PersonType('xiaohong');
xiaoming.sayName(); // xiaoming
xiaohong.sayName(); // xiaohong
console.log(xiaoming instanceof PersonType); // true
console.log(xiaohong instanceof PersonType); // true
我们通过class声明了一个personClass的类,并通过new关键字调用该类生成了多个实例,而且每个示例的表现都与上一个例子中生成的实例表现一致,明明两种方式所实现的功能完全一模一样,那class相对于prototype的优势又存在于哪里?我的理解有以下几点:
- 将所有状态都统一在了一个类中,方便后期管理;
- class必须先声明后使用(和let、const一样),不存在函数声明提升的问题,且class内的代码会强制以严格模式运行,这些特性将会强迫你规范自己的代码。
使用class时需要注意:
- class声明的类的prototype是只读属性哒,但即使如此你还是可以在prototype上随意的搞些骚操作(千万千万不要在prototype上搞骚操作!就算它允许你这么做也不要去做!),就像下面的示例一样:
class PersonClass {
// 等价于personType构造函数
constructor(name) {
this.name = name;
}
// 等价于personType.prototype.sayName
sayName() {
console.log(this.name);
}
}
console.log(PersonClass.prototype); // PersonClass {}
// 虽然该属性只读,但是直接给它赋值并不会报错,但也不会被修改
PersonClass.prototype = null;
console.log(PersonClass.prototype); // PersonClass {}
// 这里的表现很诡异
// 诡异的地方在于我们居然能对一个类做扩展,对于笔者来说一个类被声明后应该被禁止一切对该类的修改
PersonClass.prototype.sayAge = function (){
console.log('3岁');
};
let xiaoli = new PersonClass('xiaoli');
xiaoli.sayAge(); // 3岁
- 利用class声明的类中的所有方法不可枚举:
Object.keys(PersonClass.prototype); // []
- class内的constructor是不可缺少,如果你没有在你的class内定义它,不管你愿不愿意一个空的constructor会被加入:
class PersonClass {}
// 上面的和下面的是同一回事
class PersonClass {
constructor (){}
}
- class也支持在原型上增加访问器属性,像下面这样:
class Glasses {
constructor (e){
// 这里针对vistor的赋值操作会触发setter
this.vistor = e;
this.ha = 'ha';
}
// 对vistor设置getter
get vistor() {
console.log('ha');
}
// 对vistor设置setter
set vistor(newValue) {
console.log('+1');
}
}
let gua = new Glasses('journalist of HongKong'); // +1
console.log(gua.ha); // ha
// 这里获取vistor的操作会触发getter
console.log(gua.vistor); //ha undefined
prototype实现继承与class实现继承
继承的实现方式-prototype
在ES6还未推广之前,实现继承便一直都是一个比较头疼的问题,各种形式的继承方式眼花缭乱,只不过实现继承的本质是重写原型对象,记住这点便不难理解下面的代码:
function Rectangle (length, width){
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function (){
return this.length * this.width;
}
function Square (length){
Rectangle.call(this, length, length);
}
// Object.create() 方法会使用指定的原型对象及其属性去创建一个新的对象,就当做是创建了一个Rectangle的实例就好了
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value: Square,
enumerable: true,
writable: true,
configurable: true
}
});
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
就如上述代码所描述的,通过将Square的prototype指向了Rectangle的一个实例上,从而使所有Square构造函数创建的实例都能通过原型链访问到Rectangle原型上的方法。(这里不对继承相关知识做过多描述,如果对继承有相关疑问请仔细阅读JavaScript高级程序设计第六章相关内容。)
继承的实现方式-class
class的出现使继承不再像prototype时代的实现方式那样繁琐(但是需要注意的是class实现继承任然是基于prototype!任然是基于prototype!任然是基于prototype!),通过使用extend关键字指定类继承的函数,其原型会自动调整,通过调用super()方法即可访问基类的构造函数:
// 基类
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
// 派生类
class Square extends Rectangle {
constructor(length) {
// 通过super访问Rectangle的构造函数(其实就是constructor里面的那些)
// 等价于上面例子中的 Rectangle.call(this, length, length);
super(length, length);
}
}
let square = new Square(3);
console.log(square.getArea());
console.log(square instanceof Square);
console.log(square instanceof Rectangle);
在该例中Square类通过extends继承了Rectangle类,在Square的构造函数中(constructor部分)通过super()调用了基类中的构造函数。
class实现继承需要注意以下几点
- super()只能在派生类的构造函数中使用,否则抛出错误。
class ErrorSuper {
constructor() {
super();
}
}
// SyntaxError: 'super' keyword unexpected here
- 在派生类的构造函数中super()负责初始化this,因此在调用this必须在super()之后。
- 派生类中的方法总是会覆盖基类的同名方法(原型链的特性)。
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 派生类中的同名函数会覆盖基类中的同名函数,如果你想调用基类上的该方法可以通过super这一实例访问到。
getArea() {
return super.getArea();
}
}
总结
- 不要再把class当做什么特殊的类型啦!它其实就是一个语法糖!
- 利用class实现的原型继承与prototype实现的原型继承并无差别!class本身其实就可以理解为一个function
typeof Square; // function
typeof Rectangle; // function
// 试一下画出Square与Rectangle的原型链!提示:将Square与Rectangle视为构造函数!再思考一下Square.prototype所指向的Rectangle的实例的创建操作被藏到什么位置去了?
// 这是一道思考题!
Square.prototype.__proto__ === Rectangle.prototype // true
- class可真是太好用了!就算你不愿意学习如何使用class,看看那边的react!它正在对你微笑呢!