︿
Top

2015年2月28日 星期六

Javascript: 如何建立物件 (Part 1)


緣起

由於網頁的前端技術越來越熱門, 很多 Framework / Library (ex: jQuery, AngularJS / Bootstrap ... ) 或技術 (ex: / HTML5 / CSS3 / TypeScript / SCSS ...) 不斷出現. 但其中最主要的基礎還是在 HTML5 / CSS3 / Javascript.
本文主要針對 Javascript 建立物件的方式及相關的注意事項, 作了整理; 畢竟, 想要看懂Javascript Framework (ex: jQuery) 的內容, 還是要有一些底子, 特別是物件的建立及的物件導向觀念,
筆者才疏學淺, 目前的功力只能寫到物件建立的方式; 至於物件導向的實作, 則需另外再花時間研究.

完整範例, 請由此下載.


基本共用方法


為了能夠列出物件所包含屬性 (properties) 的 key / value 值對, 故撰寫了一些基本的共用方法, 提供後續範例使用.

// ==================================================
// 註: 
// 1. 本函式以 <signature> 模擬 overload 的情境
// 2. 本函式提供的2種處理方式, 在同一個頁面裡, 不可混用, 只能擇一使用
// 3. 本函式的第1個方式, 因為係以純文字模式輸出, 外層必須加上 <pre> </pre> 的 tag,
//     才能輸出 \t, \r 這類的控制字元, 達到階層的效果
// 4. 本函式的第2個方式 (採用 ul tag), 無法呈現階層性, 且在頁面需有以下元素
//      <body>
//          <ul id="foo"></ul>
//      </body>
// ==================================================
function dispLog(msg, ul) {
    /// <signature>
    ///  <summary>顯示訊息, using document.writeln()</summary>
    ///  <param name="msg" type="String">要顯示的訊息</param>
    /// <returns type="void"></returns>
    /// </signature>
    /// <signature>
    ///  <summary>顯示訊息, using page "ul" element</summary>
    ///  <param name="msg" type="String">要顯示的訊息</param>
    ///  <param name="ul" type="Boolean">是否要與 id 為 foo 的 ul 標籤整合</param>
    /// <returns type="void"></returns>
    /// </signature>
    var d = new Date();
    if (ul === undefined) {
        document.writeln(d.toISOString().substr(14, 9) + " " + msg);
    }
    else {
        if (ul === false) {
            document.writeln(d.toISOString().substr(14, 9) + " " + msg);
        }
        else {
            //with pure Javascript
            var obj = window.document.getElementById("foo");
            var text = obj.innerHTML;
            text += "<li>" + d.toISOString().substr(14, 9) + " " + msg + "</li>";
            obj.innerHTML = text;
            ////with jQuery
            //$("<li />").text(d.toISOString().substr(14, 9) + " " + msg).appendTo("#foo");
        }
    }
}

function iterateObjectAllProperties(obj, level, onlyOwnedProperties) {
    /// <signature>
    ///  <summary>列舉物件的屬性 name, value 值對</summary>
    ///  <param name="obj" type="Object">被列舉的物件</param>
    ///  <param name="level" type="Number">第幾層 (物件的某個屬性可能也是物件, 要一併列舉)</param>
    /// <returns type="void"></returns>
    /// </signature>
    /// <signature>
    ///  <summary>列舉物件的屬性 name, value 值對</summary>
    ///  <param name="obj" type="Object">被列舉的物件</param>
    ///  <param name="level" type="Number">第幾層 (物件的某個屬性可能也是物件, 要一併列舉)</param>
    ///  <param name="onlyOwnedProperties" type="Boolean">只列舉單純屬於該物件的屬性</param>
    /// <returns type="void"></returns>
    /// </signature>
    function doOutput()
    {
        var msg = "";

        //前置放 tabs
        if (level !== 0) {
            for (var i = 0; i < level ; i++) {
                msg += "\t";
            }
        }
        if (obj[prop] === null) {
            msg += "obj[" + prop + "] = " + null;
            dispLog(msg);
        } else {
            try {
                msg += "obj[" + prop + "] = " + obj[prop].toString();
            } catch (e) {
                msg += "obj[" + prop + "] = " + e.message;
            }
            dispLog(msg);
        }
        //如果是子物件, 就遞迴 ...
        if (typeof (obj[prop]) == "object" && obj[prop] != null) {
            level++;
            iterateObjectAllProperties(obj[prop], level, onlyOwnedProperties);
            level--;
        }
    }

    var listAll = false;
    for (var prop in obj) {
        msg = "";
        if (onlyOwnedProperties === undefined) {
            listAll = true;
        } else {
            if (onlyOwnedProperties === false) {
                listAll = true;
            }
        }

        //如果全部列舉
        if (listAll == true) {
            doOutput();
        } else {
            //如果只列舉物件本身
            if (obj.hasOwnProperty(prop)) {
                doOutput();
            }
        }
    }
}


