Day 12 - OOP 初探 (2) - Class

前言

昨天讲完 Javascript OOP 两个重要支柱,今天接着这个主题,来讲讲 class 吧!

Class(类别)

Class 可以想像成印章,每压一下就盖出一个印,每 new 一下就产生一个物件。

class 是 ECMAScript 6 引入的语法,但由於 Javascript 仍是基於原型(prototype-based)的语言,所以这个所谓的 class,其实也只是语法糖,Javascript 依然没有真正的 "class",所以透过原型链(prototype chain) 来营造出继承的效果。

Class 宣告方式

class 可以当作特别形式的函式,所以宣告也有分 class expressions 跟 class declarations,我个人比较喜欢後者,可以少写一点XD

class expressions

const Person = class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
};

class declarations

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

上述都只是宣告,现在用 new 来真正将这些 class「实体化」:

const person = new Person('Joey', 20);

Class 语法介绍

基本上 class 内的语法,跟我们昨天用 function 在写的时候其实非常像,只是有几点需要注意:

constructor,用来建立和初始化一个类别的物件,里面基本上就是在做初始化,且一个 class 里面只能有一个。

而一般的 method 则是像下面这样,用一般的非箭头函式,但 function 关键字也可以省略:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  sayHello() {
      return `Hello, ${this.name}~`;
  }
}

const person = new Person('Joey', 20);
console.log(person.sayHello());

执行结果

Hello, Joey~

class 有几个分解动作,是 Javascript 背後帮我们完成的:

  • class 里的 constructor() 抓出来,指定给 Person
  • class 里的其他 method 指定给 Person.prototype

你会发现其实用 function 都做得到,只是用 class 会比较有在写 OOP 的感觉,所以才说 class 是「特别形式」的 function。

extends 子类别

比如说我们有个 class 叫做 Animal

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 发出声音!');
  }
}

Animal 是个比较大的范围,动物(基本上)都会发出声音,如果今天我们需要建立一个猫的类别,猫也是动物的一种,所以动物会有的属性,猫都会有:

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 发出声音!');
  }
}

class Cat {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 发出声音!');
  }
}

可以看到其实很大量的 code 在重复,有这种大类别包含小类别的状况,就可以使用 extends 去继承大类别:

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 发出声音!');
  }
}

class Cat extends Animal {}

const cat = new Cat('Lulu', 5);
cat.speak();
console.log(cat.age);

执行结果

Lulu 发出声音!
5

覆写父类别的 method

但基本上不会只写一行

class Cat extends Animal {}

因为如果子类别长得跟父类别一模一样,那好像也没什麽必要分出来,假如我们希望让猫的声音更有区别性,可以去覆写父类别的 method:

class Animal {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  speak() {
    console.log(this.name + ' 发出声音!');
  }
}

class Cat extends Animal {
  speak() {
    console.log(this.name + ' 喵~~~~');
  }
}

const cat = new Cat('Lulu', 5);
cat.speak();
console.log(cat.age);

执行结果

Lulu 喵~~~~!
5

super 呼叫父类别建构子

但你是否有注意到一点,Cat 类别里面怎麽会没有 constructor method?这样也能跑吗?

其实是可以的,因为如果用 extends 语法去继承父类别,而又没有给 constructor method 的时候,Javascript 会在背後偷偷帮你补上:

class Cat extends Animal {
  // 这一段是自动补上的 start
  constructor(...args) {
    super(...args);
  }
  // 这一段是自动补上的 end
  
  speak() {
    console.log(this.name + ' 喵~~~~');
  }
}

这个 super 是只能够用在子类别的语法,意思是呼叫父类别的建构子。constructor 里面务必要先执行 superthis 才会有东西,再开始使用 this.namethis.age 之类的属性,不然像这样死掉哦:

Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

类别方法 v.s. 实体物件方法

发现 class 讲完还有一点时间,我们再来看看,同样是 method,写在类别上,跟写在实体化的物件上,有什麽样的差别呢?

假如我们一样是电商系统,要判断 user 的钱包有没有办法负担商品价格,所以我们有个 checkPriceAffordable 的 method。

方案A,如果写在类别上是这样:

class User {
    constructor(name, money) {
        this.name = name;
        this.money = money;
    }
    
    checkPriceAffordable(price) {
        return this.money >= price;
    }
}

const user = new User('Joey', 2000);

if (user.checkPriceAffordable(1000)) {
    console.log('可以买! 买起来!!!!');
}

执行结果

可以买! 买起来!!!!

方案 B,写在实体化出来的物件里面:

class User {
    constructor(name, money) {
        this.name = name;
        this.money = money;
        this.checkPriceAffordable = function(price) {
            return this.money >= price;
        }
    }
}

const user = new User('Joey', 2000);

if (user.checkPriceAffordable(1000)) {
    console.log('可以买! 买起来!!!!');
}

执行结果

可以买! 买起来!!!!

差别其实就在於

  • 方案A(存在类别里)从头到尾都只有一个 checkPriceAffordable
  • 方案B(存在实体化物件里)则是new 一次就复制一个

效能落差?

当我们呼叫 user.checkPriceAffordable() 的时候,Javascript 是怎麽找到 checkPriceAffordable 这个 method 呢? 

查找的顺序就是由内而外,沿着原型链往上找,先看 user 物件本人有没有这个 method,如果没有就顺着 __proto__ 往上看 User 这个类别有没有。

所以理论上好像会是存在实体化物件上,呼叫的效率最高。

但事实上是,Javascript 当然也知道这一点,因此有针对原型链查找的效能最佳化,其实花费的时间不会差太多,但可以肯定的却是,存在实体化物件中,每复制一份就多花一份记忆体来存。

因此原则上还是把共用的 method 直接写在类别或原型上即可。

结语

Javascript 虽然常被说很松散,过度自由,但也是因为如此,要写 FP 要写 OOP,都可以自由选择,class 虽然也是透过 prototype 做出来的语法糖,少了像是 privatepublic 这种方便的语法,但有总比没有好,让这个语言充满了非常多可能性!

在喧嚣嘈杂的社会里
或许你我都是
寂静的克隆体

参考资料

Class MDN
深入浅出 JavaScript ES6 Class


<<:  30-12 之 Domain Layer - Domain Model ( 未完成版 )

>>:  【面试】技术与专案问答

Day22 火焰文字

火焰文字 教学原文参考:火焰文字 这篇文章会介绍在 GIMP 里使用涂抹工具、渐层映对、文字...等...

【Day 6】Git分支(branch)

何谓Git分支? 说明 : 在实务开发上,通常会有主要运行版本、测试版本,以及跟员工A、B自己各别修...

【day10】狗一下居酒食堂

今天来介绍很难预约的狗一下居酒食堂 这是去年12月底去吃的 并提前一个月预定 当天的方案是88道菜吃...

【开篇 / 大纲】现在才努力是不是搞错了什麽?

前几礼拜终於收到挂号寄来的大学学费总收据, 开始估计自己的价值和手上的筹码。 演算法竞赛选手的深厚 ...

JavaScript学习日记 : Day1 - 前言

参赛动机 厘清JavaScript中自己一知半解的概念 透过写文章加强记忆,培养自己写文章的能力 ...