檔案: 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 o1 = { r:1, i:2 }, o2={ r:2, i:1 };

var add12 = add(o1, o2);
var sub12 = sub(o1, o2);
var mul12 = mul(o1, o2);

var c = console;

c.log("o1=%s", toStr(o1));
c.log("o2=%s", toStr(o2));
c.log("add(c1,c2)=%s", toStr(add12));
c.log("sub(c1,c2)=%s", toStr(sub12));
c.log("mul(c1,c2)=%s", toStr(mul12));

執行結果

D:\Dropbox\cccwd\db\js\code>node complex.js
o1=1+2i
o2=2+1i
add(c1,c2)=3+3i
sub(c1,c2)=-1+1i
mul(c1,c2)=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 c = console;

var c1=createComplex(1,2), c2=createComplex(2,1);

var c3 = c1.add(c2).add(c2).add(c2).add(c2);

c.log("c1=%j", c1);
c.log("c2=%j", c2);
c.log("c1.add(c2)=%j", c1.add(c2));
c.log("c3=%j", c3);

執行結果

D:\Dropbox\cccwd\db\js\code>node ocomplex1.js
c1={"r":1,"i":2}
c2={"r":2,"i":1}
c1.add(c2)={"r":3,"i":3}
c3={"r":9,"i":6}

在上述程式中,我們透過 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 c = console;

var c1=createComplex(1,2), c2=createComplex(2,1);

var c3 = c1.add(c2).add(c2).add(c2).add(c2);

c.log("c1=%s", c1);
c.log("c2=%s", c2);
c.log("c1.add(c2)=%s", c1.add(c2));
c.log("c3=%s", c3);

執行結果

D:\Dropbox\cccwd\db\js\code>node ocomplex2.js
c1=1+2i
c2=2+1i
c1.add(c2)=3+3i
c3=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 c = console;

var c1=createComplex(1,2), c2=createComplex(2,1);

var c3 = c1.add(c2).sub(c2).add(c2).sub(c2);

c.log("c1=%s", c1);
c.log("c2=%s", c2);
c.log("c1.add(c2)=%s", c1.add(c2));
c.log("c1.sub(c2)=%s", c1.sub(c2));
c.log("c1.mul(c2)=%s", c1.mul(c2));
c.log("c3=%s", c3);

執行結果

D:\Dropbox\cccwd\db\js\code>node ocomplex3.js
c1=1+2i
c2=2+1i
c1.add(c2)=3+3i
c1.sub(c2)=-1+1i
c1.mul(c2)=0+5i
c3=1+2i

上述程式雖然很完整了,但是在語法上 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 c = console;

var c1=Complex.create(1,2), c2=Complex.create(2,1);

var c3 = c1.add(c2).sub(c2).add(c2).sub(c2);

c.log("c1=%s", c1);
c.log("c2=%s", c2);
c.log("c1.add(c2)=%s", c1.add(c2));
c.log("c1.sub(c2)=%s", c1.sub(c2));
c.log("c1.mul(c2)=%s", c1.mul(c2));
c.log("c3=%s", c3);

執行結果

D:\Dropbox\cccwd\db\js\code>node ocomplex4.js
c1=1+2i
c2=2+1i
c1.add(c2)=3+3i
c1.sub(c2)=-1+1i
c1.mul(c2)=0+5i
c3=1+2i

必須注意的是,這種寫法仍然必須把 Complex.create 提出來到外面寫,否則在 Complex 都尚未創建完成時就要用 Object.create(Complex) 創建 Complex 物件的話,就會產生錯誤了。

結語

以上是我們對 JavaScript 物件導向機制的一個簡單入門,但是並不完整,因為我們還沒有看到更深入的《原型鏈》機制,關於《原型鏈》的概念我們將在後續的文章中再來探討。