D18 - 吃一颗 Class 语法糖 (下)比较 Constructor 与 Class

前言

语法糖 Syntactic sugar,指电脑语言中添加的某种语法,这种语法对语言的功能没有影响,但是让程序更加简洁,有更高的可读性。

class 称为 ES6 新增的语法糖,也意味着没有新功能的变动,而是对於物件的继承与建造有更方便的写法,今天就来比较一下使用 constructor 与 ES6 增加的 class 有什麽写法上的差异吧!

图片来源 CLEANPNG


本文段落分为:

  • class 与 constructor 使用七大差异
  • class-based 与 prototype-based 范例比较

class 与 constructor 比较

七大差异比较:

1. hoisting 失效

函式可以在定义呼叫使用,这称为 function declaration 有 hoisting 的缘故,但是在 class 使用 class declaration 并不会有 hoisting 效果,必须先定义 class 後才能呼叫!否则抛出 error 讯息 not defined,这个错误讯息意味着不允许在宣告之前实体化 instantiate 一个类别。

// constructor
let cookie1 = new Cookie(); // chocolate
function Cookie(){
    console.log('chocolate')
}

// class
let cake1 = new CakeClass(); // error; CakeClass is not defined
class Cake {
    constructor(){
        console.log('tiramisu')
    }
}

2. 必加 new

建立新物件使用 constructor 时,若忘记加上 new 关键字,虽然不会产生新的物件但也不会报错,而是变成的 function 赋值,若没有 return 印出会是 undefined;
但在 class 使用上规定要加上new ,否则抛出错误讯息

Uncaught TypeError: Class constructor Cake cannot be invoked without 'new'

// constructor
function Cookie(flavor) {
    this.flavor = flavor;
}

let cookie1 = Cookie('mocha'); // 没有加上 new,cookie1 印出 undefined
let cookie2 = new Cookie('chocolate'); // cooke2 -> Cookie {flavor: 'chocolate'}加上 new 後,Cookie 为 constructor 建立出实例 cookie2

// class
class Cake {
    constructor(flavor) {
        this.flavor = flavor;
    }
}

let cake1 = Cake('cheezeCake');  // error; 必须加上 new
let cake2 = new Cake('tiramisu'); // cake2 -> Cake {flavor: 'tiramisu'}

3. 自动写入 prototype

constructor 的共享属性需要透过 constructor.prototype 的方式写入原型物件中,class 可以直接写在 class body 内 {},会自动写入该类别的 prototype 中。

// constructor
function Cookie(flavor){
    this.flavor = flavor;
}

Cookie.prototype.price = (cost) => `NTD ${cost * 1.5}`

let cookie1 = new Cookie('chocolate')
console.log( cookie1 )     // {flavor: 'chocolate'}
console.log( cookie1.price(50)) // NTD 75

// class
class Cake {
    constructor(flavor){
        this.flavor = flavor;
    }
    price(cost) {
        return `NTD ${cost * 1.5}`
    }
}

let cake1 = new Cake('tiramisu')
console.log( cake1.price(50) )   // NTD 75

4. static 静态方法

静态方法只能透过 constructor 或是 class 呼叫,并不会继承,因此由 constructor、class 建立的实例 instance 无法取得,constructor 可以使用 . 方法写入; 而 class 只要加上 static 关键字就可直接在 class body 内定义。

// constructor
function Cookie(flavor) {
    this.flavor = flavor;    
}

Cookie.secret = ()=> {console.log('老板娘很票酿')}

let cookie1 = new Cookie('chocolate')
cookie1.secret()  // error
Cookie.secret()   // 老板娘很票酿


// class
class Cake {
    constructor(flavor) {
        this.flavor = flavor;
    }
    price(cost) {
        return `NTD${cost* 1.5 }`
    }
   static secret(){
       console.log('老板娘很票酿')
   } 
}

let cake1 = new Cake('tiramisu')
cake1.secret()    // error
Cake.secret()   // 老板娘很票酿

5. extends 扩增原型链

在 ES6 以前,若想扩增原型链,必须在 constructor 的原型中创立一个新物件,再指定继承母阶的 prototype,并且须重新设定 constructor 属性指回自己; 而 class 大大简化了这个步骤,透过 extendsuper 语法绑定 parentClass 。

// constructor
function Pastry(category){
    this.category = category;
}
Pastry.prototype.owner = ()=> {console.log('Hoo')}
function Cookie(flavor){
    this.flavor = flavor;
}

// 新增 Pastry 到原型链上
Cookie.prototype = Object.create(Pastry.prototype)
Cookie.prototype.constructor  = Cookie

// 之後
let cookie1 = new Cookie('chocolate')
cookie1.owner()  // Hoo,成功继承到 Pastry prototype
cookie1.__proto__.__proto__ === Pastry.prototype // true


