前端工程研究:關於JavaScript 的物件藍圖建立方法

文章推薦指數: 80 %
投票人數:10人

不用藍圖的物件建立方法. 很多人都說寫JavaScript 的爽度很高,因為怎麼寫都可以,就以一個簡單的物件為例,根本連類別都不用宣告,直接用以下語法就 ... ←如何成功刪除AmazonS3Glacier的Vaults備份資料 如何設定gitignore命令並自動下載所需的.gitignore範本→ 我們都知道JavaScript物件建立的過程,大多都不需要事先設計「藍圖」就可以建立「物件」,不像C#或Java等強型別語言,需要先設計「藍圖」(也就是類別),才能產生物件。

在ES2015出來之前,並沒有class語法,而是使採用以原型為基礎的物件導向設計模式(Prototype-basedOO)。

本篇文章將介紹幾種在JavaScript裡面建立物件藍圖的方式。

不用藍圖的物件建立方法 很多人都說寫JavaScript的爽度很高,因為怎麼寫都可以,就以一個簡單的物件為例,根本連類別都不用宣告,直接用以下語法就可以建立物件: vara={}; 建立物件後,可以隨意新增或刪除屬性進去: vara={}; a.name='Will';//擴增name屬性 deletea.name;//刪除name屬性 如果要建立一個含有name屬性的物件,可以這樣寫: vara={ name:'Will' }; 也可以這樣寫,因為屬性名稱預設就是字串型別: vara={ 'name':'Will' }; 這樣寫也可以,讓變數值當成新物件的屬性名稱: varpropName='name'; vara={ [propName]:'Will' }; 如果要放Symbol物件進去,也可以這樣寫: vara={ name:'Will', [Symbol.iterator]:function*(){ for(letiinthis){ yieldthis[i]; } } }; 總之,你可以完全不用對物件進行規劃,就開始隨心所欲的建立物件,因此寫起來的爽度很高!相對的,在複雜的應用程式架構下,也比較容易失控! 定義物件的藍圖(1)-使用建構式函數 在JavaScript裡面,函數(function)其實就是建構式(constructor),因此我們會透過定一個函數,來當成一個物件的建構式,也就是物件的藍圖。