function iterateObjectProperties(obj, onlyOwnedProperties) {
    /// <signature>
    ///  <summary>列舉物件的屬性 name, value 值對</summary>
    ///  <param name="obj" type="Object">被列舉的物件</param>
    /// <returns type="void"></returns>
    /// </signature>
    /// <signature>
    ///  <summary>列舉物件的屬性 name, value 值對</summary>
    ///  <param name="obj" type="Object">被列舉的物件</param>
    ///  <param name="onlyOwnedProperties" type="Boolean">只列舉單純屬於該物件的屬性</param>
    /// <returns type="void"></returns>
    /// </signature>
    iterateObjectAllProperties(obj, 0, onlyOwnedProperties);
}


方式一: Object Literal

使用時機: 

建立一個單純的 Javascript 物件, 類似 C# 的匿名物件

範例1.1 -- 利用 Object Literal 建立物件 (有點類似 C# 的 anonymous 物件) 


// =================================================
// 以 Object Literal 的方式建立物件
// =================================================
function createByObjectLiteral() {
    /// <summary>利用 Object Literal 建立物件 (有點類似 C# 的 anonymous 物件) </summary>
    var empty = {};                   // An object with no properties (Object.prototype)
    var point = { x: 0, y: 0 };     // Two properties
    var point2 = { x: point.x, y: point.y + 1 };        // More complex values
    var book = {
        "main title": "JavaScript",                           // 屬性名稱含有 空白, "-", 或 保留字,,
        'sub-title': "The Definitive Guide",            // 必須以雙引號括住
        "for": "all audiences",                                 //
        author: {                                                       // 屬性本身為子物件
            firstname: "David",                                 //
            surname: "Flanagan"                             //
        },
        //出版商資訊
        publisher: {
            name: "O'Reilly",
            pressContacts: ["North America", "Japan", "Taiwan"]        //陣列也是物件
        },
        email: "maureen@oreilly.com"
    }

    document.write("<pre>");
    iterateObjectProperties(empty);
    document.write("</pre>");

    document.write("<pre>");
    iterateObjectProperties(point);
    document.write("</pre>");

    document.write("<pre>");
    iterateObjectProperties(point2);
    document.write("</pre>");

    document.write("<pre>");
    iterateObjectProperties(book);
    document.write("</pre>");

    //輸出結果:
    //31:47.830 obj[x] = 0
    //31:47.830 obj[y] = 0

    //31:47.830 obj[x] = 0
    //31:47.831 obj[y] = 1

    //31:47.831 obj[main title] = JavaScript
    //31:47.831 obj[sub-title] = The Definitive Guide
    //31:47.831 obj[for] = all audiences
    //31:47.831 obj[author] = [object Object]
    //31:47.832         obj[firstname] = David
    //31:47.832         obj[surname] = Flanagan
    //31:47.832 obj[publisher] = [object Object]
    //31:47.832         obj[name] = O'Reilly
    //31:47.832         obj[pressContacts] = North America,Japan,Taiwan
    //31:47.833             obj[0] = North America
    //31:47.833             obj[1] = Japan
    //31:47.833             obj[2] = Taiwan
    //31:47.833 obj[email] = maureen@oreilly.com
}

方式二: Constructor Function

使用時機: 

建立一個 Javascript 物件的範本, 再依實際需要, 以 new 敍述建立一個或多個物件實體 (instances). 類似 C# 的 Class; 但必須說明的是 Javascript 是以原型為基礎 (Prototype-Based) 的程式語言, 與其它以類別為基礎 (Class-Based) 的程式語言不同.

範例2.1 -- 利用 new 建立物件後, 再新增該物件的屬性


// =================================================
// 以 Constructor Function 的方式建立物件
// =================================================
function createByConstructor01() {
    /// <summary>利用 new 建立物件後, 再新增該物件的屬性</summary>
    var o = new Object();              // Create an empty object: same as {}.
    var a = new Array();                // Create an empty array: same as [].
    var d = new Date();                 // Create a Date object representing the current time
    var r = new RegExp("js");       // Create a RegExp object for pattern matching.

    var objCard = new Object();     // objCard 係以 Object 為範本, 建立一個物件
    objCard.name = "Jasper";        // 加入專屬於 objCard 的屬性
    objCard.age = 47;
    objCard.phones = ["02-2123-4567", "02-2321-7654"];
    objCard.email = "jasper@abc.com";

    document.write("<pre>");
    iterateObjectProperties(objCard);
    document.write("</pre>");

    //輸出結果:
    // 56:59.116 obj[name] = Jasper
    // 56:59.131 obj[age] = 47
    // 56:59.147 obj[phones] = 02-2123-4567,02-2321-7654
    // 56:59.159      obj[0] = 02-2123-4567
    // 56:59.175      obj[1] = 02-2321-7654
    // 56:59.189 obj[email] = jasper@abc.com
}

範例2.2 -- 利用 constructor function (類似 Class) + new 建立物件 


// =================================================
// 以 Constructor Function 的方式建立物件
// =================================================
function createByConstructor02() {
    /// <summary>利用 constructor function (類似 Class) + new 建立物件 </summary>

    // ---------------- 較單純的 (帶一個簡單的 method) -----------------------
    function Shape(color, borderThickness) {
        this.color = color;
        this.borderThickness = borderThickness;
        this.describe = function () {
            document.write("<pre>");
            dispLog("I am a " + this.color + " shape, with a border that is " + this.borderThickness + " thick");
            document.write("</pre>");
        }
    }
    var shape = new Shape("red", 2.0);
    shape.describe();

    document.write("<pre>");
    iterateObjectProperties(shape);
    document.write("</pre>");

    // ---------------- 較複雜的 -----------------------
    function Phones(office, mobiles) {
        ///<summary>定義 Phones 的建構函式</summary>
        this.office = office;
        this.mobiles = mobiles;
    }

    function Contactor(name, phones, email) {
        ///<summary>定義 ContactInfo 的建構函式</summary>
        this.name = name;
        this.phones = phones;
        this.email = email;
    }

    //建立物件, 並同時給值
    var contactor01 = new Contactor("Jasper", new Phones("02-2123-4567", ["0912-345-678", "0912-876-543"]), "jasper@abc.com");
    //建立物件, 後續才給值
    var contactor02 = new Contactor();
    contactor02.name = "Judy";
    contactor02.phones = new Phones();
    contactor02.phones.office = "02-2321-7654";
    contactor02.phones.mobiles = null;
    contactor02.email = "judy@abc.com";

    document.write("<pre>");
    iterateObjectProperties(contactor01);
    document.write("</pre>");

    document.write("<pre>");
    iterateObjectProperties(contactor02);
    document.write("</pre>");

    //輸出結果:
    //39:42.343 I am a red shape, with a border that is 2 thick
    //
    //39:42.362 obj[color] = red
    //39:42.372 obj[borderThickness] = 2
    //39:42.382 obj[describe] = function () {
    //    document.write("");
    //    dispLog("I am a " + this.color + " shape, with a border that is " + this.borderThickness + " thick");
    //    document.write("");
    //
    //04:49.864 obj[name] = Jasper
    //04:49.875 obj[phones] = [object Object]
    //04:49.886     obj[office] = 02-2123-4567
    //04:49.895     obj[mobiles] = 0912-345-678,0912-876-543
    //04:49.907             obj[0] = 0912-345-678
    //04:49.933             obj[1] = 0912-876-543
    //04:49.942 obj[email] = jasper@abc.com
    //
    //04:49.959 obj[name] = Judy
    //04:49.970 obj[phones] = [object Object]
    //04:49.979     obj[office] = 02-2321-7654
    //04:49.988     obj[mobiles] = null
    //04:50.001 obj[email] = judy@abc.com

}


以第2個範例而言, 經由 Visual Studio 2013 進行逐步除錯 (圖 2.2.1 及 圖 2.2.2), 可以發現有 __proto__ 這個屬性, 指向其上層的物件. 例如: shape --> Shape --> Object --> Null (圖 2.2.3). 但這個只是初步的結論,
實際上, __proto__ 指向的是一個 prototype 物件; 更準確的說, 應該是建構函式的 prototype 屬性.

圖 2.2.1: Javascript __proto__ 架構示意圖

圖 2.2.2: Javascript __proto__ 架構示意圖

圖 2.2.3: Javascript __proto__ 架構示意圖


補充觀念: prototype

參考連結: Understanding JavaScript Object Creation Patterns
在進行下一個物件建立方式之前, 先針對 prototype 作一下說明,

就方式二的最末段說明:  "實際上, __proto__ 指向的是一個 prototype 物件; 更準確的說, 應該是建構函式的 prototype 屬性", 寫一下程式作驗證

範例3.1 -- 用以說明 prototype 的觀念


// =================================================
// 用以說明 prototype 的觀念
// =================================================
function testPrototype01() {
    /// <summary>用以說明 prototype 的觀念 </summary>
    function Shape(color, borderThickness) {
        this.color = color;
        this.borderThickness = borderThickness;
        this.describe = function () {
            document.write("<pre>");
            dispLog("I am a " + this.color + " shape, with a border that is " + this.borderThickness + " thick");
            document.write("</pre>");
        }
    }
    var shape01 = new Shape("red", 2.0);
    var shape02 = new Shape("blue", 3.0);
    document.write("<pre>");
    iterateObjectProperties(shape01, true);
    iterateObjectProperties(shape02, true);
    document.write("</pre>");
    //
    document.write("<pre>");
    //以下的 shape01 及 shape02 的 __proto__ 都指向 Shape.prototype
    dispLog("shape01.__proto__ === Shape.prototype ? " + (shape01.__proto__ === Shape.prototype));
    dispLog("shape02.__proto__ === Shape.prototype ? " + (shape02.__proto__ === Shape.prototype));
    dispLog("shape01.__proto__ === shape02.prototype ? " + (shape01.__proto__ === shape02.__proto__));
    dispLog("");
    //以下的 Shape.prototype.__proto__ === Object.prototype, 且 Shape.__proto__ === Function.prototype
    //說明: Shape 其實是一個建構函式, 所以其 __proto__ 會指向 Function.prototype; 
    //         而 Function.prototype.__proto__ 才會指向 Object.prototype
    dispLog("Shape.prototype.__proto__ === Object.prototype ? " + (Shape.prototype.__proto__ === Object.prototype));
    dispLog("Shape.__proto__ === Function.prototype ? " + (Shape.__proto__ === Function.prototype));
    dispLog("Function.prototype.__proto__ === Object.prototype? " + (Function.prototype.__proto__ === Object.prototype));
    //
    document.write("</pre>");

    //輸出結果:
    //00:08.843 obj[color] = red
    //00:08.860 obj[borderThickness] = 2
    //00:08.874 obj[describe] = function () {
    //    document.write("<pre>");
    //    dispLog("I am a " + this.color + " shape, with a border that is " + this.borderThickness + " thick");
    //    document.write("</pre>");
    //}
    //00:08.888 obj[color] = blue
    //00:08.904 obj[borderThickness] = 3
    //00:08.923 obj[describe] = function () {
    //    document.write("<pre>");
    //    dispLog("I am a " + this.color + " shape, with a border that is " + this.borderThickness + " thick");
    //    document.write("</pre>");
    //}
    //
    //00:08.950 shape01.__proto__ === Shape.prototype ? true
    //00:08.966 shape02.__proto__ === Shape.prototype ? true
    //00:08.978 shape01.__proto__ === shape02.prototype ? true
    //00:08.994 
    //00:09.007 Shape.prototype.__proto__ === Object.prototype ? true
    //00:09.021 Shape.__proto__ === Function.prototype ? true
    //00:09.038 Function.prototype.__proto__ === Object.prototype? true

}
圖 3.1.1 Javascript: prototype 示意圖

但 prototype 到底有什麼好處呢? 其實, 它最主要在於可擴充現行建構函式 (or 類別) 的功能, 茲以範例3.2 作說明. 這個範例看來跟範例3.1 很像, 但事實上是有區別的, 將 describe() 方法由建構函式, 移到 Shape.prototype

範例3.2 -- 用以說明 prototype 的觀念 (利用 prototype, 加入擴充方法) 


// =================================================
// 用以說明 prototype 的觀念
// =================================================
function testPrototype02() {
    /// <summary>用以說明 prototype 的觀念 (利用 prototype, 加入擴充方法) </summary>
    function Shape(color, borderThickness) {
        this.color = color;
        this.borderThickness = borderThickness;
    }

    //在 Shape.prototye 加入一個擴充方法, 這個方法可以由多個 instances 呼叫
    Shape.prototype.describe = function () {
        document.write("<pre>");
        dispLog("I am a " + this.color + " shape, with a border that is " + this.borderThickness + " thick");
        document.write("</pre>");
    }

    var shape01 = new Shape("red", 2.0);
    var shape02 = new Shape("blue", 3.0);
    document.write("<pre>");
    iterateObjectProperties(shape01, true);
    iterateObjectProperties(shape02, true);
    document.write("</pre>");
    //每一個 instances 都可以呼叫經由 建構函式的 prototype 作擴充的方法
    shape01.describe();
    shape02.describe();

    //輸出結果:
    //00:09.139 obj[color] = red
    //00:09.152 obj[borderThickness] = 2
    //00:09.171 obj[color] = blue
    //00:09.189 obj[borderThickness] = 3
    //
    //04:21.626 I am a red shape, with a border that is 2 thick
    //04:21.657 I am a blue shape, with a border that is 3 thick
}

圖 3.2.1 Javascript: prototype 示意圖


如上的說明, 我們也可以為 String 加上一些原來沒有的功能, 例如: reverse() ...

如果仔細查看, 可以發現在各個 prototype 物件上, 其實還有一個 constructor 的屬性, 指向建構函式.

範例3.3 -- 用以說明 prototype 的觀念 (constructor 屬性) 


// =================================================
// 用以說明 prototype 的觀念
// =================================================
function testPrototype03() {
    /// <summary>用以說明 prototype 的觀念 (利用 prototype, 加入擴充方法) </summary>
    function Shape(color, borderThickness) {
        this.color = color;
        this.borderThickness = borderThickness;
    }

    //在 Shape.prototye 加入一個擴充方法, 這個方法可以由多個 instances 呼叫
    Shape.prototype.describe = function () {
        document.write("<pre>");
        dispLog("I am a " + this.color + " shape, with a border that is " + this.borderThickness + " thick");
        document.write("</pre>");
    }

    var shape01 = new Shape("red", 2.0);
    var shape02 = new Shape("blue", 3.0);
    document.write("<pre>");
    dispLog("Shape.prototype.constructor === Shape ? " + (Shape.prototype.constructor === Shape));
    dispLog("Shape.prototype.constructor = " + Shape.prototype.constructor);
    dispLog("Function.prototype.constructor = " + Function.prototype.constructor);
    dispLog("Object.prototype.constructor = " + Object.prototype.constructor);
    document.write("</pre>");

    //輸出結果:
    //21:38.524 Shape.prototype.constructor === Shape ? true
    //21:38.540 Shape.prototype.constructor = function Shape(color, borderThickness) {
    //    this.color = color;
    //    this.borderThickness = borderThickness;
    //}
    //
    //21:38.556 Function.prototype.constructor = 
    //function Function() {
    //    [native code]
    //}
    //
    //21:38.592 Object.prototype.constructor = 
    //function Object() {
    //    [native code]
    //}
}

圖 3.3.1 Javascript: prototype 示意圖

方式三: Object.create() method

使用時機: 藉由某個物件的 prototype, 建立一個單純的 Javascript 物件. 此方式不需經由 Constructor Function 即可建立物件 (Object.create() is an excellent choice for creating an object without going through its constructor.) 但因為不經由 Constructor Function, 所以有些原本以為可正常運作的寫法, 但其實無法運作 ...

範例4.1 -- Object.create(), 看起來可以運作, 但實際上運作會發生預期之外的執行結果


// =================================================
// 採用 Object.create() 
// =================================================
function createByCreateFunc01() {
    /// <summary>採用 Object.create()  (錯誤的示範) </summary>
    function Car(desc) {
        this.desc = desc;
        this.color = "red";
    }
    Car.prototype = {
        getInfo: function () {
            return 'A ' + this.color + ' ' + this.desc + '.';
        }
    };
    //instantiate object using the constructor function
    var car = Object.create(Car.prototype);
    car.color = "blue";

    //以下會出現 undefined, 主因在於 Object.create() 是利用 prototype, 但不經由 constructor 作初始化
    //The description is lost. So why is that? Simple; the create() method only uses the prototype and 
    //not the constructor. Hence, Object.create() is an excellent choice for creating an object without 
    //going through its constructor.
    document.write("<pre>");
    dispLog(car.getInfo()); //displays 'A blue undefined.' ??!
    document.write("</pre>");

    //輸出結果:
    //51:34.411 A blue undefined.
}


範例4.2 -- 修正上述錯誤的範例


// =================================================
// 採用 Object.create() 
// =================================================
function createByCreateFunc02() {
    /// <summary>採用 Object.create()  (修正因為沒有經過 constructor 作初始化, 造成 undefined 的問題) </summary>
    //var Car = Object.create(null); //this is an empty object, like {}  ==> Actually, this is not true:
    //var Car = Object.create(null); //this is an empty object - no toString() etc.
    //var Car = Object.create(Object.prototype); // this is like {}

    //var Car = Object.create(null); //this is an empty object - no toString() etc.
    var Car = Object.create(Object.prototype); //this is an object - include toString() ... methods
    Car.prototype = {
        getInfo: function () {
            return 'A ' + this.color + ' ' + this.desc + '.';
        }
    };

    var car = Object.create(Car.prototype, {
        //value properties
        color: { writable: true, configurable: true, value: 'red' },
        //concrete desc value
        rawDesc: { writable: false, configurable: true, value: 'Porsche boxter' },
        // data properties (assigned using getters and setters)
        desc: {
            configurable: true,
            get: function () {
                return this.rawDesc.toUpperCase();
            },
            set: function (value) {
                this.rawDesc = value.toLowerCase();
            }
        }
    });
    car.color = 'blue';
    document.write("<pre>");
    dispLog(car.getInfo()); //displays 'A blue PORSCHE BOXTER.'
    document.write("</pre>");

    //輸出結果:
    //20:40.488 A blue PORSCHE BOXTER.
}



範例4.3 -- 一個來自MDN的範例

Mozilla Developer Network : Object.create()
// =================================================
// 採用 Object.create() 
// =================================================
function createByCreateFunc03() {
    /// <summary>採用 Object.create()  (Mozila 的範例) </summary>
    // Shape - superclass
    function Shape() {
        this.x = 0;
        this.y = 0;
    }

    // superclass method
    Shape.prototype.move = function (x, y) {
        this.x += x;
        this.y += y;
        dispLog('Shape moved to x:' + x + ' y:' + y);
        //console.info('Shape moved.');
    };

    // Rectangle - subclass
    function Rectangle() {
        Shape.call(this);       // call super constructor.
    }

    // subclass extends superclass
    Rectangle.prototype = Object.create(Shape.prototype);
    Rectangle.prototype.constructor = Rectangle;

    var rect = new Rectangle();

    document.write("<pre>");
    dispLog('Is rect an instance of Rectangle? ' + (rect instanceof Rectangle)); // true
    dispLog('Is rect an instance of Shape? ' + (rect instanceof Shape)); // true
    //console.log('Is rect an instance of Rectangle? ' + (rect instanceof Rectangle)); // true
    //console.log('Is rect an instance of Shape? ' + (rect instanceof Shape)); // true 
    rect.move(1, 1); // Outputs, 'Shape moved to x:1 y:1'
    document.write("</pre>");

    //輸出結果:
    //30:41.466 Is rect an instance of Rectangle? true
    //30:41.484 Is rect an instance of Shape? true
    //30:41.499 Shape moved to x:1 y:1
}


範例 4.3 有點複雜, 筆者還不是很能了解, 但推測是利用 Object.create() 建構類別之間的繼承關係 ...


總結

採用 Object.create() 看起來似乎比前面2種方式要來得複雜 (如果要建立第2個 instance, 就要寫那麼一長串的程式 ...), 實在不是很方便; 但應該有其真正適用的情境才對, 只是筆者功力有限, 有空再繼續往下研究.

參考文件




沒有留言:

張貼留言