在了解了 Prototype Chain 之後,我們要來利用物件導向的概念,近一步了解在 ES5、 ES6 之間實作的差異,以及 new、super、封裝 的意義是什麼。
new 關鍵字 : 模擬 new 在背後幫你做的事
上一篇文章分享過要建立一個新的物件來繼承另一個資源的方法時,需要用到 new
這個關鍵字,現在我們要來探討,當我們使用 new 時,背後系統幫我們做了什麼
接下來以一個自己寫的 function newDog('yello'),來模擬 new Dog('yello') 在做的事:
const yello = newDog('yello');
white.sayHi();
yello.sayHi();
首先目的是通過自己寫的 newDog 產生一個 instance yello,希望這個 instance 會有 跟用 new 出來的 instance white 有一樣的屬性跟方法。
所以目標放在如何寫出 newDog() 來模擬 new 這個關鍵字背後做的事情。
function newDog(name) {
const obj = {}; // 1.
Dog.call(obj, name); // 2.
obj.__proto__ = Dog.prototype; // 3.
return obj; // 4.
}
- new 背後做的事:
- 產生一個新 object => obj
- 把 Dog 當作 constructor,將 this 指向 obj,同時把參數 name 丟進去
=> 因為 Dog 只有一個參數所以用 call,如果是兩個以上的參數就可以用 apply 放陣列 - 將
obj.__proto__
指向Dog.prototype
- 回傳 obj
在 ES5 時,通常是把 function 當成 constructor 來用,稱為「 建構函式 」,而共用的 method 則會放在 prototype 新增,可以避免在每個 instance 重複新增內容一樣的 method。
而關鍵在於 new 關鍵字,用 new 出來的 function, JS 會在背後幫你做好一連串的機制。
接下來我們來看,到了 ES6 之後,寫法上面會變成什麼樣子
ES5 VS ES6
ES5 : 建構函式 + prototype
function Dog(name) {
this.name = name;
}
Dog.prototype.sayHello = function() {
console.log(this.name, ' Hello');
}
const happy = new Dog('happy');
happy.sayHello();
const white = new Dog('white');
white.sayHello();
ES6 : Class
class Dog {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(this.name, ' Hello');
}
}
const happy = new Dog('happy');
happy.sayHello();
const white = new Dog('white');
white.sayHello();
在 ES6 之後,開始有了 Class 這個語法糖可以使用。當我們想要建立一個物件導向的資源時,感覺有點像宣告一個 class 的物件,把所有跟這個物件相關的 property & method 通通包在一起的感覺。
如此一來只要是在 class 之中宣告的 function 都會被加入 prototype。
extends 繼承
當我們建立好ㄧ個 class 之後,可以以這個 class 為基礎,新建一個類似的 class。就像我想要以「狗」這個類別為基礎,新建一個類別叫「柴犬」,它可以繼承「狗」的屬性及方法。
而這個新建語法就是 extends
- class 柴犬 extends 狗
class Dog {
...
}
class TaiwansDog extends Dog {
...
}
super
在 ES6 使用繼承時,如果要將 子類別的 constructor
改寫,需要先呼叫 super() 這個函式,否則會跳出錯誤:Must call super constructor in derived class before accessing 'this' or returning from derived constructor。
class Dog {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(this.name, 'Hi');
}
}
class TaiwansDog extends Dog {
constructor(name) {
super(name); // => 記得要先 super()
this.sayHi();
}
}
const white = new Dog('white');
const black = new TaiwansDog('black');
封裝
先前在 Closure 的概念當中有提到,如果你希望一個變數是不能被任意賦值的,可以將它封裝起來,使外部無法任意存取。
- ES5 - function 模擬 Class 版 ( real private )
function Wallet(init) {
let money = init;
this.getMoney = function() {
return money;
}
};
const wallet = new Wallet(100);
console.log(wallet.getMoney());
console.log(wallet.money); // => 存取不到,真正的 private
- ES6 - constructor 版 ( real private )
一般在 class 裡面初始化變數,會在 constructor 裡面用 this.variable = variable
的寫法,用這個方式宣告出來的變數,是能夠從外部存取的
class Wallet {
constructor(num) {
this.money = num;
}
};
const wallet = new Wallet(100);
console.log(wallet.money) // 100
但是若是在 constructor 裡面用 var
、let
或者 const
宣告變數,只要在 constructor 外部就通通存取不到了,所以需要存取 private property 的 function 只能一並寫在 constructor 裡面。
- private property 在命名的時候習慣性以
_
開頭
class Wallet {
constructor(num) {
var _money = num;
this.getMoney = () => _money;
this.setMoney = (newNum) => _money = newNum;
}
getMoney = () => _monsy; // 像這樣寫在 constructor 外面的 function 就存取不到 _money 了
};
const wallet = new Wallet(100);
console.log(wallet.getMoney());
console.log(wallet._money); // => 存取不到,真正的 private
現在 ES6 還沒有看到通用的方法,最簡單的方法其實可以再 private property or method 的名稱加上 _ 前綴,同事們可以很簡單看出這是個私有屬性,沒事不要去動它。