當javaScript從入門到提高前需要注意的細節:對象部分

最近看了一個帖子,樓主抱怨說javaScript不是標準的面嚮對象語言,最多勉強算是基於面向對象的語言。這樣的說法也是非常有現實的市場的,主要是基於class的編程語言影響力太大了,C++、java、C#哪個不是名門之後,搞得大家現在一說面向對象就是必須有class關鍵字了。

面向對象的開發編程只是一種編程的思想,和對編程的指導意見(設計模式更是一種經驗的歸納和名稱,絕對不是聖經來的)。面向對象的思想主要是建議開發人員注意幾個事項:儘可能的實現代碼重用(過程性的函數也是代碼重用來的),儘可能的爲特定的個體設計獨特的數據類型,在這個獨特的類型中包含了這個類型所有自我完成的運算,但是僅僅將需要調用者關心的數據和函數公開,其他都屏蔽掉。

具體一個語言是怎麼實現面向對象的,和麪向對象的開發準則並沒有具體的約定。由於歷史原因,我們大部分的面向對象編程選擇了C++風格的靜態CLASS方式,所以我們很多人就習慣上了基於CLASS的面向對象編程方式而已。而且CLASS從C++開始到java再到C#,編譯器廠商爲了討好開發人員再語法糖上面下了極大的功夫,推陳出新了衆多用於描述對象封裝,繼承,多態應用的關鍵字,讓我們開發效率極大的提高,自然我們也就喜歡上了CLASS的編程,也自然的認可了CLASS作爲面向對象開發的標準或代言人。

既然我們開始理解了面向對象的編程不僅僅有基於CLASS的方式,還有其他的各種方式,比如VB是基於IMPLEMENTS的實現方式,而javaScript是基於PROTOTYPE來實現的。

javaScript的特殊在於,其沒有類,所有的一切都是實例(這裏我特別的用了實例而不是對象,就是擔心說都是對象其實是不嚴謹的),從類型來觀察,javaScript提供了6種我們可以訪問的基本數據類型:undefined、null、boolean、string、number、object,以下的代碼在null的時候有點另人疑惑,其他都蠻正常,原因是在javaScript中null被定義爲了Null。

alert(typeof undefined);  
alert(typeof null);  
alert(typeof 123);  
alert(typeof 123.45);  
alert(typeof true);  
alert(typeof false);  
alert(typeof "Hello");  
alert(typeof {});  

說到Null,我們知道在javaScript中還有String、Date、Number、Boolean、Object、Function,在java或C#中,我們可以判斷出這些應該是對象,而且是對基本數據類型的隱射,但是在javaScript中,那就完全不同

alert(typeof Number);  
alert(typeof String);  
alert(typeof Date);  
alert(typeof Boolean);  
alert(typeof Function);  
alert(typeof Object);  

以上得到的結果全是function。這點就告訴我們兩個基本事實:1 javaScript中只有object是對象數據類型(就這麼唯一一個);2 對象和函數直接的確有着非常曖昧的關係。

如果對以上的函數採用new運算符,我們得到的結果是

alert(typeof new Number());  
alert(typeof new String());  
alert(typeof new Date());  
alert(typeof new Boolean());  
alert(typeof new Function());  
alert(typeof new Object());  

我們可以觀察到除了Function之外,其他都返回對象。現在我們開始進入主題。

在javaScript中最簡單的對象定義就是使用字面量進行定義

var obj = {};  
  
var Poker = {  
    Title: "K",  
    Style: "spade",  
    Value: 13  
};  

上面這段代碼,我們可以得到兩個對象。要記住在javaScript中,不採用CLASS來抽象對象,所有的一切值都是實例的。我們不需要任何CLASS就可以得到一個對象,使用{}就是聲明瞭一個對象實例。在這樣的一個直接對象中所定義的值默認都是公開的,試圖採用var在字面量的對象中聲明一個私有屬性是不可以的。需要說明下,javaScript對象是一個key-value的集合,我們說的屬性其實是一個key,正確的將應該是我們在Poker中定義了3個可便利的Key:Title,Style和Value。講屬性那是方便使用C#、Java的程序員瞭解。 javaScript對面向對象的封裝和CLASS的不同,他沒有提供public和private等訪問修飾符。如果我們使用現代的瀏覽器,比如IE9,Chrome等支持ECMAScript5 的瀏覽器,那麼我們可以定義不可遍歷的key,並且可以爲key來定義set和get,並且可以約定一個key只讀,好像已經接近CLASS的習慣了,如下是一個demo

var obj = {};  
  
