在寫上一篇有關apply和call的博文時(閑聊JS中的apply和call),起初我還是擔心大家理解起來比較困難,因為要理解apply調用方式的前提是,至少先理解在JavaScript中函數是什麼?this到底代表什麼意思?等等。不過從大家的反饋來看,我的擔心是多餘的,諸位園友都是高手,理解這些基 ...
在寫上一篇有關apply和call的博文時(閑聊JS中的apply和call),起初我還是擔心大家理解起來比較困難,因為要理解apply調用方式的前提是,至少先理解在JavaScript中函數是什麼?this到底代表什麼意思?等等。不過從大家的反饋來看,我的擔心是多餘的,諸位園友都是高手,理解這些基礎的東東是小菜一碟。
話雖這樣講,不過今天我還是和大家聊聊JavaScript中與this相關的各種函數調用方式,可以把知識補充完整,日後回顧起來也比較方便。
【背景介紹】光明小區是一個別墅小區,家家戶戶門前屋後都有一塊小草坪。小區的物業公司為了提高業主的滿意度,為小區的業主提高了修整草坪的服務,並且還把割草機需要用的汽油列入每月的預算,確保隨時都能提供優質的服務。
接下來我們就結合具體的業務場景,來講解JavaScript中的函數調用方式。
1. 方法調用
針對光明小區為業主提供修整草坪服務的情況,我們可以用如下代碼描述:
var GuangMing = { oilVolume:5000, cutGrass:function( grass_num ){ var oilConsumption = grass_num * 20 ; //假設每平米草坪需要耗油 20 ml console.log( '正在割 ' + grass_num + ' 平米的草坪,需要用油:' + oilConsumption + ' mL。' ); console.log( '原來有油:' + this.oilVolume + ' mL' ); this.oilVolume = this.oilVolume - oilConsumption ; console.log( '用完之後,剩餘的油量為:' + this.oilVolume + ' mL' ); return ; } }; GuangMing.cutGrass( 15 ); //小區門口有15平米的草坪需要割 console.log( '>>光明小區中剩餘的油:' + GuangMing.oilVolume );
在瀏覽器的控制台運行之後,輸出結果為:
正在割 15 平米的草坪,需要用油:300 mL。 原來有油:5000 mL 用完之後,剩餘的油量為:4700 mL >>光明小區中剩餘的油:4700
這就是所謂的方法調用,與我們預期中的一樣(特別是之前搞過Java等靜態語言的高手),毫無違和感。
"oilVolume 和 cutGrass 都是GuangMing這個對象的成員,在cutGrass這個方法中調用的this.oilVolume當然是指的是和它同屬於一個對象(GuangMing)的oilVolume 。"
不過,我們上面的這種理解從本質上來講,是錯誤的。
cutGrass函數體中引用的this.oilVolume指代了GuangMing.oilVolume這個變數,不是因為在GuangMing對象的'聲明'時,cutGrass和oilVolume都是GuangMing這個對象的成員,而是因為,我們是通過GuangMing.cutGrass( 15 ); 的方式來使用這個cutGrass這個函數的。
也就是說:函數體內的this指向誰,不是看聲明時,而是看運行時。
完整的結論我們隨著後面的講解逐步來完善。
2. 函數調用
光明小區的這項服務推出之後,得到了廣大業主的好評。小區所屬的紅旗街道辦也知道了這事,街道辦的同志覺得這項服務是提升單位形象的好舉措,於是也宣稱可以提供割草的服務。不過,紅旗街道辦沒有去自己購買割草機設備,而是當有人要求服務的時候,直接通知光明小區的物業公司幫忙提供一下就可以了。
用代碼表示如下:
1 var oilVolume = 8000 ; 2 3 var GuangMing = { 4 oilVolume:5000, 5 cutGrass:function( grass_num ){ 6 var oilConsumption = grass_num * 20 ; //假設每平米草坪需要耗油 20 ml 7 console.log( '正在割 ' + grass_num + ' 平米的草坪,需要用油:' + oilConsumption + ' mL。' ); 8 console.log( '原來有油:' + this.oilVolume + ' mL' ); 9 this.oilVolume = this.oilVolume - oilConsumption ; 10 console.log( '用完之後,剩餘的油量為:' + this.oilVolume + ' mL' ); 11 return ; 12 } 13 }; 14 15 var hongQiCutgrass = GuangMing.cutGrass ; //光明小區所在的紅旗街道也說給大家提供割草服務 16 //但是既然光明小區已經有割草機,所以街道辦就沒有單獨購買割草機, 17 18 console.log( '>>光明小區中原來的油:' + GuangMing.oilVolume ); //而是直接使用光明小區的割草機。 19 hongQiCutgrass( 20 ); 20 console.log( '>>光明小區中剩餘的油:' + GuangMing.oilVolume );
運行後的輸出結果如下:
>>光明小區中原來的油:5000 正在割 20 平米的草坪,需要用油:400 mL。 原來有油:8000 mL 用完之後,剩餘的油量為:7600 mL >>光明小區中剩餘的油:5000
關鍵代碼解析如下:
第1行:為瞭解釋函數調用方式中,函數體內的this會指向全局對象(瀏覽器中就是window),增加一個全局變數oilVolume。
第15行:聲明瞭一個全局變數hongQiCutgrass指向GuangMing對象的成員函數cutGrass。
第19行:運行全局函數hongQiCutgrass。
從最後輸出的結果來看:cutGrass()中的this已經不是指代GuangMing對象了,而是指向'全局對象'。
我們再回顧一下前面得出的結論:
"函數體內的this指向誰,不是看聲明時,而是看運行時。"
而剛纔的場景中,運行時的代碼是:hongQiCutgrass( 20 );變數hongQiCutgrass指向一個函數,變數hongQiCutgrass沒有隸屬於任何的其他對象,所以,變數hongQiCutgrass是一個全局變數,隸屬於全局對象。
這也就意味著,運行hongQiCutgrass( 20 );時,
1. cutGrass函數體中的this是指向'全局對象',
2. this.oilVolume 就指代另外一個全局變數oilVolume。運行前的值是8000,運行之後變成7600。
講到這裡,有些同學可能會問,hongQiCutgrass不是指向了GuangMing.cutGrass了嗎?這可能需要理一下JavaScript中'變數'和'變數所指的對象'的關係。
(具體JavaScript引擎如何實現,我無從考證,下圖是我對在JavaScript中'變數'和'變數所指的對象'之間的關係的理解,不對之處,歡迎各位大俠指出。)
我們知道,在JavaScript中:
1. 對象傳遞,都是引用傳遞(包括:賦值、參數傳遞等)。
2. 函數也是對象。
代碼執行到第13行時,狀態如(a)所示,對象GuangMing的成員cutGrass指向具體的函數對象F008。
當代碼執行到第15行時,因為賦值的過程其實傳遞的也是引用,所以,GuangMing.cutGrass 和 hongQiCutgrass都指向了F008這個函數對象。
當代碼執行到第19行時,因為hongQiCutgrass不屬於某個具體的對象,所以,函數體內的this就指代全局對象,這就是所謂的'函數調用方式'。
如果把全局變數hongQiCutgrass理解為'全局對象'的成員,把全局變數oilVolume也理解為'全局對象'的成員,那麼,其實'方法調用方式'和'函數調用方式'的原理是一致的。
某一天,旁邊的東方小區見光明小區提供的'割草服務'很好,所以,它也向業主宣稱可以提供割草服務。但是,東方小區也沒有自己去購買一臺割草機,而是配了一把'光明小區'的割草機的鑰匙。如果用代碼表示如下:
//...前面的代碼不變,增加dongfang小區的聲明部分 var DongFang = { oilVolume:6000, cutGrass:GuangMing.cutGrass }; DongFang.cutGrass( 6 );
運行後的輸出結果如下:
正在割 6 平米的草坪,需要用油:120 mL。 原來有油:6000 mL 用完之後,剩餘的油量為:5880 mL
這樣做,業務功能是達到了,但是存在的缺點也很明顯,一方面比較繁瑣,另一方面與常識不太相符,容易導致混亂,不好理解。
相當於小明家有兩個小孩,小紅家也有兩個小孩。
>>開始時小明的媽媽跟小明交代說,你去買一個冰淇淋給你弟弟吃。(相當於聲明時的樣子)
>>後來,小明的媽媽遠遠地看到有人在喂冰淇淋。心裡想著:'應該是小明在給他弟弟小亮喂冰淇淋吧。'
而實際上呢,是小明在給小紅的弟弟(小藍)喂冰淇淋。(相當於運行時的樣子)
>>這和'小明媽媽'的預期是不一樣的!!
就是說,設計 DongFang 這個對象的設計師的這種搞法,GuangMing這個對象的設計師是預期不到的!某天GuangMing.cutGrass裡面的功能變化了,DongFang.cutGrass的行為也會跟著變化,而這種關係,在代碼中表現得並不明顯。
所以,如果把某個'方法f'是'聲明'在一個'對象A'里的,那麼,這個對象的設計者當然是希望這個方法中的this,就是指向'對象A',其他的對象如果要使用這個'方法f'也可以,但是,不能偷偷摸摸的'引用',而是應該明確指出:'對象B借用了對象A的方法f'。'對象B'借用'對象A'的方法f,沒錯,這就是我們接下來馬上要介紹的apply調用。
(看來JavaScript的設計者也是用心良苦哈,為了可能會'亂用'對象方法的熊孩子操碎了心^_^~~)
有人也許會問,JavaScript對this的處理方式,確實是不好理解哈,除了容易引起邏輯混亂之外,難道有什麼好處嗎?好處其實也是有的,我們這裡先留一個懸念,等在講解構造器調用時再細講。
3. apply調用
關於apply調用模式的細節,前一篇博文"兄臺息怒,關於arguments,您的想法和大神是一樣一樣的----閑聊JS中的apply和call" 已經講得比較詳細,這裡就順著今天我們的'割草機'的例子,展現一下相關的代碼,一起回顧一下。
var GuangMing = { oilVolume:5000, cutGrass:function( grass_num ){ var oilConsumption = grass_num * 20 ; //假設每平米草坪需要耗油 20 ml console.log( '正在割 ' + grass_num + ' 平米的草坪,需要用油:' + oilConsumption + ' mL。' ); console.log( '原來有油:' + this.oilVolume + ' mL' ); this.oilVolume = this.oilVolume - oilConsumption ; console.log( '用完之後,剩餘的油量為:' + this.oilVolume + ' mL' ); return ; } }; var DongFang = { oilVolume:6000 }; //光明小區是全國文明小區,你要借用就光明正大的借用,偷偷配把鑰匙放在自己的辦公室就不應該了嘛 //正確的'借用'姿勢 //因為我們只想割6平米的草坪,相當於只有一個'參數列表',而不是一個參數數組,所以用call,而不是apply GuangMing.cutGrass.call( DongFang , 6 );
運行後的輸出結果如下:
正在割 6 平米的草坪,需要用油:120 mL。 原來有油:6000 mL 用完之後,剩餘的油量為:5880 mL
4. 構造器調用
這種方式也許是應用最廣泛的調用方式,特別是當我們剛剛學慣用JavaScript寫面向對象的應用時。儘管用這種方式存在沒有私密性等諸多問題,大神Douglas Crockford也極力反對用這種方式創建對象,但是,因為這種方式"太方便"了,與傳統的靜態語言(Java等)的對象建模方式比較像,所以,似乎非常好'理解'。
閑話休說,我們還是回到我們的場景。據說光明小區物業配備'割草機'為廣大業主割草的事獲得了紅旗街道辦的好評,於是紅旗街道辦就給出了規定,轄區內每個小區的物業都應該配備這樣的設備,提供割草服務,並指定相關的規章制度。詳細的規章制度由小明來制定。於是,小明開始挑燈夜戰,寫下瞭如下的代碼:
var Area = function( area_name ){ this.name = area_name ; this.oilVolume = 5000 ; //每個小區預設配備5000mL的油用於割草服務 } Area.prototype.cutGrass = function( grass_num ){ console.log( '**'+ this.name + '小區**正在提供割草服務:' ); var oilConsumption = grass_num * 20 ; //假設每平米草坪需要耗油 20 ml console.log( '正在割 ' + grass_num + ' 平米的草坪,需要用油:' + oilConsumption + ' mL。' ); console.log( '原來有油:' + this.oilVolume + ' mL' ); this.oilVolume = this.oilVolume - oilConsumption ; console.log( '用完之後,剩餘的油量為:' + this.oilVolume + ' mL' ); return ; } //創建一個東方小區 var dongfang = new Area( '東方' ); dongfang.cutGrass( 2 ); //創建一個勞動小區 var laodong = new Area( '勞動' ); laodong.cutGrass( 10 );
運行後的輸出結果如下:
**東方小區**正在提供割草服務: 正在割 2 平米的草坪,需要用油:40 mL。 原來有油:5000 mL 用完之後,剩餘的油量為:4960 mL **勞動小區**正在提供割草服務: 正在割 10 平米的草坪,需要用油:200 mL。 原來有油:5000 mL 用完之後,剩餘的油量為:4800 mL
在這裡,我們做瞭如下的動作:
1. 構建構造器函數Area,以便可以用來創建不同的小區。
2. 為構造器函數Area的原型對象(prototype)增加割草服務的方法:cutGrass。因為我們知道,即使是不同的小區,提供割草服務的作業流程是一樣的,所以把方法定義到原型對象中。
3. 使用new運算符,依次創建了'東方小區'(dongfang)和'勞動小區'(laodong),並分別調用了它們的割草服務(cutGrass)。
發現運行的結果和我們預期的一樣。
現在,我們就來看一下new運算符(構造器調用)的原理,以var dongfang = new Area( '東方' ); 為例說明:
1. 執行new運算時,JavaScript引擎會生成一個全新的對象,這個對象的原型鏈,會'鏈向'構造函數的原型對象(prototype)。例子中的構造函數就是Area。
註意:是一個全新的對象哦,就是JavaScript引擎會單獨開闢一塊空間給新建的對象。
('變數'和'變數所引用的對象'的關係,我們前面有介紹,這裡就不再贅述。)
2. 在將新建的'全新對象'返回之前,JavaScript引擎會執行'構造函數'(Area)函數體內的代碼,
這時候,函數體內的this就是指代這個'新建的全新的對象'。 (註意:這就是構造器調用與其他調用方式的不同所在。)
顯然,如果沒有這個new運算符而直接調用構造器函數,那麼,依據前面我們分析的規則,函數體中的this將會指向'全局對象',
而這種情況發生時,JavaScript不會報任何的錯誤!這也是許多大神為什麼對'構造器調用'方式創建對象比較詬病的原因。
3. 構造器函數執行到最後,
如果用return語句返回一個不是對象類型的值,或者沒有執行return語句,則返回前面創建的全新的對象(這種情況是符合我們預期的)。
如果用return語句返回一個是'對象類型'的值,那麼,返回的就是指定的這個'對象',而不一定是之前創建的全新的對象。
(後面這種情況是我們需要避免的,試想,辛辛苦苦創建了一個對象,JavaScript引擎為其開闢了空間,但是被沒有返回,沒有變數引用它,意味著在瞎搞。)
關於構造器調用方式,整個過程就是這樣。
我們再回去分析一下在講解'函數調用'時留下的懸念,當時經過我們的分析,JavaScript的this動態綁定特性(函數體內的this指向誰,不是看聲明時,而是看運行時。)似乎除了把問題搞複雜,沒有帶來什麼好處。現在,我們來剖析一下例子中的dongfang.cutGrass( 2 );這個語句的來龍去脈,就能體會到JavaScript中的這個特性還是有它的意義的。
我們知道:
1. dongfang這個對象是前面通過new運算符創建的全新對象(約定:'變數'和'變數所指的對象'在不影響語義理解的情況下,我們就不作區別)。
2. 我們並沒有為dongfang這個對象增加cutGrass的成員屬性。
那麼,為什麼我們能夠這樣執行dongfang.cutGrass( 2 );呢?
答案是:JavaScript的原型繼承機制。
當JavaScript引擎發現dongfang並沒有cutGrass的成員屬性時,它就會順著'原型鏈'去找,而根據前面的new運算符的特點,dongfang這個全新的對象所'鏈向'的'原型對象正是Area.prototype這個對象,在Area.prototype對象中發現有cutGrass這個函數,於是就調用了Area.prototype中的cutGrass這個函數。
(註意:我們的行文中用'鏈向'這個詞,以便於區別'指向'或'引用'的含義,關於原型鏈的前世今生,我們有空再聊。)
整個過程的來龍去脈已經有點眉目了。
我們再綜合後面的語句laodong.cutGrass( 10 ),從執行結果來看,laodong 和 dongfang 是兩個獨立的對象,它們都調用了Area.prototype中定義的cutGrass。
正是因為(函數體內的this指向誰,不是看聲明時,而是看運行時。)這個特性,才確保了:
- 執行 dongfang.cutGrass( 2 ); 時,cutGrass()中的this指向 dongfang 這個對象。
- 執行 laodong.cutGrass( 10 ); 時,cutGrass()中的this指向 laodong 這個對象。
如果JavaScript不是採用這種'動態'的機制,cutGrass中的this就只能指向Area.prototype,那麼,'函數'這種重要的數據類型,在'原型繼承'的這種機制中就很難發揮強大的作用。
用閉包實現業務建模
4種調用模式已經講解完畢,我們舉了一個'小區提供割草服務'這樣的例子,顯然,前面的例子主要是為了講解特性的方便而設計的,從'業務建模'的角度來看,是存在很多不足的。
如果真的要求創建一個小區這樣的對象,然後對外提供割草服務該如何搞呢?請看代碼:
var createArea = function( area_name ){ var _name = area_name ; var _oilVolume = 5000 ; //預設還是配備5000mL的油 var cutGrass = function( grass_num ){ console.log( '**'+ _name + '小區**正在提供割草服務:' ); var oilConsumption = grass_num * 20 ; //假設每平米草坪需要耗油 20 ml console.log( '正在割 ' + grass_num + ' 平米的草坪,需要用油:' + oilConsumption + ' mL。' ); console.log( '原來有油:' + _oilVolume + ' mL' ); _oilVolume = _oilVolume - oilConsumption ; console.log( '用完之後,剩餘的油量為:' + _oilVolume + ' mL' ); return ; } var new_area = {}; new_area.cutGrass = cutGrass ; new_area.setOilVolume = function( oil_volume ){ _oilVolume = oil_volume ; console.log( _name + '小區中割草機用的汽油配備到:' + oil_volume + 'mL' ); return _oilVolume; } new_area.getOilVolume = function( ){ return _oilVolume; } return new_area ; }; var dongfang = createArea( '東方' ); dongfang.cutGrass( 2 ); console.log( '還剩餘可用的油:' + dongfang.getOilVolume() ); dongfang.setOilVolume( 9000 ); dongfang.cutGrass( 7 );
運行後的輸出結果如下:
**東方小區**正在提供割草服務: 正在割 2 平米的草坪,需要用油:40 mL。 原來有油:5000 mL 用完之後,剩餘的油量為:4960 mL 還剩餘可用的油:4960 東方小區中割草機用的汽油配備到:9000mL **東方小區**正在提供割草服務: 正在割 7 平米的草坪,需要用油:140 mL。 原來有油:9000 mL 用完之後,剩餘的油量為:8860 mL
代碼中展示的業務模型,基於如下的業務理解:
1. 小區的名稱,僅僅在創建小區的時候通過參數傳入設置一下,之後就不允許修改了。
就像在現實生活中,小區的門口會搬一塊大石頭放到那裡,上面刻上"愛情灣畔"幾個大字,整好之後就不會去改了。
2. 小區對外提供割草服務(cutGrass)。
可以看看小區現在還有多少割草機的儲備油(getOilVolume),以便判斷是否夠這次割草作業。
也可以給小區添加儲備的油(setOilVolume),例如:物業公司規定,到了月底,儲備的油要增加到8000mL,於是可以執行dongfang.setOilVolume( 8000 );。
這是一個採用'閉包'的方式構建的對象,我們發現,構建出來的dongfang對象,似乎也能滿足我們的業務要求,創建好對象之後,不能改變小區的名稱,只能通過setOilVolume和getOilVolume操作變數_oilVolume。更重要的特點時,居然'沒有用到this'!
把這個例子補充在這裡作為對比,我們不難發現:"在應用開發的過程中,一切應該以業務目標為導向,語言的各種特性只是一種手段"。
因為例子中的小區對象是一個非常具體的'業務對象',所以可以不使用this。如果你是在寫框架性質的對象,那你一定要理解this,一定要理解apply調用方式。"
閉包是一個"簡單但很有內涵"的特性,前面我們聊過'閉包'與'原型繼承'的使用,後續如果有時間,我們可以聊聊閉包的其他故事。
【總結】
JavaScript中函數的多種調用方式,是它作為動態語言,具有強大表現力的基礎,此外,不對函數的參數類型進行校驗、可以把函數作為對象到處傳遞,也是JavaScript的動態語言特性的體現。我們可以與其他靜態語言作一個對比。
1. 類型檢查
靜態語言中,對傳入的參數會做類型檢查。而動態語言中,則不會對傳入的參數類型以及個數做檢查。
在Java社區,一天熊孩子小東去跟社區的王叔叔請求幫助。
小東:"張叔叔,今天我和小傑想去'衛星農場'割草,你可不可以幫我?"
老張:"好啊,你在這裡登記一下,這是割草機的鑰匙,讓王叔叔跟你們一起去。"
小東找到老王,讓他幫忙去'衛星農場'去割草,老王問了一下情況,跟小東說:"'衛星農場'哪裡有草坪啊?那時麥地!"。
同樣的場景,在JavaScript社區就會發生不一樣的情況,
小東:"張叔叔,今天我和小傑想去'衛星農場'割草,你可不可以幫我?"
老張:"好啊,這是割草機的鑰匙,你拿去用吧。"
到了'衛星農場',小東就啟動"割草機",不一會兒功夫,就把一塊麥地里的小麥放倒了。
回來之後的日記是這麼寫的:"今天,天氣不錯,萬里無雲的天空飄著朵朵白雲......這真是有意義的一天啊!"
【分析】
在靜態語言(例如:Java)中,在編譯階段就會對方法的參數類型以及參數數量做檢查,發現不對勁就提示'編譯錯誤',相當於每個方法邊上都站著一個'老王',負責對方法的類型和數量進行檢查。
在動態語言(例如:JavaScript)中,則不會對方法做這方面的檢查,如果把方法比作'割草機',那麼對於JavaScript引擎來說,它才不管你割的是'小草'還是'小麥',不管你往'割草機'的油箱中加入的是'汽油'還是'醬油',等運行的時候,發現錯誤才給你指出這裡出錯了。
雖然JavaScript在函數這個層面沒有提供類型檢查的服務,作為補充,它也提供了typeof, instanceof等檢查類型的方式,所以,我們經常看到一些函數的函數體的開始部分,是一堆的if-else,對傳入的參數進行檢查。
想起了高考考場的門衛保全:'有沒有帶准考證?沒帶的出去。有沒有帶手機?帶了的也出去。脫一下....'
沒辦法,畢竟'考生'不是'保全','保全'也不是'考生'。
"小李,這是杜老闆的兒子。"
"..."
2. 方法調用
在靜態語言(Java)中,當我們需要使用某個對象(類)的方法的時候,我們至少是要通過這個對象(類)來調用的,或者繼承這個類,然後在子類中使用父類的方法,或者將這個類的對象作為當前對象的一個成員,然後通過這個成員調用。相當於,要使用社區的'割草機',至少是應該打聲招呼登記一下的。
但是在動態語言(JavaScript)中,函數在記憶體中似乎是獨立存在的(我們知道:在JavaScript中,函數也是對象),儘管聲明時好像這個函數是'屬於'對象A的,但只有真正使用的時候(運行時)才表現出來它是'屬於'誰的,函數體內的this是指代誰的。
並且,引用某個函數並沒有太多的限制,就像你有割草機的鑰匙,你就可以啟動這台割草機,使用這台割草機。
很顯然,JavaScript的某些特性(原型鏈、apply調用方式、高階函數屬性、閉包等等)使JavaScript變得非常靈活,當然也容易出錯。這跟現實生活一樣,太多的條條框框,你會覺得不自由,行為受到束縛,但是,沒有了一些規則約束,沒有了父母的嘮叨和提醒,有時也確實會犯錯誤。
所以,在使用JavaScript開發應用時,一方面我們通過編程規範(共同的約定)來減少錯誤的發生。比如:在社區中,大家約定從'業主之家'借的梯子,用完之後要還回去。另一方面,我們也可以利用JavaScript中的某些特性(例如:閉包),來構建封裝性、穩定性更好的具有彈性的應用。比如:我們擔心熊孩子在使用社區的割草機時,可能會把'醬油'當做'汽油'倒到割草機的油箱中,這時候我們可以把割草機的油箱鎖起來,不讓隨便往裡面加東西。
顯然熊孩子是玩不好JavaScript的,在更多的時候,我們要使用JavaScript,是因為我們要開發的應用只能用它來開發,既然沒得其他的選擇,那就只有'好好學習'一條路了。正如某位前輩所言,只有我們理解了它的精華,也明白它的糟粕,才能真正用好它。
好像有人在敲門,應該是網上預定的宵夜送到了.....
好,今天就和大伙聊到這裡,感謝諸位捧場。