AngularJS 1.x 學習心得整理(二)

層次分明,學習就不會感到迷惑

最近公司請我幫忙因為新Web專案要使用的AngularJS Framework而需要接受教育訓練的開發人員規劃SPA相關課程,順便我可以整理一下我這個過來人的學習經驗來分享給新進,於是開始收集這方面資料。

啄木鳥認為最重要的課題是該如何以深入淺出的方式,讓每個人都能具體的,理解從JQuery的開發經驗到AngularJS開發的差異處。

既然都是based on Javascript的擴展應用,一定存在異中求同的方法,藉由基本的Javascript所累積經驗,快速建立可以理解AngularJS Framework設計哲學的基礎。

將新知識疊加到現有知識的秘訣就是要有正確的學習計畫,每一階段主題都需要用心規劃。只要層次分明就不會感到迷惑,對Angular JS來說這樣的學習曲線是最好的,所以我開始設計課程。

JSON(JavaScript Object Notation)

首先,必須針對JSON這個物件(Object)做說明,因為它太重要了!

很多JSON程式人員常常沒有把所使用的Javascrip 物件(Object) 基礎 打穩就開始玩了起來(包括啄木鳥在內),在職場走了很久技術還是沒有甚麼精進的原因就是甚麼都一知半解,雖能解燃眉之急,但非長久之計。

它的重要性有多高呢?(謎之音:有這麼高...(抬頭張望))

包含AngularJS以及較知名的ReactJS,當前所有的基於MVVM技術框架的Web開發工具中操作的Data Model/Veiw Model與系統設計都是集操作JSON技術之大成沒有之一。其他基於互聯網的電子商務的EDI媒介也幾乎都由JSON取代了XML(SAOP)成為主流交換標準。

結論是Web前後端點的資料應用標準都已是「可序列化JSON」的天下。也許有人並不認為XML已經式微,但由於JSON實在太夯了,甚至連與Javascript應用無直接關係的後端資料儲存技術也開始看到JSON活躍的身影。這很像當初XML的情況,而JSON更勝XML當時的風雲程度(MS SQL Server 2016已支援JSON格式資料操作)。

本段相關連結:
維基百科介紹JSON
JSON和XML的比較

當異質平台只是單純使用JSON格式傳遞資料時,不曉得其實JSON是 Javascript 物件導向語言的中代表一切實體物件的原型的一部分,根據規範,JSON一種是可以序列化的資料原型(OK for JSON.stringify),離線儲存時可做為跨語言平台的一種資料結構描述,類似XML(或取代XML);一種是無法序列化的物件原型(Not OK for JSON.stringify),通稱為構造函數Constructor,這兩種隨時融合又隨時可以分離的語法,互為表裡的構成了Javascript物件導向的語言特性。就我所知,除了Javascript以外沒有任何一種語言裡面有這種某宣告語法(JSON)被拔擢成跨平台應用標準的例子。

以上就是為什麼JSON叫做JavaScript Object Notation的意義,原來資訊科技這麼大的領域,大家在用的竟然是Javascript的原生物件,你不覺得Javascript好偉大嗎?!

創建一個物件它自己的屬性的方法就是設置這個物件的屬性;
var myCar = new Object();
myCar.make = "Ford";
myCar.model = "Mustang";
myCar.year = 1969;
這個可以成功讓JSON.stringify()函式序列化的物件就是一種不包含函數屬性(Function)的Data Model,也就是JSON;反之則是構造函數Constructor,它是包含函數屬性的物件,它們就是AngularJS裡的Module(Controller、Provider、Factory、Service、Directive、filter...等),使用JSON.stringify去序列化構造函數或JSON中的函數屬性)只會傳回"undefiend"或被去掉該名稱/值,雖然JSON存在這些無法序列化的屬性值,但在程式中也是合法的唷。

整個AngularJS Framework都是物件與物件鏈接的架構,它利用AOPIoC和DI等OOP方法來實踐整個Framework的設計哲學,所以不懂Javascript物件導向設計的人,自然看不懂AngularJS架構來源程式碼,想學好其他的MVVM如ReactJS、vue也都是Out!

以下是包含構造函數Module的例子:
angular.module('app')
    .service('testService', function () {
        var self = this;
        self.getString = function () {
            return 'woodpecker';
        };
    });


angular.module('app')
    .controller('testCtrl', function ($scope, testService) {
    // 第一次Angular會初始化testService方法,並把testService參考傳進來

    // 類似 new testService()

        $scope.name = testService.getString();
    });
