跳到主要內容

JavaScript 物件導向探討

last updated:2013/05

參考文章:
談JavaScript使用prototype實作物件導向的探討
JavaScript使用物件導向技術來建立進階 Web 應用程式
Javascript繼承機制的設計思想
Javascript面向對象編程(二):構造函數的繼承

如同大多數語言一樣,Javascript的base class是Object
物件是一組 key / value 的dictionary
也因為如此,再加入新屬性、物件時也很寬鬆(抓錯當然也比較困難)
如下示範:

var newobj = {};                                     //等同 var newobj = new Object();
var obj = {"liar" : 4, "test" : new Date()};
obj.test2 = " new object!! ";                     //能隨時加入新屬性的特性
delete obj.test2;                                       //簡單的刪除屬性方法
alert(obj.liar);        
alert(obj["test"]);

delete運算符使用上還是有限制,可以參考這篇文章
JavaScript可輕易建立全域變數,但全域變數的使用會削弱程式的彈性
減少全域變數的使用的方法之一是建立唯一的全域變數,然後用它來保存應用程式
var MyApp = {};
MyApp.obj1 = {
    "first": "test",
    last: {
        alfa: 1,
        beta: 2
    }
};

function在Javascript中也是物件,等同於class的使用
所以下列寫法均有相同效果
//1
function func(x){
    alert(x);
}
func("show1");

//2
var func = function(x){
    alert(x);
};
func("show2");

//3
var func = new Function("x", "alert(x);");    //Function類別也是確實可行
func("show3");


Javascript的this指標會指向呼叫的物件本身,如下範例:
function displayQuote() {
    // this會依據呼叫的物件而改變
    alert(this.memorableQuote);  
}

var williamShakespeare = {
    "memorableQuote": "It is a wise father that knows his own child.",
    "sayIt" : displayQuote                    //引用 displayQuote
};

var markTwain = {
    "memorableQuote": "Golf is a good walk spoiled.",
    "sayIt" : displayQuote                  //引用 displayQuote
};

var oscarWilde = {
    "memorableQuote": "True friends stab you in the front."
};

williamShakespeare.sayIt();
markTwain.sayIt();

/*
oscarWilde 則示範了另一種引用方式
每個function都有call()這個funciton, 會將傳入的物件作為其本身(this)來使用
*/
displayQuote.call(oscarWilde);

call()和另一個沒提到的apply()能將丟進去的物件指定為this
兩個函式的差別在於後續要傳入的參數形式,後續可看到示範
.call(oscarWilde, 參數1, 參數2, ...etc)
.apply(oscarWilde, 參數陣列)

使用this時要注意,如果沒指定物件作為this指向的對象時
則有可能覆寫全域函式,如下示範:
alert("NaN is NaN: " + isNaN(NaN));     //true

function x() {
    this.isNaN = function() {
        return "changed!";
    };
}
x();    //指定物件的意思是例如宣告var test = new x(); ,test的isNaN()的this指向自己

alert("NaN is NaN: " + isNaN(NaN));    //changed!

this的使用上需注意內層函式的this不是連結到外層的this, 而是直接連結到全域變數
這違反直覺的設計算是JavaScript的程式設計錯誤之一
解決方式是建立一個變數儲存外層的this給內層函式使用:
obj.double = function () {
    var t = this;
    var helper = function () {
        t.value = ...
    };
    helper();
};

回頭看Javascipt建立物件的過程
使用類別的過程如下:
function Animal(describe) {
    this.describe = describe;
    this.action = function () {
        alert(describe);
    };
}

var obj1 = new Animal("eat");
obj1.action();

/*
new Animal這行等同於
var obj1 = {};
Animal.call(obj1, "eat");
建立新物件後將它設為function的this值
*/


使用new關鍵字時,Javascript會先建立一個空物件
接著設定物件的prototype指向建構式的prototype
然後呼叫建構式並將所建立的空物件設為this
prototype是JavaScript獨特的機制,意義等同於類別的屬性

每次在Javascript建立一個物件,其實就是複製一次類別的內容(前面說過類別也是物件)
若上例建立100個animal物件,在過程中也一併建立了100個action
而同一個類別的物件不管複製多少個,都會共用相同的屬性(prototype 物件)
所以這個概念衍伸出,如果類別裡有共用的物件或方法,在設計的時候就應該把它想成屬性
每次建立物件時就不會重複建立內部物件,以減少資源的浪費
prototype示範如下:
function Animal(describe) {
    this.describe = describe;
    Animal.prototype.action = function () {
        alert(describe);
    };
}

var obj1 = new Animal("eat");
var obj2 = new Animal("sleep");

//注意!輸出皆是sleep,因為共用的關係,所以action會被後續的obj2覆蓋
obj1.action();                              
obj2.action();

prototype 本身是用object產生的物件,所以也都有著Object.prototype
函式的情況是綁在Function.prototype,Function.prototype再綁到 Object.prototype 上
Javascript使用函式的過程會是這樣,以toString()為例
先在物件本身尋找function > 接著往物件的prototype上找 > 再往prototype的prototype=Object.prototype上找
所以可以知道所有的物件都從Object繼承了toString()
這種鏈狀結構被稱為原型鏈

每個 prototype 都能取得constructor屬性指向其建構式
因為原型鏈的機制,物件可以取得 constructor 屬性
這個 constructor 的來源就是物件的 prototype 的 constructor
所以相同建構式 new 出來的物件會有相同的 constructor 屬性

在JavaScript製作物件靜態方法或變數的方式如下,注意必須在建構式後才能加入靜態方法或變數:
function ST() {}
ST.show = function () {
    return "hey";
};

alert(ST.show());

接著是以Closure模擬 private 屬性,先看以下程式碼
function Person(name, age) {
    this.getName = function () {
        return name;
    };
    this.setName = function (newName) {
        name = newName;
    };
    this.getAge = function () {
        return age;
    };
    this.setAge = function (newAge) {
        age = newAge;
    };
}

關鍵點在於建構式中傳進去的name跟age會成為無法被外層直接存取的區域變數
只能透過 getName、getAge等方法對這兩個區域變數進行處理
上述的作法是以類別成員的方式建立對外開放的函式
實際上有做法能讓開放的介面意象更明確

var animal = function() {
    var name = 'tom';

    var API = {
        "getName": function(){ return name;}
    };
    return API;
}();

alert(animal.getName());

從外部無法存取函式內的區域變數,所以封裝了 'name'
將存取的 function 集中在 'API' 物件中並回傳,表明只能透過 'API'介面裡的方法作處理

物件導向的另一重要特性是繼承機制
讓程式碼得以重複使用以提高開發效率
繼承的實現方式在繼承機制一文中解釋

留言