var Poker = {  
    Title: "K",  
    Style: "spade",  
    Value: 13,  
    State: 1  
};  
  
  
Object.defineProperties(  
Poker,  
{  
    "backgroundImg": {  
        value: "images\\common\\hide.png",  
        enumerable: false//不可以for 遍歷  
        writable: false//只讀  
    },  
    "forgroundImg": {  
        value: "images\\spade\\K.png",  
        enumerable: false//不可以for 遍歷  
        writable: false//只讀  
    },  
    Img: {  
        get: function() {  
            return this.State == 0 ? this.backgroundImg : this.forgroundImg;  
        },  
        enumerable: true  
  
    }  
}  
);  
  
alert(Poker.Img); //images\\spade\\K.png  
  
for (var key in Poker) {  
    alert(key); //backgroundImg 和  forgroundImg無法被遍歷到  
}  
  
alert(Poker.backgroundImg); //依然可以訪問  
Poker.backgroundImg = "XXX";  
alert(Poker.backgroundImg); //依然是images\\common\\hide.png  

但使用字面量直接定義對象的話,至少還有兩個麻煩,1是重用比較麻煩,2是對私有變量的封裝還是不給力。雖然說字面量創建的對象也是可以繼承的比如

var Poker = {  
    State: 1  
};  
  
var PokerA = {  
    Title: "A",  
    Style: "spade",  
    Value: 14,  
    __proto__: Poker  
};  
  
var Poker10 = {  
    Title: "10",  
    Style: "club",  
    Value: 10,  
    __proto__: Poker  
};  
  
  
alert(PokerA.Title); //A  
alert(PokerA.State); //1  
alert(Poker10.Title); //10  
alert(Poker10.State); //1  
//在IE8-訪問  
alert(PokerA.__proto__.State);  
alert(Poker10.__proto__.State);  

因爲javaScript中字面量的對象都是實例,我們不管這樣的繼承是不是比較麻煩或者另類,但至少我們可以認識到,Poker對象(實例是一直存在的)每一個繼承者都需要一個Poker的實例空間

Poker.State = 3;  
PokerA.State = 0;  
alert(PokerA.State); //0  
alert(Poker10.State); //3  
alert(Poker.State); //3  

以下的代碼更是明確的告訴我們,Poker的實例中this指向的是實際調用方法的子類(當然你承認這個是子類的話)

var Poker = {  
    State: 1,  
    toString: function() {  
        alert(this.Style + this.Title + ":" + this.Value);  
    }  
};  
  
var PokerA = {  
    Title: "A",  
    Style: "spade",  
    Value: 14,  
    __proto__: Poker  
};  
  
var Poker10 = {  
    Title: "10",  
    Style: "club",  
    Value: 10,  
    __proto__: Poker  
};  
  
  
PokerA.toString(); // spadeA: 14  
Poker10.toString(); //club10:10  

同時,我們可以瞭解到一個繼承鏈的事情是:當一個被調用的key在當前對象沒有定義的時候,會順着繼承鏈向上去找,直到找到第一個key存在爲止(object自身的toString在這個demo中被無視了),如果都沒有就是undefined了。你可以認爲是CLASS中的重寫來實現多態吧,我想可以這麼說。

一般來講,我不建議在字面量(我用這個詞非常不舒服,我喜歡叫直接對象)用來繼承,直接對象最有價值的用法就是作爲參數或什麼的用一次,簡單高效,看的明白。如果要合理的使用內存,靈活的創建對象,那必須用function才比較爽。

var Poker = function(style, title, value, state) {  
    this.Title = title;  
    this.Style = style;  
    this.Value = value;  
    this.State = arguments[3] === undefined ? 0 : state;  
    var backgroundImg = "images\\common\\hide.png";  
    var forgroundImg = "images\\spade\\" + title + ".png";  
    this.Img = (function(x) {  
        return x == 0 ? backgroundImg : forgroundImg;  
    } (this.State));  
    this.toString = function() {  
        return this.Style + this.Title + ":" + this.Value  
    }  
}  
  
var Poker10 = new Poker("spade""10""10");  
var PokerA = new Poker("spade""A""14", 1);  
  
alert(PokerA.toString()); // spadeA: 14  
alert(Poker10.toString()); //club10:10  
alert(PokerA.Title); //A  
alert(PokerA.State); //1  
alert(Poker10.Title); //10  
alert(Poker10.State); //0  
alert(PokerA.Img); //images\spade\A.png  
alert(Poker10.Img); //images\common\hide.png  

上面的代碼顯然舒適多了,而且容易理解。我不反對吧Poker這個函數叫構造函數,事實上他也的確幫助我們構造了一個Poker對象,不過由於javaScript沒有CLASS的概念,所以我更喜歡傾向於說:Poker函數幫助我們構造了一個以Poker爲原型的對象。叫起來非常的繞口,那就還是說是構造函數吧。

