Class

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。

这是摘自阮一峰老师的博客,这句话放在 ES5 可以说不为过,然而到了 ES6 这么说就已经不严谨了。因为 ES6 中已经有了专属的 class 语法了。
有的同学喜欢函数式的编程方式,有的同学喜欢面向对象的编程思维,我们今天不论长短,重点讲述怎么使用 ES6 的 class 语法完成面向对象的开发。

Intro

对于面向对象编程而言,更关注类的声明、属性、方法、静态方法、继承、多态、私有属性。

Basic Syntax

首先我们要先来说明在 JavaScript 世界里如何声明一个 “类”。在 ES6 之前大家都是这么做的:

  1. let Animal = function (type) {
  2. this.type = type
  3. this.walk = function () {
  4. console.log(`I am walking`)
  5. }
  6. }
  7. let dog = new Animal('dog')
  8. let monkey = new Animal('monkey')

在上述代码中,我们定义了一个叫 Animal 的类,类中声明了一个属性 type、一个方法 walk;然后通过 new Animal 这个类生成实例,完成了类的定义和实例化。当然你也可以这样写:

  1. let Animal = function (type) {
  2. this.type = type
  3. }
  4. Animal.prototype.walk = function () {
  5. console.log(`I am walking`)
  6. }
  7. let dog = new Animal('dog')
  8. let monkey = new Animal('monkey')

在 ES6 中把类的声明专业化了,不在用 function 的方式了,请看:

  1. class Animal {
  2. constructor (type) {
  3. this.type = type
  4. }
  5. walk () {
  6. console.log(`I am walking`)
  7. }
  8. }
  9. let dog = new Animal('dog')
  10. let monkey = new Animal('monkey')

很明显,从定义上就很专业了,有构造函数、方法,但是 ES6 增加了新的数据类型 class 吗?

  1. console.log(typeof Animal); //function

可以发现 class 的类型还是 function,和 ES5 貌似并没有什么区别,那么 class 中定义的方法在哪呢?我们知道只要是函数,就一定会有 prototype 对象。那么类的方法和 prototype 对象有什么关系呢?

  1. console.log(Animal.prototype);
  2. // {constructor: ƒ, walk: ƒ}
  3. // constructor: class Animal
  4. // walk: ƒ walk()
  5. // __proto__:
  6. // constructor: ƒ Object()
  7. // hasOwnProperty: ƒ hasOwnProperty()
  8. // isPrototypeOf: ƒ isPrototypeOf()
  9. // propertyIsEnumerable: ƒ propertyIsEnumerable()
  10. // toLocaleString: ƒ toLocaleString()
  11. // toString: ƒ toString()
  12. // valueOf: ƒ valueOf()
  13. // __defineGetter__: ƒ __defineGetter__()
  14. // __defineSetter__: ƒ __defineSetter__()
  15. // __lookupGetter__: ƒ __lookupGetter__()
  16. // __lookupSetter__: ƒ __lookupSetter__()
  17. // get __proto__: ƒ __proto__()
  18. // set __proto__: ƒ __proto__()

可以看出在 Animal.prototype 对象上有两个方法,一个是构造函数(constructor)、一个是自定义的方法(walk)。这是不是和 ES5 的第二种写法一模一样?我们再来看下属性,在 ES5 中有个 API 用来判断对象的自有属性(hasOwnProperty)。

  1. console.log(dog.hasOwnProperty('type')); //true

这个表现也和 ES5 中直接使用 function 定义类的方式相同,所以得出一个结论:class 的方式是 function 方式的语法糖。

Setters & Getters

对于类中的属性,可以直接在 constructor 中通过 this 直接定义,还可以直接在类的顶层来定义:

  1. class Animal {
  2. constructor (type, age) {
  3. this.type = type
  4. this._age = age
  5. }
  6. get age () {
  7. return this._age
  8. }
  9. set age (val) {
  10. this._age = val
  11. }
  12. }

这个代码演示了,通过 get/set 来给类定一个属性,不过貌似没有说服力。因为 age 和 _age 都是类的属性,而且是相同的含义这样做感觉没有实际用途。但是如果一个属性是个只读的呢?

  1. class Animal {
  2. constructor (type) {
  3. this.type = type
  4. }
  5. get addr () {
  6. return '北京动物园'
  7. }
  8. }

毋庸赘述,大家都能看出来含义。再来看下如下的应用场景:

  1. class CustomHTMLElement {
  2. constructor (element) {
  3. this.element = element
  4. }
  5. get html () {
  6. return this.element.innerHTML
  7. }
  8. set html (value) {
  9. this.element.innerHTML = value
  10. }
  11. }

利用 set/get 实现了对 element.innerHTML 的简单封装。
可是,有时候我们真的需要设置一个私有属性(闭包),然后通过一定的规则来限制对它的修改,利用 set/get就可以轻松实现。

  1. let #age = 1
  2. class Animal {
  3. constructor(type) {
  4. this.type = type
  5. }
  6. get age() {
  7. return #age
  8. }
  9. set age(val) {
  10. if (val > 0 && val < 10) {
  11. #age = val
  12. }
  13. }
  14. }

Static Methods

静态方法是面向对象最常用的功能,在 ES5 中利用 function 实现的类是这样实现一个静态方法的。

  1. let Animal = function (type) {
  2. this.type = type
  3. this.walk = function () {
  4. console.log(`I am walking`)
  5. }
  6. }
  7. Animal.eat = function (food) {
  8. console.log(`I am eating`);
  9. }

在 ES6 中使用 static 的标记是不是静态方法,代码如下:

  1. class Animal {
  2. constructor (type) {
  3. this.type = type
  4. }
  5. walk () {
  6. console.log(`I am walking`)
  7. }
  8. static eat () {
  9. console.log(`I am eating`)
  10. }
  11. }

有没有很清爽,代码可读性一下子就上来了。

Sub Classes

面向对象只所以可以应对复杂的项目实现,很大程度上要归功于继承。如果对继承概念不熟悉的同学,可以自行查询。在 ES5 中怎么实现继承呢?

  1. // 定义父类
  2. let Animal = function (type) {
  3. this.type = type
  4. }
  5. // 定义方法
  6. Animal.prototype.walk = function () {
  7. console.log(`I am walking`)
  8. }
  9. // 定义静态方法
  10. Animal.eat = function (food) {
  11. console.log(`I am eating`)
  12. }
  13. // 定义子类
  14. let Dog = function () {
  15. // 初始化父类
  16. Animal.call(this, 'dog')
  17. this.run = function () {
  18. console.log('I can run')
  19. }
  20. }
  21. // 继承
  22. Dog.prototype = Animal.prototype

从代码上看,是不是很繁琐?而且阅读性也较差。再看看 ES6 是怎么解决这些问题的:

  1. class Animal {
  2. constructor (type) {
  3. this.type = type
  4. }
  5. walk () {
  6. console.log(`I am walking`)
  7. }
  8. static eat () {
  9. console.log(`I am eating`)
  10. }
  11. }
  12. class Dog extends Animal {
  13. constructor () {
  14. super('dog')
  15. }
  16. run () {
  17. console.log('I can run')
  18. }
  19. }

虽然 ES6 在类的定义上仅是 ES5 定义类的语法糖,但是从开发者的角度而言,开发更有效率了,代码可阅读性大大提升。

练习

  1. 请实现一个堆栈类,具备 push、pop 功能。
  2. 请回忆下自己在业务中有哪些场景可以用类来实现。

    阅读