AngularJS到處都是這樣的程式碼。 

話說到這,我們趕快來看看這一個單純是Data Model格式範例(範例沒設計得很好,但整個寫太多不想改了):
不包含構造函數的JSON(Data Model)
var jsonObj = [
 { Text: "10", Value: 10 },
 { Text: "20", Value: 20 },
 { Text: "50", Value: 50 },
 { Text: "100", Value: 100 }
];

JSON只會用到兩種可互相鏈結的結構;大括號 { 和 } 表示是物件(object),中括號 [ 和 ] 代表是陣列(array),上面JSON範例就是一個包含有四個物件的一維陣列,注意陣列中的物件(object)要用逗號 , 隔開。

再來,談到JSON物件(object)的內容格式:
要描述一個JSON物件內容要用一或多組的名稱/值(collection)來宣告,很多組的時候用逗號 , 隔開。JSON裡的object都是單指JSON物件{ },這點請注意,以免混淆到Object Class(註一)。
var jsonObj = [
 { Text: "10", Value: 10 },
 { Text: "20", Value: 20 },
 { Text: "50", Value: 50 },
 { Text: "100", Value: 100 }
];
console.log(jsonObj instanceof Object); //print true
console.log(jsonObj instanceof Array); //print true
註一:
Javascript的數據類型就是以下七(六)種:
Undefined類型;
Null類型;
Boolean類型;
String類型;
Symbol類型(此為ES6規範所新增);
Number類型;
Object類型;
引用自https://www.zhihu.com/question/24804474

上面JSON範例這一段 {Text: "10", ...}, ...的意思是四個用 {  } 括起來的資料物件,全包在一維陣列 [  ] (array)裡面,每個物件的資料結構都一樣(有兩組collection)。

值得注意的是:JSON的可以是物件(object) {  },也可以是陣列(array) [  ],這兩者都是被JSON認可的資料結構。因此,後面會示範如何才能正確的存取它。

回到格式的說明,請繼續看範例中第一個物件裡的第一組collection,名稱是"Text",這個Text名稱的值是一個字串"10";依此類推,第二組的名稱是"Value",值是一個數字 10;請記住值的內容允許放進 文字 或 數字 或 布林值 或 JSON物件 或 其他Javascript資料型別。例如:
var jsonObj_1 = [
 { Text: "10", Value: 10 },
 { Text: "20", Value: 20 },
 { Text: "50", Value: 50 },
 { Text: "100", Value: 100 }
]; //建立JSON物件(object),並初始化collection內容

var jsonObj_2 = jsonObj_1; //取得jsonObj_1物件的參考(註1)

var jsonObj_3 = [
 { Text: ["1", "2", "3"], Value: jsonObj_1 },
 { Text: ["A", "B", "C"], Value: jsonObj_2 }
]; //建立JSON物件(object),並初始化內容包含JSON資料的collection

註1:var jsonObj_2 = jsonObj_1; 語法不是複製(clone)jsonObj_1物件內容給jsonObj_2這個物件,後面會對"物件參考"這句話的意思作完整的講解。

Javascript 讀取 JSON

現在來一段Javascript操作JSON物件的程式碼:
var JsonObj = [
 { Text: "10", Value: 10 },
 { Text: "20", Value: 20 },
 { Text: "50", Value: 50 },
 { Text: "100", Value: 100 }
];

var txt = JsonObj[0]["Text"]; //txt的值為 "10"
var val = JsonObj[0]["Value"]; //val的值為 10

txt = JsonObj[0].Text; //txt的值為 "10"
val = JsonObj[0].Value; //val的值為 10

var SubObj = JsonObj[0]; //將SubObj變數設為JsonObj陣列裡的第一個物件{ Text: "10", Value: 10 }的參考
console.log(JSON.stringify(SubObj)); //"{\"Text\":\"10\",\"Value\":10}"
可以使用迴圈取出JsonObj陣列內容:
for(var i = 0; i < JsonObj.length; i++){
 console.log(JsonObj[i].Text + "/" + JsonObj[i].Value);
}
若jsonObj是物件(object)則如此做:
var JsonObj2 = { Text : "10", Value : 10};
for(var _index in JsonObj2 ){
 console.log(_index + " = " + JsonObj2[_index]); // _index 為JsonObj2物件的屬性名稱(proptyname):"Text", "Value"
} //print Text = 10, print Value = 10

