緣起
由於網頁的前端技術越來越熱門, 很多 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 }
如上的說明, 我們也可以為 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的範例
// ================================================= // 採用 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() 建構類別之間的繼承關係 ...
沒有留言:
張貼留言