無論是直接對象還是函數構造得到的對象,對於javaScript來講都是動態的,這個實例可以動態的添加key。

看看以下代碼,在構造完成了Pkoner10和PokerA之後,我們在Poker的原型上創建了一個新的函數createDom,這個函數依然對Pkoner10和PokerA是有效的。

var Poker10 = new Poker("spade""10""10");  
var PokerA = new Poker("spade""A""14", 1);  
  
Poker.prototype.createDom = function() {  
    return $("<img id=" + this.Style + this.Title + ">");  
}  
  
  
$(  
function() {  
    PokerA.createDom().appendTo("body");  
    Poker10.createDom().appendTo("body");  
}  
);  

不過你要記得,function新加的key必須加在prototype上,以下代碼就是錯誤的

Poker.insert = function(box, poker) {  
    poker.appentTo(box);  
}  
  
PokerA.insert("body", PokerA.createDom()); //錯誤了  

原因我估計是這樣的,function當函數用的時候就是函數,當用new構造的時候,編譯器將this指向的key防止到了function的prototype(原型)上,以下算是一個證明吧

alert(Poker.prototype.constructor === Poker); //true  
alert(PokerA.constructor === Poker); //true  
alert(PokerA.createDom === PokerA.__proto__.createDom); //true  
alert(PokerA.__proto__.constructor === Poker.prototype.constructor); //true

說到key了,正好說下javaScript中的數組,數組一般在靜態語言的概念中式連續分配的一段內存,大小是固定的。不過javaScript中的數組我看其實是對象的變形

var arr = [];  
for (var i = 0; i < 5; i++) {  
    arr[i] = i;  
}  
arr.push(99);  
  
for (var index in arr) {  
    alert(index); // 0 1 2 3 4 5  
}  

最後的for得到的不是數組的值,而是數組的index,所以我們可以這樣求值

var arr = [];  
for (var i = 0; i < 5; i++) {  
    arr[i] = i;  
}  
arr.push(99);  
  
for (var index in arr) {  
    alert(arr[index]);  
}  

所以,估計arr[i] = i是動態的添加了一個屬性,並賦值而已啦。

還是回到構造函數來說,一般看來,爲了符合面向對象的一個編程意思:對象是自己的一組數據的集合,所以我們一般在構造函數中定義對象的屬性(也就是數據),數據由構造函數的參數提供,而對象的方法在外部用函數編寫,並且指向這個構造的原型。原因是在javaScript中字面量的對象沒有property……

這個property就是function構造出的實例,如果把方法直接寫在function上,那就是這個function的靜態成員了,估計可以這麼說,一下是一個demo

function Poker(style, title, value) {  
    this.Style = style;  
    this.Title = title;  
    this.Value = value;  
}  
  
Poker.max = function(Poker1, Poker2) {  
    var p;  
    for (var i = 0; i < arguments.length; i++) {  
        if (arguments[i] instanceof Poker) {  
            if (typeof p == "undefined") {  
                p = arguments[i];  
            }  
            else {  
                p = p.Value > arguments[i].Value ? p : arguments[i];  
            }  
        }  
    }  
    return p;  
}  
  
  
var p = Poker.max(new Poker("club""K", 13),  
            new Poker("diamond""A", 14),  
            "",  
            new Poker("diamond""10", 10));  
  
alert(p.Style + p.Title + "[" + p.Value + "]"); // diamondA[14]  

那麼函數,構造函數,對象之間的原型關係到底怎麼樣的呢?下面的代碼描述了這些關係

alert(Poker.constructor); //function Function() { [native code] }  
alert(new Poker().constructor); //function Poker(style, title, value) {  
//            this.Style = style;  
//            this.Title = title;  
//            this.Value = value;  
//        }  
alert(Poker.constructor.prototype); //function Empty() {}  
alert(Poker.prototype == new Poker().constructor.prototype); // true  
alert(Poker.constructor.prototype == new Poker().constructor.prototype); // false  
alert(new Poker().propertye); //undefined  

函數的構造函數是Function對象;

函數的對象化(函數創建的對象)的構造函數是function的定義;

函數的構造函數的原型是一個空函數;

函數的原型和函數創建的對象的構造函數的原型相等(所以對象是函數構造出來的);

函數的構造函數的原型和函數的對象化的構造函數的原型不同;

對象沒有原型可以直接訪問(這個和字面量的對象一樣的);

上面的文字看上去和繞口令一樣的,但多念念就明白了……

所以證明前面的說法:如果需要函數的成員讓實例訪問,要麼加到函數代碼裏面,要麼在外面加到函數的原型上去

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章