第 6 章 - 物件導向與原型
最簡單的物件
範例: people.js
var People={
name:"john",
age:30,
print:function() {
console.log("name=", this.name, "age=", this.age);
}
}
People.print();
執行結果:
$ node people.js
name= john age= 30
範例: circle.js
var circle = {
r:3,
area:function() {
return 3.14*this.r*this.r;
}
}
console.log("circle.r=%d", circle.r);
console.log("circle.area()=%d", circle.area());
執行結果
NQU-192-168-60-101:ccc csienqu$ node circle.js
circle.r=3
circle.area()=28.259999999999998
複數的範例
接著,我們將以複數 (Complex Number) 為範例,用最簡單的方式闡述 JavaScript 的物件導向設計法。
但必須聲明的是,我們不採用傳統的 new 方式進行說明,因為那種方式很詭異,不容易看清楚 JavaScript 物件導向的特性。
相反的、我們採用 Object.create() 的方式作為入門踏板,因為這種方式比較好理解。
要理解 JavaScript 的物件導向之前,先讓我們看看傳統的非物件導向寫法,怎麼樣撰寫一個複數 (Complex Number) 的模組。
非物件的寫法
檔案: complex.js
function add(c1, c2) {
return { r:c1.r+c2.r, i:c1.i+c2.i };
}
function sub(c1, c2) {
return { r:c1.r-c2.r, i:c1.i-c2.i };
}
function mul(c1, c2) {
return { r:c1.r*c2.r-c1.i*c2.i,
i:c1.r*c2.i+c1.i*c2.r };
}
function toStr(c) {
return c.r+"+"+c.i+"i";
}
var a = { r:1, i:2 }, b={ r:2, i:1 };
var add12 = add(a, b);
var sub12 = sub(a, b);
var mul12 = mul(a, b);
console.log("a=%s", toStr(a));
console.log("b=%s", toStr(b));
console.log("add(a,b)=%s", toStr(add12));
console.log("sub(a,b)=%s", toStr(sub12));
console.log("mul(a,b)=%s", toStr(mul12));
執行結果
NQU-192-168-60-101:object csienqu$ node complex.js
a=1+2i
b=2+1i
add(a,b)=3+3i
sub(a,b)=-1+1i
mul(a,b)=0+5i
您可以看到這種寫法也很模組化,看起來相當不錯,只是函數是函數,資料是資料,函數只是用來處理資料的程式,此種寫法還沒有用到物件導向的技術。
接著、讓我們來看一個簡化後的物件導向版本,這個簡化後的版本只有一種運算函數,那就是加法 add 。
物件寫法 1 : ocomplex1.js
檔案: ocomplex1.js
var Complex = {
add:function(c2) {
return createComplex(this.r+c2.r, this.i+c2.i);
}
}
var createComplex=function(r,i) {
var c = Object.create(Complex);
c.r = r;
c.i = i;
return c;
}
var a=createComplex(1,2), b=createComplex(2,1);
var x = a.add(b).add(b).add(b);
console.log("a=%j", a);
console.log("b=%j", b);
console.log("a.add(b)=%j", a.add(b));
console.log("x=%j", x);
執行結果
$ node ocomplex1.js
a={"r":1,"i":2}
b={"r":2,"i":1}
a.add(b)={"r":3,"i":3}
x={"r":7,"i":5}
在上述程式中,我們透過 Object.create(Complex) 創造一個物件之後,立刻在其中塞入 r, i 等欄位,此時雖然物件看來只有兩個欄位,但事實上還有一些隱藏的物件資訊沒有被印出來,其中的一個隱藏物件資訊就是原型,在 JavaScript 中的物件都有一個原型 prototype 的欄位,這個欄位在執行完 Object.create(Complex) 後,會指向 Complex 物件。
var createComplex=function(r,i) {
var c = Object.create(Complex);
c.r = r;
c.i = i;
return c;
}
上述程式中我們在 log() 函數中用了 %j 的格式,這代表要將該物件以 json 的方式印出來。
因此當我們後來呼叫 c1.add(c2) 這樣的指令時,JavaScript 的解譯系統才能夠從 c1 這個物件中找到 add 這個欄位,然後將其當成函數使用。
這種用點符號 "." 串起來的寫法可以一直串下去,成為一種串式語法,因此我們可以用 c1.add(c2).add(c2).add(c2).add(c2) 進行連續的加法。
物件寫法 2 : ocomplex2.js
在物件的原型 prototype 裏通常還有些其他未顯示出來的函數,像是 toString() 就是一個很好用的函數,假如我們為物件加上 toString() 函數的話,那麼在該物件需要被轉換成字串的時候,就會自動呼叫 toString() ,以下是我們為上述 ocomplex1.js 程式加上 toString() 函數之後的結果,這個版本稱為 ocomplex2.js 。
檔案: ocomplex2.js
var Complex = {
add:function(c2) {
return createComplex(this.r+c2.r, this.i+c2.i);
},
toString:function() {
return this.r+"+"+this.i+"i"
}
}
var createComplex=function(r,i) {
var c = Object.create(Complex);
c.r = r;
c.i = i;
return c;
}
var a=createComplex(1,2), b=createComplex(2,1);
var x = a.add(b).add(b).add(b).add(b);
console.log("a=%s", a);
console.log("b=%s", b);
console.log("a.add(b)=%s", a.add(b));
console.log("x=%s", x);
執行結果
$ node ocomplex2.js
a=1+2i
b=2+1i
a.add(b)=3+3i
x=9+6i
您可以看到由於我們加入了 toString() 函數,而且在印出來的語法上採用了 %s 這個字串式印法,於是在印到螢幕前 console.log 會先呼叫這些複數物件的 toString() 函數,結果印出來的格式就好看很多了。
物件寫法 3 : ocomplex3.js
當然、我們也可以把減法 sub() 和乘法 mul() 函數放到這個物件導向版的複數程式中,這樣就和前面的非物件導向版功能相當了,以下是這個比較完整的版本。
檔案: ocomplex3.js
var Complex = {
add:function(c2) {
return createComplex(this.r+c2.r, this.i+c2.i);
},
sub:function(c2) {
return createComplex(this.r-c2.r, this.i-c2.i);
},
mul:function(c2) {
return createComplex(this.r*c2.r-this.i*c2.i,
this.r*c2.i+this.i*c2.r);
},
toString:function() {
return this.r+"+"+this.i+"i"
}
}
var createComplex=function(r,i) {
var c = Object.create(Complex);
c.r = r;
c.i = i;
return c;
}
var a=createComplex(1,2), b=createComplex(2,1);
var x = a.add(b).sub(b).mul(b);
console.log("a=%s", a);
console.log("b=%s", b);
console.log("a.add(b)=%s", a.add(b));
console.log("x=%s", x);
執行結果
$ node ocomplex3.js
a=1+2i
b=2+1i
a.add(b)=3+3i
x=0+5i
上述程式雖然很完整了,但是在語法上 createComplex() 沒有和 Complex 物件直接綁釘在一起,感覺怪怪的。為了讓語法更漂亮,我們乾脆將該函數直接塞回 Complex 物件內,成為 Complex.create() 函數,這樣感覺就更「物件化」了一些。請看以下的版本!
物件寫法 4 : ocomplex4.js
檔案: ocomplex4.js
var Complex = {
add:function(c2) {
return Complex.create(this.r+c2.r, this.i+c2.i);
},
sub:function(c2) {
return Complex.create(this.r-c2.r, this.i-c2.i);
},
mul:function(c2) {
return Complex.create(this.r*c2.r-this.i*c2.i,
this.r*c2.i+this.i*c2.r);
},
toString:function() {
return this.r+"+"+this.i+"i"
}
}
Complex.create=function(r,i) {
var c = Object.create(Complex);
c.r = r;
c.i = i;
return c;
}
var a=Complex.create(1,2), b=Complex.create(2,1);
var x = a.add(b).sub(b).mul(b);
console.log("a=%s", a);
console.log("b=%s", b);
console.log("a.add(b)=%s", a.add(b));
console.log("a.sub(b)=%s", a.sub(b));
console.log("a.mul(b)=%s", a.mul(b));
console.log("x=%s", x);
執行結果
$ node ocomplex4.js
a=1+2i
b=2+1i
a.add(b)=3+3i
a.sub(b)=-1+1i
a.mul(b)=0+5i
x=0+5i
必須注意的是,這種寫法仍然必須把 Complex.create 提出來到外面寫,否則在 Complex 都尚未創建完成時就要用 Object.create(Complex) 創建 Complex 物件的話,就會產生錯誤了。
物件寫法 5 : pcomplex.js (採用建構函數)
上述幾種寫法都使採用 Object.create 的方式,根據某物件創造出新物件。
但是、 JavaScript 的典型物件寫法,是採用《建構函數+原型鏈》的方式進行物件導向設計的,以下我們將介紹這種典型做法。
首先讓我們看看 javascript 當中的建構函數怎麼寫,以下同樣用《複數物件》當作範例。
檔案: pcomplex1.js
var Complex=function(r,i) {
this.r = r;
this.i = i;
this.add=function(c2) {
return new Complex(this.r+c2.r, this.i+c2.i);
}
this.sub=function(c2) {
return new Complex(this.r-c2.r, this.i-c2.i);
}
this.mul=function(c2) {
return new Complex(this.r*c2.r-this.i*c2.i,
this.r*c2.i+this.i*c2.r);
}
this.toString=function() {
return this.r+"+"+this.i+"i"
}
}
var a=new Complex(1,2), b=new Complex(2,1);
var x = a.add(b).sub(b).mul(b);
console.log("a=%s", a);
console.log("b=%s", b);
console.log("a.add(b)=%s", a.add(b));
console.log("a.sub(b)=%s", a.sub(b));
console.log("a.mul(b)=%s", a.mul(b));
console.log("x=%s", x);
執行結果
$ node pcomplex1.js
a=1+2i
b=2+1i
a.add(b)=3+3i
a.sub(b)=-1+1i
a.mul(b)=0+5i
x=0+5i
您可以看到這個版本的 Complex 並不是個物件,而是一個函數,我們用 new Complex(1,2) 這樣的語句呼叫這個建構函數,創建出新物件。
var Complex=function(r,i) {
this.r = r;
...
}
...
var a=new Complex(1,2), b=new Complex(2,1);
然後在建構函數當中,我們仍然可以使用 this 代表這個物件,將內容放到物件裡面。
物件寫法 6 : pcomplex2.js (採用建構函數)
雖然上一個範例採用了建構函數,但是這種寫法的每個物件裡面,都會儲存一份 add, sub, mul 等函數,如果有 100 個複數物件,就會儲存 100 份這種函數,這顯然是很浪費空間的做法。
還好、javascript 提供了一個稱為《原型》的機制,讓我們可以《共用》這些《應該共用且只有一份》的欄位與函數,以下就是一個採用《原型寫法》的複數物件程式。
檔案: pcomplex2.js
var Complex=function(r,i) {
this.r = r;
this.i = i;
}
Complex.prototype.add=function(c2) {
return new Complex(this.r+c2.r, this.i+c2.i);
}
Complex.prototype.sub=function(c2) {
return new Complex(this.r-c2.r, this.i-c2.i);
}
Complex.prototype.mul=function(c2) {
return new Complex(this.r*c2.r-this.i*c2.i,
this.r*c2.i+this.i*c2.r);
}
Complex.prototype.toString=function() {
return this.r+"+"+this.i+"i"
}
var a=new Complex(1,2), b=new Complex(2,1);
var x = a.add(b).sub(b).mul(b);
console.log("c1=%s", a);
console.log("c2=%s", b);
console.log("c1.add(c2)=%s", a.add(b));
console.log("c1.sub(c2)=%s", a.sub(b));
console.log("c1.mul(c2)=%s", a.mul(b));
console.log("x=%s", x);
執行結果
$ node pcomplex2.js
c1=1+2i
c2=2+1i
c1.add(c2)=3+3i
c1.sub(c2)=-1+1i
c1.mul(c2)=0+5i
x=0+5i
其中採用 prototype 關鍵字的語句,就是所謂的原型。
Complex.prototype.add=function(c2) {
return new Complex(this.r+c2.r, this.i+c2.i);
}
ES6 新版的類別寫法
檔案: ComplexClass.js
class Complex {
constructor(r,i) {
this.r = r;
this.i = i;
}
add(c2) {
return new Complex(this.r+c2.r, this.i+c2.i);
}
sub(c2) {
return new Complex(this.r-c2.r, this.i-c2.i);
}
mul(c2) {
return new Complex(this.r*c2.r-this.i*c2.i,
this.r*c2.i+this.i*c2.r);
}
toString() {
return this.r+"+"+this.i+"i"
}
}
var a=new Complex(1,2), b=new Complex(2,1);
var x = a.add(b).sub(b).mul(b);
console.log("a=%s", a);
console.log("b=%s", b);
console.log("a.add(b)=%s", a.add(b));
console.log("a.sub(b)=%s", a.sub(b));
console.log("a.mul(b)=%s", a.mul(b));
console.log("x=%s", x);
執行結果:
$ node ComplexClass.js
a=1+2i
b=2+1i
a.add(b)=3+3i
a.sub(b)=-1+1i
a.mul(b)=0+5i
x=0+5i
不管你總共建立了多少 Complex 物件, Complex 的 prototype 都只會有一份,因此這種寫法會比前一種寫法更省記憶體。
而這種寫法,也是最經典的 javascript 物件導向寫法。
在一般的物件導向語言裡,會有《繼承》的機制,但是在 javascript 的早期版本 (ES6 之前) 裡面,並沒有《繼承》的機制。
但是、javascript 仍然可以做到類似《繼承》的功能,方法是在原型裏再塞入原型,這種原型裡面還可以有原型的做法,真的是非常美妙的一種設計阿!
習題
1 請寫出一個具有加減乘除運算的複數物件? (Complex, add, sub, mul, div) (除法可以不寫,算加分題)
提示:第一題請參考本章內文
2 請寫出一個具有『加、減、內積、負』的向量物件? (Vector, add, sub, dot, neg)
提示:第二題架構如下:
class Vector {
add(v2) { ... }
sub(v2) { ... }
dot(v2) { ... }
neg() { ... }
}
3 請寫一組物件,包含《矩形、圓形》與抽象的形狀,每個物件都具有 area() 函數可以計算其面積? (Shape.area(), Rectangle, Circle)
提示:第三題架構如下:
class Shape {
}
class Circle {
constructor(radius) {...}
area() { ... }
}
class Rectangle {
constructor(width, height) {...}
area() { ... }
}
4 請寫一組物件,包含『浮點數,有理數,複數』,這三個都繼承『數』這個物件,而且每個都具有 add, sub, mul, div, power 等成員函數!
提示:第四題的架構如下:
class Number {
power(n) {
var p = this;
for (var i=1; i<n; i++) {
p = p.mul(this);
}
return p;
}
}
class Float extends Number {
add(o2) { ...}
sub(o2) { ...}
mul(o2) { ...}
div(o2) { ...}
toString() {... }
}
class Rational extends Number {
add(o2) { ...}
sub(o2) { ...}
mul(o2) { ...}
div(o2) { ...}
toString() {... }
}
class Complex extends Number {
add(o2) { ...}
sub(o2) { ...}
mul(o2) { ...}
div(o2) { ...}
toString() {... }
}
第四題不含有理數與浮點數,只有 Number 和 Complex 的版本如下:
class Number {
power(n) {
var p = this;
for (var i=1; i<n; i++) {
p = p.mul(this);
}
return p;
}
}
class Complex extends Number {
constructor(r,i) {
super(r,i);
this.r = r;
this.i = i;
}
add(c2) {
return new Complex(this.r+c2.r, this.i+c2.i);
}
sub(c2) {
return new Complex(this.r-c2.r, this.i-c2.i);
}
mul(c2) {
return new Complex(this.r*c2.r-this.i*c2.i,
this.r*c2.i+this.i*c2.r);
}
toString() {
return this.r+"+"+this.i+"i"
}
}
var a=new Complex(1,2), b=new Complex(2,1);
var x = a.add(b).sub(b).mul(b);
console.log("a=%s", a);
console.log("b=%s", b);
console.log("a.add(b)=%s", a.add(b));
console.log("a.sub(b)=%s", a.sub(b));
console.log("a.mul(b)=%s", a.mul(b));
console.log("x=%s", x);
console.log("a.power(3)=%s", a.power(3));