iT邦幫忙

2021 iThome 鐵人賽

DAY 18
0
Modern Web

舌尖上的JS系列 第 18

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


上一篇
D17 - 吃一顆 Class 語法糖 (上)
下一篇
D19 - 今晚我想來點 唯獨派 getter 唯寫派 setter
系列文
舌尖上的JS30

尚未有邦友留言

立即登入留言