ES6-class

407 阅读6分钟

前言

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的优势又存在于哪里?我的理解有以下几点:

  1. 将所有状态都统一在了一个类中,方便后期管理;
  2. 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!它正在对你微笑呢!