如下範例就是一個建構式,建構式名稱為Lesson,但看起來就像是一個函數: functionLesson(name){ this.name=name; this.sayHello=()=>{ return`Hello${this.name}`; } } 請注意this.sayHello必須使用箭頭函數(ArrowFunction)才不會出錯! 這個函數有兩點不太相同: 不需要return任何物件 會使用this來代表未來即將建立的物件實體(objectinstance) 當你想建立一個自訂型別為Lesson的物件時,就可以使用以下語法來建立: vara=newLesson('JavaScript開發實戰:核心概念篇'); a.sayHello(); 透過這種方式建立起的物件,由於事先透過建構式函數規劃與設計過,因此建立的物件,更能確保物件的一致性,也更加簡化物件建立的過程,不但可以讓程式碼更抽象,獲得更好的封裝,也能增加程式的可維護性!👍 定義物件的藍圖(2)-使用class類別 從ES2015開始,你開始可以透過class來定義類別,用來建立物件藍圖,如下範例: classLesson{ name;//宣告屬性(Property) constructor(name){//建構式函數 this.name=name; } sayHello(){//宣告方法(Method) return`Hello${this.name}`; } } 當要建立物件時,就跟以建構式函數建立物件的方式相同: vara=newLesson('JavaScript開發實戰:核心概念篇'); a.sayHello(); 這樣的寫法,相較於C#或Java這種以類別為基礎的程式語言來說,其實上手會較快,因為語法相近。

透過這種方式建立的物件,你會發現跟建構式函數建立的物件,是相當接近的。

你可以說ES2015推出的class語法,其實只是早期寫法的語法糖而已,實際上物件的特性並沒有什麼差異,只有些微的變化!⭐ 如何實現物件的繼承 在物件導向程式設計領域中有許多基本原則,例如封裝、繼承、多形等等。

在JavaScript裡,繼承是一大重點! 由於從ES2015開始,出現了兩種定義物件藍圖的方式,所以我用兩種不同的語法,來表達相同的物件繼承關係,藉此設計出一份更好的物件藍圖,各位可以比較一下差異,並選擇你喜歡的語法來寫即可: 使用prototype實現物件繼承 如下範例是一個簡單的物件繼承關係,建構式函數名稱為Lesson,但我們透過建構式函數提供的prototype屬性來建立物件實體(objectinstance)的上層物件(parentobject): functionLesson(name){ this.name=name; } Lesson.prototype.sayHello=function(){ return`Hello${this.name}`; } vara=newLesson('JavaScript開發實戰:核心概念篇'); a.sayHello(); 上面這段程式碼比較特別的地方有以下幾點: 這個建構式函數只需要撰寫初始化物件的程式碼 物件需要共用的部分全部移到Lesson.prototype原型物件中,而不是保存在物件實體中 使用class實現物件繼承 我直接拿上面這個prorotype的例子,直接改寫成class的版本: classLesson{ name; constructor(name){ this.name=name; } sayHello(){ return`Hello${this.name}`; } } vara=newLesson('JavaScript開發實戰:核心概念篇'); a.sayHello(); 你可以從上述兩個範例的執行結果來看,物件的結構幾乎是完全相同的!而且使用class語法糖來定義物件繼承關係,會相對簡單許多!👍 這裡比較特別的地方在於,透過class所建立的物件,只有類別中的Methods(方法)才會放進物件的上層物件中(a.__proto__),也就是Lesson.prototype物件中。

建立一個沒有物件繼承的『純物件』 一般來說,你建立的任何一個物件,無論有幾層物件繼承,其頂層物件皆為Object.prototype,那如果我真的想建立一個完全沒有上層物件的純物件,那該怎麼做呢?你可以參考以下程式寫法: vara=Object.create(null); 我覺得這樣的物件感覺真的蠻酷的,非常的乾淨!😃 如何實現物件的多層繼承 如果要在JavaScript實現多層的繼承,那麼程式碼就會再複雜一些! 使用prototype實現多層物件繼承 //定義上層建構式 functionPerson(age,weight){ this.age=age; this.weight=weight; } //定義下層建構式 functionEmployee(age,weight,salary){ this.age=age; this.weight=weight; this.salary=salary; } //將Employee的上層物件改為Person的物件實體 Employee.prototype=newPerson(0,0); //建立Employee物件實體 vare=newEmployee(23,70,40000); 此時,變數e所指向的物件,其上層物件就是Employee.prototype,而Employee.prototype的上層物件就是Person.prototype,而Person.prototype的上層物件是誰呢?當你沒有特別定義的時候,那就是Object.prototype物件,這個物件幾乎是所有物件的最頂層物件! Object.prototype的上層物件為null 使用class實現多層物件繼承 透過JavaScript的class所建立出來的物件,跟用傳統prototype建立出來的物件,其繼承關係可能跟你想像的有點不太一樣,尤其是拿C#與Java的類別特性來相比,在觀念上的差異其實是不太相同的! 我直接拿上面這個例子,直接改寫成class的版本: //定義上層類別 classPerson{ age; weight; constructor(age,weight){ this.age=age; this.weight=weight; } } //定義下層類別 classEmployeeextendsPerson{ salary; constructor(age,weight,salary){ super(age,weight); this.age=age; this.weight=weight; this.salary=salary; } } //建立Employee物件實體 vare=newEmployee(23,70,40000); 從上圖示來看,你應該不難發現,如果你用C#與Java的類別特性來想,程式碼肯定不會如你預期的來執行! 上層類別的「屬性」其實並非「上層物件」的屬性,而是在建構式執行的時候,都寫入到「物件實體」中! 雖然Employee與Person是繼承關係,但並非「物件」的繼承,而僅是「類別」的繼承而已! 如果我在Person類別加入一個getAge方法,你會發現getAge確實會繼承下來,但是最終的物件結構不太相同: //定義上層類別 classPerson{ age; weight; constructor(age,weight){ this.age=age; this.weight=weight; } getAge(){ returnthis.age; } } //定義下層類別 classEmployeeextendsPerson{ salary; constructor(age,weight,salary){ super(age,weight); this.age=age; this.weight=weight; this.salary=salary; } } //建立Employee物件實體 vare=newEmployee(23,70,40000); e.getAge(); 從上圖可以看出,所有建構式寫入的屬性,都會出現在第一層物件實體中。

但是上層類別的方法,會放在e.__proto__.__proto__物件中。

設定一般物件為類別的上層物件 由於class其實只是prototype的語法糖,因此如果你想用class去繼承另一個建構式,也完全是可行的!範例如下: functionAnimal(name){ this.name=name; } Animal.prototype.speak=function(){ console.log(this.name+'makesanoise.'); } classDogextendsAnimal{ speak(){ console.log(this.name+'barks.'); } } vard=newDog('Mitzie'); d.speak();//Mitziebarks. 如果你想設定class類別的上層物件,可以透過Object.setPrototypeOf()來達成,例如: varAnimal={ speak(){ console.log(this.name+'makesanoise.'); } }; classDog{ constructor(name){ this.name=name; } } //直接將Dog.prototype設定為Animal物件 Object.setPrototypeOf(Dog.prototype,Animal); vard=newDog('Mitzie'); d.speak();//Mitziemakesanoise. 定義物件屬性的特性-使用Object.defineProperty 在JavaScript裡,還有一個進階的物件定義方法,就是去定義每個物件中屬性的特性(Attributesinproperty),你可以使用Object.defineProperty()去定義特定物件下某個屬性的特性,或透過Object.defineProperties()一次定義多個屬性的特性。

我們通常會先準備好一個物件,然後使用Object.defineProperty去設定特定幾個屬性的特性,如下程式範例: //先建立一個空物件 varobj={}; //然後定義該物件下的name屬性(Property),並設定descriptors(敘述內容) Object.defineProperty(obj,'name',{ enumerable:false, configurable:false, writable:false, value:undefined }); 這裡的descriptors其實就是一個物件! 當我們在定義屬性的時候,其描述器(descriptors)有兩種類型: 資料描述器(datadescriptor):用來描述特定屬性有哪些特性(共4種) configurable 用來設定該屬性『是否可刪除』以及『是否允許再次變更資料描述器』 enumerable 用來設定該屬性『是否可枚舉』(透過for-in或Object.keys()取得物件中所有屬性) value 用來設定該屬性的預設值 當你設定value的時候,就不能設定訪問描述器(accessordescriptor),這兩者是衝突的! writable 用來設定該屬性的值『是否可被修改』 當你設定writable的時候,就不能設定訪問描述器(accessordescriptor),這兩者是衝突的! 存取描述器(accessordescriptor):用來描述存取屬性的get/set自訂邏輯 以下是ES2015的範例: classLesson{ constructor(name){ this.name=name; } } Object.defineProperty(Lesson.prototype,'message',{ get(){return`Hello${this.name}`;}, enumerable:false,//無法枚舉的屬性 configurable:false//無法被刪除的屬性 }); 以下是ES5的範例: functionLesson(name){ this.name=name; } Object.defineProperty(Lesson.prototype,'message',{ get(){return'Hello'+this.name;}, enumerable:false,//無法枚舉的屬性 configurable:false//無法被刪除的屬性 }); 凍結與密封物件結構 預設所有的JavaScript物件,所有屬性都是「可變的」(Mutable)。

但你可以用Object.freeze()將一個物件凍結,讓該物件從此之後不能再新增或移除任何屬性,甚至不能修改屬性值!換句話說,該物件會被轉變成一種「不可變的」狀態(Immutable)! vara={ name:'Will' }; Object.freeze(a);//凍結物件 a.name='John';//無法變更屬性值(不會報錯) deletea.name;//也無法刪除屬性(不會報錯) a.type='pp';//也無法新增屬性(不會報錯) 被凍結的物件,基本上就再也無法變更,但你可以透過Object.assign()快速重建一個新的物件。

vara={ name:'Will' }; Object.freeze(a);//凍結物件 varb=Object.assign({},a);//建立一個{}新物件,並將a物件的內容全部複製過去 varc=Object.assign({},a,{type:'pp'});//重建物件並加入新屬性 JavaScript還有一個可以密封物件的API叫做Object.seal(),它跟Object.freeze()不一樣的地方,就是「密封物件」是可以變更屬性值的! vara={ name:'Will' }; Object.seal(a);//密封物件 a.name='John';//可以變更屬性值! deletea.name;//也無法刪除屬性(不會報錯) a.type='pp';//也無法新增屬性(不會報錯) 結論 在JavaScript中使用Object型別,相對來說比較隨心所欲一些,雖然容易上手,但隨著JS程式碼量越來越多,漸漸的就會開始產生許多問題。

因此,對常用物件進行適當的藍圖設計,不但可以藉由抽象化大幅降低程式碼的複雜度,若搭配TypeScript一起使用,對所有類別中的屬性、方法與參數,都能在編譯時期進行型別檢查,程式碼品質肯定會有所改善,開發效率也大幅提升!👍 相關連結 MDN Object-JavaScript JavaScriptdatatypesanddatastructures-JavaScript Classes-JavaScript CanIuse JavaScriptbuilt-in:Object:freeze JavaScriptbuilt-in:Object:seal JavaScriptbuilt-in:Object:defineProperty JavaScriptbuilt-in:Object:defineProperties JAVASCRIPT.INFO Classinheritance 相關文章 前端工程研究:關於JavaScript的物件藍圖建立方法 我們都知道JavaScript物件建立的過程,大多都不需要事先設計「藍圖」就可以建立「物件」,不像C#或Java等強型別語言,需要先設計「藍圖」(也就是類別),才能產生物件。

在ES201 我要成為前端工程師!給JavaScript新手的建議與學習資源整理 今年有越來越多企業開始跟我們接洽企業內訓的事,想請我幫他們培訓前端工程師,但你知道一個好的前端工程師絕對不是兩三個月可以養成的,需要多年的努力與磨練才會有點成績。

而這幾年可謂前端正夯,有為數不少的人開 前端工程研究:關於JavaScript中物件的valueOf方法 在JavaScript程式語言裡,這個valueOf()函式算是非常少用的一個內建函式,甚至於很多人連聽都沒聽過。

然而,這個valueOf()函式十分重要,我在研究之後發現,理解了val 工商服務(廣告) 每月文章 2022 九月(16) 八月(10) 七月(11) 六月(4) 五月(10) 四月(5) 三月(9) 二月(9) 一月(15) 2021 十二月(12) 十一月(5) 十月(6) 九月(13) 八月(16) 七月(13) 六月(13) 五月(7) 三月(1) 二月(1) 2020 十二月(3) 十一月(1) 十月(3) 九月(7) 八月(7) 七月(9) 六月(2) 五月(5) 三月(4) 二月(5) 一月(4) 2019 十二月(7) 十一月(2) 十月(5) 九月(1) 六月(3) 四月(4) 三月(3) 二月(7) 一月(12) 2018 十二月(3) 十一月(1) 十月(4) 九月(12) 八月(4) 六月(2) 五月(3) 四月(6) 二月(1) 一月(2) 2017 十二月(2) 十月(3) 九月(4) 七月(1) 六月(1) 五月(1) 四月(1) 二月(3) 一月(4) 2016 十二月(4) 十一月(1) 十月(1) 九月(4) 八月(3) 七月(2) 五月(1) 四月(1) 三月(3) 二月(3) 一月(2) 2015 十二月(6) 十一月(4) 十月(3) 九月(5) 八月(1) 七月(4) 六月(2) 五月(3) 四月(1) 三月(1) 二月(3) 一月(2) 2014 十二月(3) 十一月(2) 十月(3) 九月(3) 八月(1) 七月(4) 六月(2) 五月(4) 四月(5) 三月(4) 二月(1) 一月(7) 2013 十二月(1) 十一月(8) 十月(1) 九月(2) 八月(4) 七月(5) 六月(8) 五月(9) 四月(8) 三月(10) 二月(9) 一月(10) 2012 十二月(4) 十一月(6) 十月(6) 九月(4) 八月(5) 七月(10) 六月(5) 五月(6) 四月(10) 三月(12) 二月(1) 一月(6) 2011 十二月(3) 十一月(7) 十月(8) 九月(14) 八月(15) 七月(10) 六月(10) 五月(11) 四月(11) 三月(13) 二月(14) 一月(13) 2010 十二月(13) 十一月(10) 十月(16) 九月(17) 八月(12) 七月(22) 六月(15) 五月(11) 四月(14) 三月(17) 二月(17) 一月(26) 2009 十二月(16) 十一月(18) 十月(18) 九月(23) 八月(20) 七月(26) 六月(24) 五月(21) 四月(26) 三月(30) 二月(27) 一月(26) 2008 十二月(30) 十一月(31) 十月(31) 九月(29) 八月(31) 七月(31) 六月(30) 五月(30) 四月(31) 三月(31) 二月(29) 一月(33) 2007 十二月(31) 十一月(37) 十月(13) 文章分類 .Net (223) .NETCore (57) Accessibility (3) Angular (25) AngularJS (11) ASP.NET (223) ASP.NET5 (3) ASP.NETBlazor (1) ASP.NETCore (37) ASP.NETIdentity (2) ASP.NETMVC (105) ASP.NETWebAPI (13) AzureDevOps (33) C# (127) CloudComputing (4) CSS (29) DevOps (22) Docker (28) EntityFramework (23) Git (31) Golang (2) HTML5 (8) IIS (103) Java (24) JavaScript (108) Jenkins (7) Kubernetes (15) LINQ (36) Linux (112) MicroK8s (7) MicrosoftAzure (41) MySQL (15) Office (49) Office365 (28) Oracle (10) PHP (25) Scrum (1) Security (66) SQLServer (126) Subversion (35) SystemCenter (2) TFS (6) TFS2010 (10) Tips (189) UnitTesting (10) Usability (1) VBA (5) VisualBasic (5) VisualStudio (122) VisualStudio11 (2) VisualStudio2012 (10) VisualStudio2013 (4) VS2010Tips (23) Web (172) WebMatrix (8) Windows (44) Windows8 (14) WindowsAzure (18) WindowsPhone7 (14) 介紹好用工具 (225) 心得分享 (88) 多奇快訊 (9) 系統管理 (367) 前端工程研究 (20) 專案管理 (8) 團隊合作 (9) 網路管理 (21)



請為這篇文章評分?