// class 使用 extend 和 super 简化写法
class Cake extends Pastry{
    constructor(flavor){
        super('cake');
        this.flavor = flavor;
    }
}
let cake1 = new Cake('tiramisu')
cake1.owner()  // Hoo
cake1.__proto__.__proto__ === Pastry.prototype   // true

6. 严格模式

使用 class 不管是 declaration 或 expression 都会自动开启严格模式,就算没有加上 use strict 字样,所有严格模式禁止行为在 class 区块内都会抛出错误!

严格模式禁止行为请看-> D4 - 加盐不加价 严格模式开启

以严格模式下禁止使用的保留字 package 测试看看

// 一般 function declaration 
function func(){
    let package = '一般模式 package 不是保留字';
    console.log(package)
}

func()    // 一般模式 package 不是保留字

// class declaration
class ClassFunc {
    constructor(){
        let package = '印不出来';
        console.log(package)
    }
}

// Uncaught SyntaxError: Unexpected strict mode reserved word

7. DevTools

在 Chrome DevTools 印出可以很明确的知道这个实例是由 constructor 还是 class 建立。

由 constructor 建立的实例,[[Prototype]] 属性下的 constructor 显示为 function

由 class 建立的属性,[[Prototype]] 属性下的 constructor 显示为 class

差异图表整理

difference constructor class
code function 开头 class 开头,实例内容写在 constructor
hoisting function declaration 有 hoisting 没有 hoisting
new 没有加上 new 不会生成新物件,但也不会报错 必须加上 new 才能呼叫 class,否则抛出错误讯息
严格模式 加上 use strict 才是严格模式 class 函式自动成为严格模式
原型方法 原型方法写在 prototype 物件内 原型方法写在 class body 内
静态方法 静态方法须另外写在 constructor 属性内 在 class body 中加上 static 就可增加静态方法
继承 另外指派 prototype 来增加继承对象 使用 extends 增加 parentClass
Devtools 显示 function 显示 class

class-based 与 prototype-based class 比较

纯粹的 class-based 物件导向语言,物件是由 class 创造,因此所有方法及成员须一开始写在 class 内,无法在 class 外的区域定义,因此当实例已经建立完成,就无法再额外新增属性。

而 JavaScript 就不限定啦!基於 prototype 设定,物件被创立後可以自己再新增内部属性,或是跟着继承来的 prototype 内容变动。

举例:建立一个 Employee class 并 new 出实例 john,分别以 Java 及 JavaScript 写法做比较

Java 写法

// 建立 Employee class
public class Employee {
 
    public String employeeName = "Default name";
    public int employeeId = 0;
     
    public Employee(String name, String id) {
        System.out.println("Employee class instantiated");
        this.employeeName = name;
        this.employeeId = id ;
    }
 
    public void printEmployee() {
        System.out.println("Name: " + employeeName + " Id: " + employeeId);
    }
    setEmployeName(String name, String id){}
    getEmployeeName(String name, String id){}
}

// new + 建构子建立实例
Employee john = new Employee("John”, 123); // output: "Employee class instantiated"
 
john.printEmployee(); // output: "Name: John Id: 123"

JavaScript 写法

class Employee {
    constructor (name, id) {
        this.name = name;
        this.id = id;
    }
    getDetails() {
        return `${this.name} , ${this.id}`;
    }
}
 
// new + class 建立物件
let john = new Employee("John", 123);
 
// john 建立後可以继续增加属性
john.saysHello = function() {
    console.log(this.name + " says: Hello World!");
}

john.getDetails()  // "John , 123"
john.saysHello()  // John says: Hello World!

Reference

忍者JavaScript 开发技巧探秘 2
MDN - class
As a JS Developer, This Is What Keeps Me Up at Night
The Difference Between Java And JavaScript

结语

从物件导向开始到原型链、 class 与 constructor 比较,这系列终於结束!
终於又把一个大坑填平,填坑挖坑的过程真是又累又满足啊~
/images/emoticon/emoticon37.gif


<<:  Re: 新手让网页 act 起来: Day18 - React Hooks 之 useRef

>>:  # Day#18 设定、上传照片与default值

DAY2: 何谓node.js?

学习一门全新的东西,势必要先打好根基,为了求快而省略了一些基础,那麽在之後的学习上,可能会碰上一些需...

为何软件产品要进行免费开源 或 免费使用

七年前,我在跟主管讨论我手头目前使用开源函式库来进行程序编辑时,老板问了一个问题:『为何他们愿意提供...

System Design: 读书心得1

原文在这: Title: How WhatsApp enables multi-device cap...

Day30|Git 学习资源与方式暨完赛心得

30 天的铁人赛终於来到尾声,回想自己能够连续 30 天都上传文章实在太不可思议! 以学习 Git ...