物件與類別
上個章節中, 提到了原型鏈的概念
繼續深入類別之前, 先來看看ES 5之前的類別是如何實現的:
/* Class Example */
function Rect(w,h) {
if(!(this instanceof Rect)) // No
throw new Error('is constructor');
this.w = w;
this.h = h;
}
Rect.prototype.add = function (r) {
if(!(r instanceof Rect))
throw new TypeError('Only allow Rect-type');
this.w += r.w;
this.h += r.h;
}
Rect.prototype.print = function () {
console.log(this.w + ', ' + this.h);
}
let r1 = new Rect(1,1);
let r2 = new Rect(2,2);
r1.add(r2);
r1.print(); // '3, 3'
這裡定義 Rect 型別, 並宣告了 add 與 print 方法的實作。
當宣告了一個物件時, 便可以同時定義他的prototype
已知, 當調用 function 的順序(例如 Rect.add)時, 會依序查找Rect.add Rect.prototype.add Object.prototype.add
/* Class-like Example */
function Rect(w,h) {
return {
w: w,
h: h,
add: function(r) {
this.w += r.w;
this.h += r.h;
},
print: function() {
console.log(this.w + ', '+ this.h);
}
}
}
let r1 = Rect(1,1);
let r2 = Rect(2,2);
r1.add(r2);
r1.print()
這個行為"看起來"會跟使用 new Rect 一樣, 但是有個非常嚴重的問題, 就是r1.add !== r2.add
這個問題的嚴重性在於, 假定定義了像是MyClass 這種類別, 並且有個方法 myFunction, 當建立了 1000 個實例
myFunction 也會被建立1000次, 這對於記憶體的處理是非常不健康的
邏輯上, 成員變數應該保持在自己的scope, 而方法(例如 MyClass.myFunction) 是一個獨立的 function, 由所有的MyClass共用該 function 位址
僅需要傳入自己的參考, MyClass 便會假設 this 是自己傳進來的參考;
再次使用C++來舉例, C++的class實作隱含了this參數, 比方說
class Point {
public:
Point(int x, int y): x_(x), y_(y) {}
void add(Point p) {
this.x_ += p.x_;
this.y_ += p.y_;
}
private:
int x_;
int y_;
}
實際上, add 的簽章會包含一個隱含的參數 this
void Point::add(Point* this, Point p) {
this.x_ += p.x_;
this.y_ += p.y_;
}
如果要驗證這一點, 通過 std::bind 這個函式可以更好的觀察到
#include <iostream>
#include <functional>
class Point {
public:
Point(int x, int y): x_(x), y_(y) {}
void add(Point& p) {
this->x_ += p.x_;
this->y_ += p.y_;
}
int x_;
int y_;
};
int main() {
Point p1(1,1);
Point p2(3,3);
Point POINT(10,10);
auto fn = std::bind(&Point::add, &POINT, std::placeholders::_1);
fn(p1);
std::cout << POINT.x_ << ", " << POINT.y_ << "\n";
//11, 11
fn(p2);
std::cout << POINT.x_ << ", " << POINT.y_ << "\n";
//14, 14
}
逐步拆解以上的過程:
class Point宣告了addfunction, 允許兩個Point進行加法- 把
fn通過std::bind繫結了Point::add這個函式, 並且把this的 Context 繫結在POINT上 - 透過
std::placeholders, 繫結fn的第一個引數與Point::add的第一個參數 - 調用
fn(p1)時, 相當於調用了POINT.add(p1) - 調用
fn(p2)時, 相當於調用了POINT.add(p2)
以筆者的理解來說明:
在Class的實現上, 可以拆解為 屬性 以及 方法
屬性是由實例自行維護的數據區塊
方法則是所有的實例共享同樣的函式宣告與實作
所有的方法雖然共用相同的function 區段, 但是因為隱含了 *this, 所以不同實例調用方法才會呈現不同的結果
可以參考 MSDN C++ 上的 __thiscall
部分程式語言, 如 Rustlang, 則要求在成員的方法實作, 顯式宣告第一個參數為 &self
在 JavaScript 上, 早期的 Class 實作要求使用 function 來宣告, 在 Rect 該例中
早期開發人員通過 if(!(this instanceof Rect)) 判斷 Rect 是通過建構式被調用, 還是通過一般函式被調用
因為一般函式與建構式的調用, this 的數值是不相同的(在下個章節進行討論)
function 的定義統一被移到 <Class Name>.prototype 這個區段, 而屬性則由實例自行維護
因此前兩個例子中:
Class Example所有的Rect實例, 會共享Rect.prototype.add與Rect.prototype.print的實現Class-like Example所有的Rect實例, 不會共享Rect.prototype.add與Rect.prototype.print的實現, 相當於add與print的實現每次在Rect()調用時, 都被重新宣告/實現一次。
在例子2中, 使用的實例越多, 記憶體的使用則越劇烈
ES6 底下的類別
幸好在 ES6 (ECMA 2016)以後的標準, 提供了 class 與 extends 關鍵字, 上方的 Rect 可寫為:
class Rect {
constructor(w, h) {
this.w = w;
this.h = h;
}
add(r) {
this.w += r.w;
this.h += r.h;
}
print() {
console.log(this.w + ', ' + this.h);
}
}
let r1 = new Rect(1,1);
let r2 = new Rect(2,2);
r1.add(r2);
r1.print(); // '3, 3'
倘若多定義了 Square 正方形類別, 只需要透過 extends 關鍵字即可
class Square extends Rect {
constructor(w) {
super(w, w);
}
getArea() {
return this.w * this.h;
}
}
let s1 = new Square(10);
s1.getArea(); // 100
super 關鍵字會依照不同context, 決定super的數值。在此處中, 是調用 Parent Class 的建構式
在以前, 需要手動處理 prototype 的指向來模擬繼承敘述:
// Rect - 父類別
function Rect(w,h) {
if(!(this instanceof Rect))
throw new Error('is constructor');
this.w = w;
this.h = h;
}
// 父類別的方法
Rect.prototype.add = function (r) {
if(!(r instanceof Rect)) // 可以通過 "instanceof" 判斷傳入的原型
throw new TypeError('Only allow Rect-type');
this.w += r.w;
this.h += r.h;
}
Rect.prototype.print = function () {
console.log(this.w + ', ' + this.h);
}
// Square - 子類別
function Square() {
Rect.call(this); // call super constructor.
}
// 子類別擴展(extends)父類別
Square.prototype = Object.create(Rect.prototype);
Square.prototype.constructor = Square;
var rect = new Square();
console.log('Is rect an instance of Square?', rect instanceof Square); // true
console.log('Is rect an instance of Rect?', rect instanceof Rect); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
倘若加上了 static 關鍵字, 其行為如同 C++的靜態方法:
class Square extends Rect {
static internal = -1;
constructor(w) {
super(w, w);
}
getArea() {
return this.w * this.h;
}
}
let s1 = new Square(10);
s1.getArea(); // 100
Square.internal // -1
// ES 5 以前:
function Square(w) {
if(!(this instanceof Square))
throw new Error('');
}
Square.prototype.getArea = function(){ /* impl */ }
Square.internal = -1;
此外, 類別可以使用 new Square 或是 new Square() 的方式初始化(假設建構式不需要參數), 他們的差異是運算子優先順序
因為成員存取算運子.的優先度比較高, 所以使用
new Square.getArea 會導致錯誤, 因為 Square 不存在 getArea 這個靜態方法, 但是使用
new Square().getArea 則不會出錯, 因為他實際上調用了 (new Square).getArea