详解ES6关键字Class

1,128 阅读6分钟

"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 (xhs-rookies.com/) 进行学习,及时获取最新文章。

本文通过以下几个方面来谈谈ES6 中新增的关键字Class

  • 背景
  • 如何使用 Class
  • 横向、纵向对比
  • JavaScript 是如何实现 Class 的
  • Class 关键字的限制
  • 总结

1. 背景

1.1 Class 的来历

Class由 2015 年的ES6中正式提出,但是这个关键字最早于一篇 JavaScript 的草案JavaScript 2.0 Classes (mozilla.org)中提出(1999.02),当时因为该关键字过于激进,导致 ES4 并未将 JavaScript2.0 中的 class 关键字合并进去,直到ES6草案的提出和通过。

1.2 ES6 加入 Class 的原因

我们在 TC39 的GitHub找到了 ES6 的提案,在Stage0 阶段关于 Class 的提案中找到了如下解释:

image-20201213151123623

翻译:

ECMAScript 已经拥有了非常完备的功能来定义各种事物的抽象。构造函数、原型、实例这三个的特性足以让实现那些别的语言中类能做到的事。这个稻草人(暂时理解为 功能)的目的不是改变那些语义。相反的,为这些语义提供简洁的声明性表示,能够清楚的表达出程序员的意图而不是底层的命令性机制。通俗来说,Class关键字给我们提供了一种方式,离开那些繁的prototypecall,写出逻辑清晰的代码。

2. 如何使用 Class

2.1 创建类

使用ES6 中的 Class 来实现类

//ES6
class Employee {
  constructor(name, dept) {
    this.name = name
    this.dept = dept
  }
  static fun() {
    console.log('static')
  }
  getName() {
    console.log(this.name)
  }
}

这里我们创建了一个类,含有 name 和 dept 两个参数,并且含有一个静态方法 fun 和一个方法 getName。

2.2 通过继承实现子类

使用extends 实现继承进行比较:

//ES6
class Manager extends Employee {
  constructor(name, dept, reports) {
    super(name, dept)
    this.reports = reports
  }
}

这里我们通过继承父类Employee创建了子类Manager,比父类多一个 reports 的属性,其余全部继承自父类。

3. 横向、纵向比较

传统的OO语言(java,c++等等),都包含例如类、构造函数、继承、静态方法等等。他们通过这些来实现面向对象的功能,那么JavaScript是如何做的呢?

3.1 JavaScript 和 Java 进行横向比较(以Java为例)

1. 类的概念(Class):

  • JavaScript 同 java 类似,都采用了 class 来声明一个类。

2. 构造函数(constructor):

  • JavaScript 使用 constructor 方法当作构造函数,而 java 采用类名作为构造函数。

3. 子类(extends):

  • JavaScript 同 java 一致,都使用 extends 来继承。

4. 静态方法(static):

  • ES6 中 JavaScript 同 java 一致,使用 extends 创建子类自然就将父类的静态方法继承过来。ES5 则需要手动声明指向。

5. 类的属性(int、double、String等):

  • JavaScript 得益于类型自动推断,不需要显示的声明类的属性。

3.2 ES6 和 ES5 的纵向比较

我们观察下面 ES5 和 ES6 两种方式创建父类和父类,使用class创建子类在逻辑上比 ES5 的方法更加清晰。

首先我们比较一下创建父类的方式:

image-2020.png

再比较子类Manager,使用extends继承创建的子类Manager自然继承了其中的属性和方法(包括静态方法),但是 ES5 中实现继承,不仅需要通过 call 解决调用的问题,同时静态方法无法直接继承,需要在子类中二次声明才能继承。

!image.png

但是Class真的在所有地方都优于传统代码吗?其实也不见得,在很多场合下如果只是单纯想使用一次该对象,为何一定要抽象出类来呢?使用字面量对象是不是更加好一些。

//字面量对象
let person = {
  name: 'Jason',
  age: 18,
  adress: 'hangzhou',
  phone: '10086',
  getPhone: function () {
    return this.phone
  },
}

4. JavaScrpit 是如何实现 Class 的

我们都知道 ES5 中实现类的方法是通过原型链来实现的(详解 JavaScript 原型链),我们使用 ES5 的写法,实现一个 Employee 的实例:

//ES5
let employ = new Employee(1, 2)
employ.__proto__.constructor === Employee //true
employ.__proto__ === Employee.prototype //true
Employee.prototype.constructor === Employee //true

这是没有问题的,__proto__指针指向构造函数的原型,我们用 ES6 的 class 再来判断一遍:

//ES6使用class
let employ = new Employee(1, 2)
employ.__proto__.constructor === Employee //true
employ.__proto__ === Employee.prototype //true
Employee.prototype.constructor === Employee //true

两者的结果是相同的!只不过使用函数创建的对象,__proto__指针指向构造函数,使用 class 关键字创建的对象,__proto__指针指向类。

我们再来看这串代码:

console.log(typeof Employee) //function

我们将 class 放入,竟然返回的是 function,也就是 class 关键字本质上还是一个函数,使用 class 创建出来的类还是基于函数和原型链。

image

5. Class 关键字的限制

既然本质上Class仍然是一个函数,他做的事情又是将 ES5 的写法更加精简,那么自然就会带来一些限制。

5.1 灵活性

假设现在有一个需求,在一个Manager对象中加入一个方法,在任何地方只要使用 ES5 的方法只需要在prototype上加入方法即可

//ES5
let manager1 = new Manager(1, 2, 3)
manager1.prototype.FunctionName = function () {
  /*写自己的逻辑*/
}

那么使用class该怎么办呢?class则一定要在类中加入该方法。

//ES6
class Manager extends Employee {
  constructor(name, dept, reports) {
    super(name, dept)
    this.reports = reports
  }
  FunctionName() {
    /*写自己的逻辑*/
  }
}

5.2 类声明不可提升

函数声明具有可提升性,虽然类本质上是函数,但是类的声明却不可提升,也就是说你一定要先声明类,才能创建对象。

let a = new ES6_Practice() //报错
let b = new ES5_Practice() //成功
class ES6_Practice {}
function ES5_Practice() {}

5.3 无法重写类

当我们在特定时刻想要重写某个函数方法,只需要重新定义方法即可,但是类却无法重写,如果出现两个相同的类名,则会报错语法错误(SyntaxError)。

class ES6_Practice{
}
class ES6_Practice{   //报错SyntaxError
	console.log("new class");
}
function ES5_Practice(){
}
function ES5_Practice(){	//没问题,重写成功
  console.log("new function");
}

6. 总结

ES6 中的关键字Class本质上还是通过原型链和函数做的,相比于使用 ES5 写法,Class给我们提供了一种写法更加方便、逻辑更加清晰的方法去实现类。但是更加灵活、方便同时也会带来一些限制,我们在写代码的时候也要注意一下这些限制。

也就如TC39在 ES6提案上所说的:

it’s to provide a terse and declarative surface for those semantics so that programmer intent is expressed instead of the underlying imperative machinery 希望程序员通过Class关键字清楚的表达出他们想要的东西,让他们的想法得以表达,而不是还要思考底层的机制。

参考内容

Stack Overflow 关于 Class 关键字的讨论

GitHub 上 TC39 的 ES6 提案库

2016 年 ES6 中关于 Class 的提案

ES6 新增关键字-MDN

JavaScript2.0 草案

类私有域 MDN

Java-wiki

JavaScript 中 7 中设计模式

JavaScript 深入之从原型到原型链

OO 语言-wiki