var allNames = Object.keys(JsonObj2); //Object.keys(JsonObj2)取得名稱集合
for(var i in allNames){
 console.log(i + " = " + allNames[i]);
} //print "0 = Text", "1 = Value"
JS Bin 測試程式碼

Javascript 新增、修改、刪除 JSON 物件

再來示範JSON的CRUD,前面示範了用直接初始化內容的方式建立JSON物件。所以現在我們要示範如何宣告一個空的JSON物件,再對這個JSON物件作出新增/修改/刪除的動作,這是標準的JSON操作方式,往後程式設計時會很常用到,請看程式範例:
var jobj = new Object(); // 產生一個物件

function jsonClass(){}; //宣告一個空的class
var jobj2 = new jsonClass(); //產生一個新的jsonClass物件(同new Object())

var jobj3 = {}; //宣告一個空的物件(同new Object())

jobj.text = "1";
jobj["obj2"] = jobj2;
jobj.job3 = jobj3;

jobj2.value = 2;
jobj3.text = "3";

console.log(jobj.text); //print 1
console.log(jobj.obj2.value); //print 2
console.log(jobj.job3.text); //print 3
蠻簡單的,是不是? 不過這裡要釐清一個重要觀念,當我們對物件使用 = (等號)操作子必須要小心,因為當目標是物件時,左值只會得到右值的參照指標。這樣不會產生另一份拷貝物件,此時左值是個參考變數,所有參考該物件的變數都會傳回那一份原始物件。

所以,原始物件任何改變會立即造成其他參考變數的運算結果改變。這與其它基本資料型別使用 = (等號)操作子的結果是截然不同的。請參考以下程式運算結果:
//基本資料型別使用 = 等號的結果測試
var x = 5;
var y = x;
y = 6;

console.log(x); //print 5,不會被y影響
console.log(y); //print 6


//物件使用 = 等號的結果測試
var obj = {val:5};
var obj2 = obj; //將obj2參考指向obj
obj2.val = 5.5

console.log(obj.val); //print 5.5 被有著相同參考的obj2改變成5.5而不是5
console.log(obj2.val); //print 5.5
因此,物件使用 = 等號操作的時候,我們一定要瞭解它的意義,才不會看不懂某些複雜的程式邏輯,或是寫出不如預期的地雷程式。

來看看更詳細的測試結果:
function jsonClass2(q){
  console.log(q);
} //宣告一個function

jsonClass2(33); //print 33

var jobj4 = jsonClass2; //將jobj4指向jsonClass2 function,因此jobj4的值是個靜態函式指標參考(function pointer reference)

console.log(jobj4); //print "function jsonClass2(q){console.log(q);}"

var jobj = new Object(); // 產生一個物件

jobj.job4 = jobj4; // 動態新增collection名稱job4,同時將job4的值指向jobj4(等於指向jsonClass2 function)

jobj4.text = 4;//對jobj4這個函式竟然可以動態新增collection名稱text並給4這個值

console.log(jobj4); //print "function jsonClass2(q){console.log(q);}",再看一次內容並沒有剛剛text這個成員出現,但 jobj4.text 卻有作用?,text是綁到jobj4的哪裡去呢? 真是個謎啊...。

jobj.job4.value = 4.5; // 再從jobj.job4掛一組collection,value: 4.5

console.log(jobj.job4.text); //print 4

console.log(jobj4.value); //print 4.5,經過jobj.job4.value = 4.5;證明和jobj4.value物件參考都是相同的

jobj.job4(99); //函式還是呼叫得到,這應該是Javascript編譯器內部實現了多載與多形,好厲害!

jobj["job4"](100); //print 100,再證明函式指標也可以是collection的值沒錯!


var exe = jobj.job4; //既然是函式指標當然可以這樣用
exe(120); //print 120 // 果然沒錯
console.log(exe.text) //print 4,jsonClass2明明是函式卻有JSON物件的特性,傑克這實在太神奇了!
注意到JSON可以新增新collection的特性嗎?原本jobj4 = jsonClass2,只有參照到jsonClass2這個靜態函式指標(function pointer),但當執行jobj4.text = 4 這個指令時,jobj4很神奇的可以在參考上新增collection(text:4)。

這表示呼叫jobj4既能指向jsonClass2函式又同時能指向{text:4}這個物件。

看倌們請繼續看下去,真相如下:
//...前略,承上
console.log(typeof exe); //print function,exe 真的只是個函式...
console.log(typeof jobj); //print object,jobj 是JSON物件沒錯


//JSON的數據驗證,使用JSON.stringify
console.log(JSON.stringify(exe)); //print undefined,函式當然無法JSON序列化
console.log(JSON.stringify(jobj)); //print "{}",JSON序列化沒問題,但jobj.job4被去掉,因為構造函數不在JSON格式規範中

//Javascript的數據驗證,使用instanceof
console.log(jobj.job4 instanceof Object); //print true
console.log(jobj.job4 instanceof Function); //print true

console.log(jobj instanceof Object); //print true
console.log(jobj instanceof Function); //print false
JS Bin 程式碼測試

我們知道jsonClass2等同於exe和jobj4也等同於jobj.job4,根據最後的幾行程式結果,顯示 exe 既是函式也是物件,但不是JSON,因為exe 無法被JSON.stringify()成功序列化。

不過,之前我們對 exe 加上的text也不在函式中,但卻能像操作JSON的collection一樣存取text名稱裡的值。 啄木鳥合理懷疑Javascript編譯器會自動根據程式碼判斷我們想呼叫的是函式還是JSON物件來選擇繼承的原型操作子解決這問題(继承与原型链),這真的讓以前習慣寫強型別程式的人有點難以適應啊。建議大家盡量別用這類寫法操作JSON,不然除錯時可能會哭哭,喜歡挑戰自己的極限的高手倒是可以玩玩。這裡對JSON.stringify()有較詳細的解說:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

再探 function 之於 JSON 的意義

經過上面的範例展示,我們已經知道JSON之所以稱為Javascript原生物件(JavaScript Object Notation)是將所有資料結構都涵蓋進去的完整架構,它是包含了Class和function等動態/靜態指標的對應容器。

因此新手要注意的是,var與this所代表的意義。這兩者的差別在於:已經使用var語句聲明的變數,在實例化過程將使用全域作為變數對象,並將該屬性定義為不可刪除,而使用this語法的變數才能成為JSON物件的內部屬性/成員(同collection)。請看下方例子:
function jsonClass2(q){
 var inside_propty1 = "01"; //全域靜態變數
 this.inside_propty2 = "02"; //可隨個體實例化之後成為內部的屬性/成員(collection)
 console.log(q);
};

var jobj5 = jsonClass2; //指定delegate jsonClass2 靜態函式
var jobj6 = new jsonClass2(); //使用new關鍵字產生實例,這樣 jsonClass2 就是個類別(Class)了

console.log("inside_propty1 = " + jobj5.inside_propty1); //print inside_propty1 = undefined
console.log("inside_propty2 = " + jobj5.inside_propty2); //print inside_propty2 = undefined

console.log("inside_propty1 = " + jobj6.inside_propty1); //print inside_propty1 = undefined
console.log("inside_propty2 = " + jobj6.inside_propty2); //print inside_propty2 = "02" 
jobj5除了函式自身指標外甚麼都抓不到,不會有實例產生自然沒辦法存取其成員。而jobj6宣告this.inside_propty2的變數可以成為函數成員。

參考來源:MDN JavaScript 物件導向介紹What is the difference between using var and this, in Javascript?

唯有使用new這個方法產生constructor實例,透過記憶體的個別生成,能讓內部使用this關鍵字宣告的變數成員在轉換成JSON操作時能被collection對應到!

這個技術認知對通往前端程式設計師之路而言是很重要的,幾乎所有的前端開發框架的程式寫法都倚賴物件導向來設計各種自動化模組,要精通這些模組可不是一件容易的工作,要打穩基礎,正確的Javascript程式觀念不可不知,這也是啄木鳥為甚麼在介紹AngularJS之前以這麼多文字來解析JSON的原因。





相關文章:

AngularJS 1.x 學習心得整理(一)

AngularJS 1.x 學習心得整理(三)

留言

  1. 這篇寫得有點長,但是寫得欲罷不能(新手就是有衝勁,哈),也從中獲得更清晰的概念,這些驗證都是自己的設計,若有謬誤之處,請不吝賜教。

    回覆刪除

張貼留言

這個網誌中的熱門文章

揭開 C#.NET 官方原始碼 System.Func、System.Action、System.Delegate 及 delegate 關鍵字背後的技術面紗

從Lambda語法來探討.NET LINQ的技術底蘊到底在哪裡?

C# 物件屬性的建構賦值與初始化 - C# constructor and object initializer