景
前端渲染有很多框架,而且形式和內容在不斷發生變化。這些演變的背后是設計模式的變化,而歸根到底是功能劃分邏輯的演變:MVC—>MVP—>MVVM(忽略最早混在一起的寫法,那不稱為模式)。近幾年興起的React、Vue、Angular等框架都屬于MVVM模式,能幫我們實現界面渲染、事件綁定、路由分發等復雜功能。但在一些只需完成數據和模板簡單渲染的場合,它們就顯得笨重而且學習成本較高了。
例如,在美團外賣的開發實踐中,前端經常從后端接口取得長串的數據,這些數據擁有相同的樣式模板,前端需要將這些數據在同一個樣式模板上做重復渲染操作。
解決這個問題的模板引擎有很多,doT.js(出自女程序員Laura Doktorova之手)是其中非常優秀的一個。下表將doT.js與其他同類引擎做了對比:
框架 | 大小 | 壓縮版本大小 | 迭代 | 條件表達式 | 自定義語法 |
---|---|---|---|---|---|
doT.js | 6KB | 4KB | √ | √ | √ |
mustache | 18.9 KB | 9.3 KB | √ | × | √ |
Handlebars | 512KB | 62.3KB | √ | √ | √ |
artTemplate(騰訊) | - | 5.2KB | √ | √ | √ |
BaiduTemplate(百度) | 9.45KB | 6KB | √ | √ | √ |
jQuery-tmpl | 18.6KB | 5.98KB | √ | √ | √ |
可以看出,doT.js表現突出。而且,它的性能也很優秀,本人在Mac Pro上的用Chrome瀏覽器(版本為:56.0.2924.87)上做100條數據10000次渲染性能測試,結果如下:
從上可以看出doT.js更值得推薦,它的主要優勢在于:
小巧精簡,源代碼不超過兩百行,6KB的大小,壓縮版只有4KB;
支持表達式豐富,涵蓋幾乎所有應用場景的表達式語句;
性能優秀;
不依賴第三方庫。
本文主要對doT.js的源碼進行分析,探究一下這類模板引擎的實現原理。
如何使用
如果之前用過doT.js,可以跳過此小節,doT.js使用示例如下:
<script type="text/html" id="tpl">
可以看出doT.js的設計思路:將數據注入到預置的視圖模板中渲染,返回HTML代碼段,從而得到最終視圖。
下面是一些常用語法表達式對照表:
項目 | JavaScript語法 | 對應語法 | 案例 |
---|---|---|---|
輸出變量 | = | {{= 變量名}} | {{=it.name }} |
條件判斷 | if | {{? 條件表達式}} | {{? i > 3}} |
條件轉折 | else/else if | {{??}}/{{?? 表達式}} | {{?? i ==2}} |
循環遍歷 | for | {{~ 循環變量}} | {{~ it.arr:item}}...{{~}} |
執行方法 | funcName() | {{= funcName() }} | {{= it.sayHello() }} |
源碼分析及實現原理
和后端渲染不同,doT.js的渲染完全交由前端來進行,這樣做主要有以下好處:
脫離后端渲染語言,不需要依賴后端項目的啟動,從而降低了開發耦合度、提升開發效率;
View層渲染邏輯全在JavaScript層實現,容易維護和修改;
數據通過接口得到,無需考慮后端數據模型變化,只需關心數據格式。
doT.js源碼核心:
...
這段代碼總結起來就是一句話:用正則表達式匹配預置模板中的語法規則,將其轉換、拼接為可執行HTML代碼,作為可執行語句,通過new Function()創建的新方法返回。
代碼解析重點1:正則替換
正則替換是doT.js的核心設計思路,本文不對正則表達式做擴充講解,僅分析doT.js的設計思路。先來看一下doT.js中用到的正則:
templateSettings: {
源碼中將正則定義寫到一起,這樣方便了維護和管理。在早期版本的doT.js中,處理條件表達式的方式和tmpl一樣,采用直接替換成可執行語句的形式,在最新版本的doT.js中,修改成僅一條正則就可以實現替換,變得更加簡潔。
doT.js源碼中對模板中語法正則替換的流程如下:
代碼解析重點2:new Function()運用
函數定義時,一般通過Function關鍵字,并指定一個函數名,用以調用。在JavaScript中,函數也是對象,可以通過函數對象(Function Object)來創建。正如數組對象對應的類型是Array,日期對象對應的類型是Date一樣,如下所示:
var funcName = new Function(p1,p2,...,pn,body);
參數的數據類型都是字符串,p1到pn表示所創建函數的參數名稱列表,body表示所創建函數的函數體語句,funcName就是所創建函數的名稱(可以不指定任何參數創建一個匿名函數)。
下面的定義是等價的。
例如:
// 一般函數定義方式
從上面的代碼中可以看出,Function的最后一個參數,被轉換為可執行代碼,類似eval的功能。eval執行時存在瀏覽器性能下降、調試困難以及可能引發XSS(跨站)攻擊等問題,因此不推薦使用eval執行字符串代碼,new Function()恰好解決了這個問題。回過頭來看doT代碼中的"new Function(c.varname, str)",就不難理解varname是傳入可執行字符串str的變量。
具體關于new Fcuntion的定義和用法,詳細請閱讀Function詳細介紹。
性能之因
讀到這里可能會產生一個疑問:doT.js的性能為什么在眾多引擎如此突出?通過閱讀其他引擎源代碼,發現了它們核心代碼段中都存在這樣那樣的問題。
jQuery-tmpl
function buildTmplFn( markup ) {
在上面的代碼中看到,jQuery-teml同樣使用了new Function()的方式編譯模板,但是在性能對比中jQuery-teml性能相比doT.js相差甚遠,出現性能瓶頸的關鍵在于with語句的使用。
with語句為什么對性能有這么大的影響?我們來看下面的代碼:
var datas = {persons:['李明','小紅','趙四','王五','張三','孫行者','馬婆子'],gifts:['平民','巫師','狼','獵人','先知']};
上面代碼中使用了一個with表達式,為了避免多次從datas中取變量而使用了with語句。這看起來似乎提升了效率,但卻產生了一個性能問題:在JavaScript中執行方法時會產生一個執行上下文,這個執行上下文持有該方法作用域鏈,主要用于標識符解析。當代碼流執行到一個with表達式時,運行期上下文的作用域鏈被臨時改變了,一個新的可變對象將被創建,它包含指定對象的所有屬性。此對象被插入到作用域鏈的最前端,意味著現在函數的所有局部變量都被推入第二個作用域鏈對象中,這樣訪問datas的屬性非常快,但是訪問局部變量的速度卻變慢了,所以訪問代價更高了,如下圖所示。
這個插件在GitHub上面介紹時,作者Boris Moore著重強調兩點設計思路:
模板緩存,在模板重復使用時,直接使用內存中緩存的模板。在本文作者看來,這是一個雞肋的功能,在實際使用中,無論是直接寫在String中的模板還是從Dom獲取的模板都會以變量的形式存放在內存中,變量使用得當,在頁面整個生命周期內都能取到這個模板。通過源碼分析之后發現jQuery-tmpl的模板緩存并不是對模板編譯結果進行緩存,并且會造成多次執行渲染時產生多次編譯,再加上代碼with性能消耗,嚴重拖慢整個渲染過程。
模板標記,可以從緩存模板中取出對應子節點。這是一個不錯的設計思路,可以實現數據改變只重新渲染局部界面的功能。但是我覺得:模板將渲染結果交給開發者,并渲染到界面指定位置之后,模板引擎的工作就應該結束了,剩下的對節點操作應該靈活的掌握在開發者手上。
不改變原來設計思路基礎之上,嘗試對源代碼進行性能提升。
先保留提升前性能作為對比:
首先來我們做第一次性能提升,移除源碼中with語句。
第一次提升后:
接下來第二部提升,落實Boris Moore設計理念中的模板緩存:
優化后的這一部分代碼段被我們修改成了:
function buildTmplFn( markup ) {
在doT.js源碼中沒有用到with這類消耗性能的語句,與此同時doT.js選擇先將模板編譯結果返回給開發者,這樣如要重復多次使用同一模板進行渲染便不會反復編譯。
僅25行的模板:tmpl
(function(){
閱讀這段代碼會驚奇的發現,它更像是baiduTemplate精簡版。相比baiduTemplate而言,它移除了baiduTemplate的自定義語法標簽的功能,使得代碼更加精簡,也避開了替換用戶語法標簽而帶來的性能消耗。對于doT.js來說,性能問題的關鍵是with語句。
綜合上述我對tmpl的源碼進行移除with語句改造:
改造之前性能:
改造之后性能:
如果讀者對性能對比源碼比較感興趣可以訪問 https://github.com/chen2009277025/TemplateTest 。
總結
通過對doT.js源碼的解讀,我們發現:
doT.js的條件判斷語法標簽不直觀。當開發者在使用過程中條件判斷嵌套過多時,很難找到對應的結束語法符號,開發者需要自己嚴格規范代碼書寫,否則會給開發和維護帶來困難。
doT.js限制開發者自定義語法標簽,相比較之下baiduTemplate提供可自定義標簽的功能,而baiduTemplate的性能瓶頸恰好是提供自定義語法標簽的功能。
很多解決我們問題的插件的代碼往往簡單明了,那些龐大的插件反而存在負面影響或無用功能。技術領域有一個軟件設計范式:“約定大于配置”,旨在減少軟件開發人員需要做決定的數量,做到簡單而又不失靈活。在插件編寫過程中開發者應多注意使用場景和性能的有機結合,使用恰當的語法,盡可能減少開發者的配置,不求迎合各個場景。
作者簡介
建輝,美團外賣高級前端研發工程師,2015年加入美團點評外賣事業部。目前在前端業務增長組,主要負責運營平臺搭建,主導運營活動業務。
歡迎大家一起溝通交流,博客Hi-FE。
不想錯過技術博客更新?想給文章評論、和作者互動?第一時間獲取技術沙龍信息?
請關注我們的官方微信公眾號“美團點評技術團隊”。
為一個使用了jQuery很多年 的人,最近,我成為了一個Vue的皈依者,我認為從一個框架到另一個框架的遷移過程將是一個值得討論的有趣的話題。
在我開始之前,我想清楚地說明一點。我并沒有以任何方式告訴任何人去停止使用jQuery。jQuery最近相當流行,而且見鬼,我幾年前也寫過類似的東西(“我如何(不)使用jQuery”)。如果你使用jQuery完成了你的項目,并且你的最終用戶成功地使用了你的站點,那么你將獲得更多的動力去繼續使用對你有用的東西。
本指南更適合那些可能具有多年jQuery經驗并希望了解如何使用Vue來完成工作的人。考慮到這一點,我將重點介紹我所認為的“核心”jQuery用例。我不會涵蓋每一個可能的特性,而是用“我經常使用jQuery來完成 [X]”的方式來代替,這種方式可能更適合那些考慮學習Vue的人。(順便提一句,請注意,我編寫示例的方式只是執行一個任務的一種方式。jQuery和Vue都提供了多種方法來實現相同的目標,這是一件很棒的事情!)
記住了這一點,我們來思考一些可以使用jQuery完成的高級的東西:
當然,jQuery還有更多的功能,但是這些用途(至少在我看來)涵蓋了最常見的用例。還要注意,在上面的列表中有很多異花授粉現象。那么,我們應該從簡單的一一對應的比較開始嗎?不,沒那么快。我們先從介紹Vue應用程序中的主要差異開始。
#定義Vue的使用場景
當我們將jQuery加入到頁面上時,我們基本上是在JavaScript代碼中添加一把瑞士軍刀來處理常見的web開發任務。我們可以按照我們認為合適的順序來處理任何一個用例。例如,今天客戶可能會要求表單驗證,然后在一個月左右后,又要求在站點的頭部添加一個基于Ajax的搜索表單。
Vue在這方面有一個顯著的不同。當使用Vue開始一個項目時,我們首先會在DOM中定義一個我們希望Vue專用的“區域”。因此,我們來考慮一個簡單的原型web頁面:
在一個典型的jQuery應用程序中,我們可以編寫代碼來處理頭部、側邊欄和登錄表單等。這很簡單:
而在一個Vue應用程序中,我們首先需要指定要處理的內容。假設我們的客戶首先要求我們向loginForm元素添加驗證,那么我們的Vue代碼就要指定這一點:
這意味著,如果客戶后來決定讓我們在側邊欄中添加一些內容,那我們通常會添加第二個Vue應用程序:
這是件壞事嗎?絕對不是。我們馬上就會得到封裝的好處。如果我們不小心使用了一個具有泛型名稱的變量(我們都這樣做過),我們不必擔心它與代碼的其他部分發生沖突。過后,當客戶端增加了另一個要求時,像這樣將我們獨特的、邏輯化的Vue代碼集區分開就會確保每一個Vue應用程序不會相互干擾。
所以,是的,這是一件好事。但當我第一次開始使用Vue時,它絕對讓我停了下來。現在,進入我們的用例。
#在DOM中查找東西
你會發現另一個有趣或可怕的方面是如何“在DOM中查找東西”。這有點模糊,但我們來考慮一個強有力的例子。我們有一個按鈕,當它被點擊時,我們讓一些事情發生。下面是一個簡短的例子,展示了它是怎樣的:
現在我們來將這個例子與用Vue的實現方式進行比較:
這個Vue應用程序有點冗長,但是請注意標記是如何在操作(“click”)和將要調用的函數之間建立一個直接連接的。Vue的代碼并沒有與DOM進行向后綁定(我們在el部分之外定義了它需要運行的地方)。這是Vue最吸引我的地方之一——它能很容易地告訴你將要發生什么。此外,我不需要過多地擔心ID值和選擇器。如果我更改了按鈕的類或ID,我不需要返回代碼中去更新選擇器。
我們來考慮另一個例子:在DOM中查找和更改文本。想象一下那個按鈕,單擊它,現在會更改DOM的另一部分的文本。
我已經添加了一個新的span,現在,當按鈕被單擊時,我們使用另一個選擇器來查找它,并使用一個jQuery工具方法來更改其中的文本。現在我們來考慮一下Vue版本:
在本例中,我們使用Vue的模板語言(突出顯示的行)來指定我們希望在span中呈現的一個變量,在本例中是resultText。現在,當按鈕被單擊時,我們更改該值,span的內部文本將會自動更改。
順便說一句,Vue支持v-on屬性的簡寫,因此示例中的按鈕可以用@click=“ doSomething"代替。
#讀寫表單變量
處理表單可能是我們可以用JavaScript做的最常見也是最有用的事情之一。甚至在JavaScript之前,我早期的“web開發”大部分都是通過編寫Perl腳本來處理表單提交。作為接受用戶輸入的主要方式,表單對web來說一直都是很重要的,而且很可能會在相當長一段時間內保持不變。我們來考慮一個簡單的jQuery例子,它將讀取幾個表單字段并設置另一個:
這段代碼演示了jQuery如何通過val( )方法讀寫表單。最后,我們從DOM中獲取四個項目(所有的三個表單字段和一個按鈕),并使用簡單的數學方法來生成一個結果。現在我們來考慮一下Vue版本:
這里介紹了一些有趣的Vue快捷方法。首先,v-model是Vue如何在DOM和JavaScript中的值之間創建雙向數據綁定。data塊變量將自動與表單字段同步。更改數據,表單就會更新。更改表單,數據就會更新。.number是Vue的一個標志,用于將表單字段的繼承字符串值視為數字。如果我們不做這一步,按原樣做加法,我們會看到字符串加法,而不是算術。我已經使用JavaScript處理了將近一個世紀了,但還是搞砸了。
另一個簡單的“技巧”是@click.prevent。首先,@click為按鈕定義了一個單擊處理程序,然后.prevent部分會阻止瀏覽器提交表單的默認行為(相當于event.preventDefault( ))。
最后一個是綁定到該按鈕的doSum方法進行的相加操作。注意,它只處理數據變量(Vue在this作用域內允許對這些變量進行操作)。
雖然這主要是我個人的感覺,但我非常喜歡在用Vue編寫腳本時,腳本中沒有查詢選擇器,以及HTML如何更清楚地顯示它在做什么。
最后,我們甚至可以完全去掉按鈕:
Vue的一個更酷的特性是computed properties(計算屬性)。它們是虛擬值,可以識別其派生值何時被更新。在上面的代碼中,只要兩個表單字段中的任何一個發生更改,總和就會更新。這也適用于表單字段之外。我們可以這樣渲染其總和:
#使用Ajax
值得稱贊的是,jQuery使Ajax的使用變得非常簡單。事實上,我可以說我已經以一種“普通”的方式完成了Ajax,可能總共只有一次(如果你對此很好奇,你可以查看XMLHttpRequest規范,并且你可能會為你已經避免了它而感到高興)。jQuery簡單的$.get(…)方法在很多情況下都能工作,并且當它需要在更復雜的東西中使用時,$.ajax()也能使它變得簡單。jQuery做得很好的另一件事是它處理JSONP請求的方式。雖然現在使用CORS基本上沒有必要,但JSONP是一種處理向不同域中的API發出請求的方法。
那么,Vue如何讓Ajax變得更簡單呢?沒有什么!
好吧,聽起來很嚇人,但其實并不可怕。有許多處理HTTP請求的選項,而Vue.js采用了一種更不可知的方式,讓我們開發人員決定如何處理它。所以,是的,這確實意味著更多的工作,但我們有一些不錯的選擇。
首先應該考慮的是Axios,這是一個Promise-based庫,在Vue社區中非常流行。下面是一個使用它的簡單的例子(摘自它們的README文件):
Axios支持POST請求,當然,它也允許我們在許多其他選項中指定頭文件。
雖然Axios在Vue開發人員中非常流行,但我并不是真心喜歡它。(至少現在還沒有。)相反,我更喜歡Fetch。Fetch不是一個外部庫,而是執行HTTP請求的一種web標準方法。Fetch在大約90%的瀏覽器
上都有很好的支持,雖然這意味著使用它并不完全安全,但是我們總是可以使用一個我們需要的polyfill。
雖然這完全超出了我們在這里討論的范圍,但是Kingsley Silas寫了一本關于在React中使用Axios和Fetch的優秀指南。
和Axios一樣,Fetch也是Promise-based的,并且有一個友好的API:
Axios和Fetch都涵蓋了所有類型的HTTP請求,所以它們都能滿足任意數量的需求。讓我們看一個簡單的比較。下面是一個使用了星球大戰API的簡單jQuery演示。
在上面的示例中,我使用$.get調用該API并返回一個電影列表。然后我用這些數據生成一個標題列表作為 li 標記元素,并將其全部插入到一個ul塊中。
現在,讓我們考慮一個使用Vue的例子:
其中最好的部分可能是使用v-for模板。注意Vue是如何做到與布局無關的(至少與JavaScript無關)。數據是從該API中獲取的。它被分配了一個變量。布局處理如何顯示它。我一直討厭在我的JavaScript中使用HTML,但是jQuery提供了解決方案,把它嵌入到Vue中看起來就很自然很合適。
#一個完整的(在某種程度上有點瑣碎)例子
為了更好地理解它,讓我們考慮一個更真實的例子。我們的客戶要求我們為一個產品API構建一個支持Ajax的前端搜索接口。功能列表包括:
我們從jQuery版本開始。首先, HTML部分如下:
有一個帶有兩個過濾器和兩個div的表單。一個用做搜索或報告錯誤時的臨時狀態,另一個用于呈現結果。現在,檢查代碼。
代碼首先為要處理的每個DOM項(表單字段、按鈕和div)創建一組變量。代碼的邏輯核心在按鈕的點擊處理程序中。我們進行驗證,如果一切正常,就對該API執行一個POST請求。當請求返回時,我們要么呈現結果
,要么在沒有匹配的情況下顯示消息。
你可以使用下面的CodePen來運行這個演示的完整版本。
現在讓我們考慮Vue版本。同樣,我們先從布局開始:
從頂部看,其中的變化包括:
現在讓我們看看代碼。
值得調用的第一個塊是data字段集。有些映射到表單字段,有些映射到結果、狀態消息等等。searchProducts方法處理的內容與jQuery版本大致相同,但通常直接綁定到DOM的代碼要少得多。例如,即使我們知道結果是以一個無序列表列出的,但代碼本身并不關心這一點。它只是進行賦值,標記才處理呈現值。總的來說,與jQuery代碼相比,JavaScript代碼更關心邏輯,jQuery代碼“感覺”是更好的分離了關注點。
和以前一樣,這里有一個CodePen可以讓你自己試試:
#jQuery將死! Vue萬歲!
好吧,這有點過分了。正如我在開始時所說的,如果你喜歡使用jQuery并且它對你有用的話,那我覺得你完全沒必要更改任何東西。
不過,我想說的是,對于習慣使用jQuery的人來說,Vue似乎是一個很好的“下一步”。Vue支持復雜的應用程序,并為搭建和構建項目提供了一個非常棒的命令行工具。但是對于更簡單的任務來說,Vue是一個很棒的“現代jQuery”的替代品,它已經成為我開發的可選工具!
有關使用Vue替代jQuery的另一個觀點,請查看Sarah Drasner的“使用Vue.js替換jQuery:無需構建步驟”,因為它包含了其他一些超級有用的例子。
英文原文:https://css-tricks.com/making-the-move-from-jquery-to-vue/
譯者:浣熊君( ????? )
者 | 司徒正美
責編 | 郭芮
出品 | CSDN(ID:CSDNnews)
JavaScript能發展到現在的程度已經經歷不少的坎坷,早產帶來的某些缺陷是永久性的,因此瀏覽器才有禁用JavaScript的選項。甚至在jQuery時代有人問出這樣的問題,jQuery與JavaScript哪個快?在Babel.js出來之前,發明一門全新的語言代碼代替JavaScript的呼聲一直不絕于耳,前有VBScript,Coffee, 后有Dartjs, WebAssembly。要不是它是所有瀏覽器都內置的腳本語言, 可能就命絕于此。瀏覽器就是它的那個有錢的丈母娘。此外源源不斷的類庫框架,則是它的武器庫,從底層革新了它自己。為什么這么說呢?
JavaScript沒有其他語言那樣龐大的SDK,針對某一個領域自帶的方法是很少,比如說數組方法,字符串方法,都不超過20個,是Prototype.js給它加上的。JavaScript要實現頁面動效,離不開DOM與BOM,但瀏覽器互相競爭,導致API不一致,是jQuery搞定了,還帶來了鏈式調用與IIFE這些新的編程技巧。在它缺乏大規模編程模式的時候,其他語言的外來戶又給它帶來了MVC與MVVM……這里面許多東西,久而久之都變成語言內置的特性,比如Prototype.js帶來的原型方法,jQuery帶來的選擇器方法,實現MVVM不可缺少的對象屬性內省機制(getter, setter, Reflect, Proxy), 大規模編程需要的class, modules。
本文將以下幾個方面介紹這些新特性,正是它們武裝了JavaScript,讓它變成一個正統的,魔幻的語言。
原型方法的極大豐富;
類與模塊的標準化;
異步機制的嬗變;
塊級作用域的補完;
基礎類型的增加;
反射機制的完善;
更順手的語法糖。
原型方法的極大豐富
原型方法自Prototype.js出來后,就不斷被招安成官方API。基本上在字符串與數組這兩大類別擴充,它們在日常業務中不斷被使用,因此不斷變重復造輪子,因此企待官方化。
JavaScript的版本說明:
這些原型方法非常有用,以致于在面試中經常被問到,如果去除字符串兩邊的空白,如何扁平化一個數組?
類與模塊的標準化
在沒有類的時代,每個流行框架都會帶一個創建類的方法,可見大家都不太認同原型這種復用機制。
下面是原型與類的寫法比較:
function Person(name) {
this.name = name;
}
//定義一個方法并且賦值給構造函數的原型
Person.prototype.sayName = function {
return this.name;
};
var p = new Person('ruby');
console.log(p.sayName) // ruby
class Person {
constructor(name){
this.name = name
}
sayName {
return this.name;
}
}
var p = new Person('ruby');
console.log(p.sayName) // ruby
我們可以看到es6的定義是非常簡單的,并且不同于對象鍵值定義方式,它是使用對象簡寫來描述方法。如果是標準的對象描述法,應該是這樣:
//下面這種寫法并不合法
class Person {
constructor: function(name){
this.name = name
}
sayName: function {
return this.name;
}
}
如果我們想繼承一個父類,也很簡單:
class Person extends Animal {
constructor: function(name){
super;
this.name = name
}
sayName: function {
return this.name;
}
}
此外,它后面還補充了三次相關的語法,分別是屬性初始化語法,靜態屬性與方法語法,私有屬性語法。目前私有屬性語法爭議非常大,但還是被標準化。雖然像typescript的private、public、protected更符合從后端轉行過來的人的口味,不過在babel無所不能的今天,我們完全可以使用自己喜歡的寫法。
與類一起出現的還有模塊,這是一種比類更大的復用單元,以文件為載體,可以實現按需加載。當然它最主要的作用是減少全局污染。jQuery時代,通過IIFE減少了這癥狀,但是JS文件沒有統一的編寫規范,意味著想把它們打包一個是非常困難的,只能像下面那樣平鋪著。這些文件的依賴關系,只有最初的人知道,要了幾輪開發后,就是定時炸彈。此外,不要忘記,<script>
標準還會導致頁面渲染堵塞,出現白屏現象。
<script src="zepto.js"></script>
<script src="jhash.js"></script>
<script src="fastClick.js"></script>
<script src="iScroll.js"></script>
<script src="underscore.js"></script>
<script src="handlebar.js"></script>
<script src="datacenter.js"></script>
<script src="util/wxbridge.js"></script>
<script src="util/login.js"></script>
<script src="util/base.js"></script>
于是后jQuery時代,國內流行三種模塊機制,以seajs主體的CMD,以requirejs為主體的AMD,及nodejs自帶的Commonjs。當然,后來還有一種三合一方案UMD(AMD, Commonjs與es6 modules)。
requirejs的定義與使用:
define(['jquery'], function($){
//some code
var mod = require("./relative/name");
return {
//some code
} //返回值可以是對象、函數等
})
require(['cores/cores1', 'cores/cores2', 'utils/utils1', 'utils/utils2'], function(cores1, cores2, utils1, utils2){
//some code
})
requirejs是世界第一款通用的模塊加載器,尤其自創了shim機制,讓許多不模范的JS文件也可以納入其加載系統。
define(function(require){
var $ = require("jquery");
$("#container").html("hello,seajs");
var service = require("./service")
var s = new service;
s.hello;
});
//另一個獨立的文件service.js
define(function(require,exports,module){
function Service{
console.log("this is service module");
}
Service.prototype.hello = function{
console.log("this is hello service");
return this;
}
module.exports = Service;
});
Seajs是阿里大牛玉伯加的加載器,借鑒了Requiejs的許多功能,聽說其性能與嚴謹性超過前者。當前為了正確分析出define回調里面的require語句,還發起了一個 100 美刀賞金活動,讓國內高手一展身手。
https://github.com/seajs/seajs/issues/478
image_1doan2vfl17ld1nin1hbm182c9b9p.png-72.9kB
相對而言,nodejs模塊系統就簡單多了,它沒有專門用于包裹用戶代碼的define方法,它不需要顯式聲明依賴。
//world.js
exports.world = function {
console.log('Hello World');
}
//main.js
let world = require('./world.js')
world;
function Hello {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function {
console.log('Hello ' + name);
};
};
module.exports = Hello;
而官方欽點的es6 modules與nodejs模塊系統極其相似,只是將其方法與對象變成關鍵字。
//test.js或test.mjs
import * as test from './test';
//aaa.js或aaa.mjs
import {aaa} from "./aaa"
const arr = [1, 2, 3, 4];
const obj = {
a: 0,
b: function {}
}
export const foo = => {
const a = 0;
const b = 20;
return a + b;
}
export default {
num,
arr,
obj,
foo
}
那怎么使用呢?根據規范,瀏覽器需要在link標簽與script標簽添加新的屬性或屬性值來支持這新特性。(詳見:https://www.jianshu.com/p/f7db50cf956f)
<link rel="modulepreload" href="lib.mjs">
<link rel="modulepreload" href="main.mjs">
<script type="module" src="main.mjs"></script>
<script nomodule src="fallback.js"></script>
但可惜的是,瀏覽器對模塊系統的支持是非常滯后,并且即便最新的瀏覽器支持了,我們還是免不了要兼容舊的瀏覽器。對此,我們只能奠出webpack這利器,它是前端工程化的集大成者,可以將我們的代碼通過各種loader/plugin打包成主流瀏覽器都認識的JavaScript語法,并以最原始的方式掛載進去。
異步機制的嬗變
在JavaScript沒有大規模應用前,用到異步的地方只有ajax請求與動畫,在請求結束與動畫結束時要做什么事,使用的辦法是經典的回調。
回調
由于javascript是單線程的,我們的方法是同步的,像下面這樣,一個個執行:
A;
B;
C;
而異步則是不可預測其觸發時機:
A;
// 在現在發送請求
ajax({
url: url,
data: {},
success:function(res){
// 在未來某個時刻執行
B(res)
}
})
C;
//執行順序:A -> C -> B
回調函數是主函數的后繼方法,基本上能保證,主函數執行后,它能在之后某個時刻被執行一次。但隨著功能的細分,在微信小程序或快應用中,它們拆分成三個,即一個方法跟著三個回調。
// https://doc.quickapp.cn/features/system/share.html
import share from '@system.share'
share.share({
type: 'text/html',
data: '<b>bold</b>',
success: function{},
fail: function{},
complete: function{}
})
在nodejs中,內置的異步方法都是使用一種叫Error-first回調模式。
fs.readFile('/foo.txt', function(err, data) {
// TODO: Error Handling Still Needed!
console.log(data);
});
在后端,由于存在IO操作,異步操作非常多,異步套異步很容易造成回調地獄。于是出現了另一種模式,事件中心,EventBus或EventEmiiter。
var EventEmitter = require('events').EventEmitter;
var ee = new EventEmitter;
ee.on('some_events', function(foo, bar) {
console.log("第1個監聽事件,參數foo=" + foo + ",bar="+bar );
});
console.log('第一輪');
ee.emit('some_events', 'Wilson', 'Zhong');
console.log('第二輪');
ee.emit('some_events', 'Wilson', 'Z');
事件可以一次綁定,多次觸發,并且可以將原來內部的回調拖出來,有效地避免了回調地獄。但事件中心,對于同一種行為,總是解發一種回調,不能像小程序的回調那么清晰。于是jQuery引進了Promise。
Promise
Promise最初叫Deffered,從Python的Twisted框架中引進過來。它通過異步方式完成用類的構建,又通過鏈式調用解決了回調地獄問題。
var p = new Promise(function(resolve, reject){
console.log("========")
setTimeout(function{
resolve(1)
},300)
setTimeout(function{
//reject與resolve只能二選一
reject(1)
},400)
});
console.log("這個先執行")
p.then(function (result) {
console.log('成功:' + result);
})
.catch(function (reason) {
console.log('失敗:' + reason);
}).finally(function{
console.log("總會執行")
})
為什么這么說呢?看上面的示例,new Promise(executor)
里的executor方法,它會待到then, catch, finally等方法添加完,才會執行,它是異步的。而then, catch, finally則又恰好對應success, fail, complete這三種回調,我們可以為Promise以鏈式方式添加多個then方法。
如果你不想寫catch,新銳的瀏覽器還提供了一個新事件做統一處理:
window.addEventListener('unhandledrejection', function(event) {
// the event object has two special properties:
alert(event.promise); // [object Promise] - 產生錯誤的 promise
alert(event.reason); // Error: Whoops! - 未處理的錯誤對象
});
new Promise(function {
throw new Error("Whoops!");
}); // 沒有 catch 處理錯誤
nodejs也有相同的事件:
process.on('unhandledRejection', (reason, promise) => {
console.log('未處理的拒絕:', promise, '原因:', reason);
// 記錄日志、拋出錯誤、或其他邏輯。
});
除此之外,esma2020年還為Promise添加了三個靜態方法:Promise.all和Promise.race,Promise.allSettled 。
其實chrome 60已經都可以用了。
Promise.all(iterable) 方法返回一個 Promise 實例,此實例在 iterable 參數內所有的 promise 都“完成(resolved)”或參數中不包含 promise 時回調完成(resolve);如果參數中 promise 有一個失敗(rejected),此實例回調失敗(reject),失敗原因的是第一個失敗 promise 的結果。
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
這個方法類似于jQuery.when,專門用于處理并發事務。
Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。此方法用于競態的情況。
Promise.allSettled(iterable)方法返回一個promise,該promise在所有給定的promise已被解析或被拒絕后解析,并且每個對象都描述每個promise的結果。它類似于Promise.all,但不會因為一個reject就會執行后繼回調,必須所有promise都被執行才會。
Promise不并比EventBus, 回調等優異,但是它給前端API提供了一個標杠,以后處理異步就是返回一個Promise。為后來async/await做了鋪墊。
生成器
生成器generator, 不是為解決異步問題而誕生的,只是恰好它的某個特性可以解耦異步的復雜性,加之koa的暴紅,人們發現原來generator還可以這樣用,于是就火了。
為了理解生成器的含義,我們需要先了解迭代器,迭代器中的迭代就是循環的意思。比如es5中的forEach, map, filter就是迭代器。
let numbers = [1, 2, 3];
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
//它比上面更精簡
numbers.forEach(function(el){
console.log(el);
})
但forEach會一下子把所有元素都遍歷出來,而我們喜歡一個個處理呢?那我們就要手寫一個迭代器。
function makeIterator(array){
var nextIndex = 0;
return {
next: function{
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
}
var it = makeIterator([1,2,3])
console.log(it.next); // {value: 1, done: false}
console.log(it.next); // {value: 2, done: false}
console.log(it.next); // {value: 3, done: false}
console.log(it.next); // {done: true}
而生成器則將創建迭代器常用的模式官方化,就像創建類一樣,但是它寫法有點怪,不像類那樣專門弄一個關鍵字,也沒有像Promise那樣弄一個類。
//理想中是這樣的
Iterator{
exector{
yield 1;
yield 2;
yield 3;
}
}
//現實是這樣的
function* Iterator {
yield 1;
yield 2;
yield 3;
}
其實最好是像Promise那樣,弄一個類,那么我們還可以用現成的語法來模擬,但生成器,現在一個新關鍵字yield,你可以將它當一個return語句。生成器執行后,會產生一個對象,它有一個next方法,next方法執行多少次,就輪到第幾個yield的值返回。
function* Iterator {
yield 1;
yield 2;
yield 3;
}
let it = Iterator;
console.log(it.next); // {value: 1, done: false}
console.log(it.next); // {value: 2, done: false}
console.log(it.next); // {value: 3, done: false}
console.log(it.next); // {value: undefined, done: true}
由于寫法比較離經背道,因此通常見于類庫框架,業務中很少有人使用。它涉及許多細節,比如說yield與return的混用。
function* generator {
yield 1;
return 2; //這個被轉換成 yield 2, 并立即設置成done: true
yield 3; //這個被忽略
}
let it = generator;
console.log(it.next); // {value: 1, done: false}
console.log(it.next); // {value: 2, done: true}
console.log(it.next); // {value: undefined, done: true}
image_1doda17jkj7kl4u1qru1era2m316.png-322.9kB
但說了這么多,這與異步有什么關系呢?我們之所以需要回調,事件,Promise這些,其實是希望能實現以同步代碼的方式組件異步邏輯。yield相當一個斷點,能中斷程序往下執行。于是異步的邏輯就可以這樣寫:
function* generator {
yield setTimeout(function{ console.log("111"), 200})
yield setTimeout(function{ console.log("222"), 100})
}
let it = generator;
console.log(it.next); // 1 視瀏覽器有所差異
console.log(it.next); // 2 視瀏覽器有所差異
如果沒有yield,肯定是先打出222,再打出111。
好了,我們搞定異步代碼以同步代碼的順序輸出后,就處理手動執行next方法的問題。這個也簡單,寫一個方法,用程序執行它們。
function timeout(data, time){
return new Promise(function(resolve){
setTimeout(function{
console.log(data, new Date - 0)
resolve(data)
},time)
})
}
function *generator{
let p1 = yield timeout(1, 2000)
console.log(p1)
let p2 = yield timeout(2, 3000)
console.log(p2)
let p3 = yield timeout(3, 2000)
console.log(p3)
return 2;
}
// 按順序輸出 1 2 3
/* 傳入要執行的gen */
/* 其實循環遍歷所有的yeild (函數的遞歸)
根絕next返回值中的done判斷是否執行到最后一個,
如果是最后一個則跳出去*/
function run(fn) {
var gen = fn;
function next(data) {
// 執行gen.next 初始data為undefined
var result = gen.next(data)
// 如果result.done 為true
if(result.done) {
return result.value
}else{
// result.value 為promise
result.value.then(val=>{
next(val)
})
}
}
// 調用上一個next方法
next;
}
run(generator)
koa早些年的版本依賴的co庫,就是基于上述原理擺平異步問題。有興趣的同學可以下來看看。
async/await
上節章的生成器已經完美地解決異步的邏輯以同步的代碼編寫的問題了,什么異常,可以直接try catch,成功則直接往下走,總是執行可以加finally語句,美中不足是需要對yield后的方法做些改造,改成Promise(這個也有庫,在nodejs直接內置了util.promisefy)。然后需要一個run方法,代替手動next。于是處于語言供應鏈上流的大佬們想,能不能直接將這兩步內置呢?然后包裝一個已經被人接受的語法提供給沒有見過世面的前端工程師呢?他們搜刮了一遍,還真有這東西。那就是C#有async/await。
//C# 代碼
public static async Task<int> AddAsync(int n, int m) {
int val = await Task.Run( => Add(n, m));
return val;
}
這種沒有學習成本的語法很快遷移到JS中,async關鍵字,相當于生成器函數與我們自造的執行函數,await關鍵字相當于yield,但它只有在它跟著的是Promise才會中斷流程執行。async函數最后會返回一個Promise,可以供外面的await關鍵字使用。
//javascript 代碼
async function addTask {
await new Promise(function(resolve){
setTimeout(function{ console.log("111"); resolve, 200})
})
console.log('222')
await new Promise(function(resolve){
setTimeout(function{ console.log("333"); resolve, 200})
})
console.log('444')
}
var p = addTask
console.log(p)
image_1dodd79nc1imnnm91q1b1p7qhdp1j.png-6.1kB
在循環中使用async/await:
const array = ["a","b", "c"]
function getNum(num){
return new Promise(function(resolve){
setTimeout(function{
resolve(num)
}, 300)
})
}
async function asyncLoop {
console.log("start")
for(let i = 0; i < array.length; i++){
const num = await getNum(array[i]);
console.log(num, new Date-0)
}
console.log("end")
}
asyncLoop
async函數里面的錯誤也可以用try catch包住,也可以使用上面提到的unhandledrejection方法。
async function addTask {
try{
await ...
console.log('222')
}catch(e){
console.log(e)
}
}
此外,es2018還添加了異步迭代器與異步生成器函數,讓我們處理各種異步場景更加得心應手:
//異步迭代器
const ruby = {
[Symbol.asyncIterator]: => {
const items = [`r`, `u`, `b`, `y`, `l`, `o`,`u`, `v`, `r`, `e`];
return {
next: => Promise.resolve({
done: items.length === 0,
value: items.shift
})
}
}
}
for await (const item of ruby) {
console.log(item)
}
//異步生成器函數,async函數與生成器函數的混合體
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine;
}
} finally {
await file.close;
}
}
塊級作用域的補完
說起作用域,大家一般認為JavaScript只有全局作用域與函數作用域,但是es3時代,它還是能通過catch語句與with語句創造塊級作用域的。
try{
var name = 'global' //全局作用域
}catch(e){
var b = "xxx"
console.log(b)//xxx
}
console.log(b)
var obj = {
name: "block"
}
with(obj) {
console.log(name);//Block塊上的name block
}
console.log(name)//global
但是catch語句執行后,還是會污染外面的作用域,并且catch是很耗性能的。而with更不用說了,會引起歧義,被es5嚴格模式禁止了。
話又說回來,之所以需要塊狀作用域,是用來解決es3的兩個不好的設計,一個是變量提升,一個重復定義,它們都不利于團隊協作與大規模生產。
var x = 1;
function rain{
alert( x ); //彈出 'undefined',而不是1
var x = 'rain-man';
alert( x ); //彈出 'rain-man'
}
rain;
因此到es6中,新添了let和const關鍵字來實現塊級作用域。這兩個關鍵字相比var,有如下特點:
作用域是局部,作用范圍是括起它的兩個花括號間,即for{}
,while{}
,if{}
與單純的{}
。
它也不會提升到作用域頂部,它頂部到定義的那一行變稱之為“暫時性死區”,這時使用它會報錯。
變量一旦變let, const聲明,就再不能重復定義,否則也報錯。這種嚴格的錯誤提示對我們調試是非常有幫助的。
let a = "hey I am outside";
if(true){
//此處存在暫時性死區
console.log(a);//Uncaught ReferenceError: a is not defined
let a = "hey I am inside";
}
//let與const不存在變量提升
console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // Uncaught ReferenceError: b is not defined
let a = 1; //Uncaught SyntaxError: Identifier 'a' has already been declared
const b = 2;
//不存在變量提升,因此塊級作用域外層無法訪問
if(true){
var bar = "bar";
let baz = "baz";
const qux = "qux";
}
console.log(bar);//bar
console.log(baz);//baz is not defined
console.log(qux);//qux is not defined
const聲明則比let聲明多了一個功能,就讓目標變量的值不能再次改變,即其他語言的常量。
基礎類型的增加
在javascript, 我們通過typeof與Object.prototype.toString.call可以區分出對象的類型,過去總有7種類型:undefined, , string, number, boolean, function, object。現在又多出兩個類型,一個是es6引進的Symbol,另一個是es2019的bBigInt。
console.log(typeof 9007199254740991n); // "bigint"
console.log(typeof Symbol("aaa")); // "symbol"
Symbol擁有三個特性,創建的值是獨一無二的,附加在對象是不可遍歷的,不支持隱式轉換。此外Symbol上面還有其他靜態方法,用來為對象擴展更多功能。
我們先看它如何表示獨一無二的屬性值。如果沒有Symbol,我們尋常表示常量的方法是不可靠的。
const COLOR_GREEN = 1
const COLOR_RED = 2
const LALALA = 1;
function isSafe(args) {
if (args === COLOR_RED) return false
if (args === COLOR_GREEN) return true
throw new Error(`非法的傳參: ${args}`)
}
console.log(isSafe(COLOR_GREEN)) //true
console.log(isSafe(COLOR_RED)) //false
console.log(isSafe(LALALA)) //true
如果是Symbol,則符合我們的預期:
const COLOR_GREEN = Symbol("1")//傳參可以是字符串,數字,布爾或不填
const COLOR_RED = Symbol("2")
const LALALA = Symbol("1")
function isSafe(args) {
if (args === COLOR_RED) return false
if (args === COLOR_GREEN) return true
throw new Error(`非法的傳參: ${args}`)
}
console.log(isSafe(COLOR_GREEN)) //true
console.log(isSafe(COLOR_RED)) //false
console.log(COLOR_GREEN == LALALA) //false
console.log(isSafe(LALALA)) //throw error
注意,Symbol不是一個構造器,不能new。
new Symbel("222")
會拋錯。
第二點,過往的對象屬性都是字符串類型,如果我們沒有用Object.defineProperty做處理,它們都能直接用for in
遍歷出來。而Symbol屬性不一樣,遍歷不出來,因此適用做對象的私有屬性,因為你只有知道它的名字,才能訪問到它。
var a = {
b: 11,
c: 22
}
var d = Symbol;
a[d] = 33
for(var i in a){
console.log(i, a[i]) //只有b,c
}
第三點,以往的數據類型都可以與字符串相加,變成一個字符串,或者減去一個數字,隱式轉換為數字;而Symbol則直接拋錯。
ar d = Symbol("11")
console.log(d - 1)
我們再來看它的靜態方法:
Symbol.for
這類似一個Symbol, 但是它不表示獨一無二的值,如果用Symbor.for創建了一個symbol, 下次再用相同的參數來訪問,是返回相同的symbol。
Symbol.for("foo"); // 創建一個 symbol 并放入 symbol 注冊表中,鍵為 "foo"
Symbol.for("foo"); // 從 symbol 注冊表中讀取鍵為"foo"的 symbol
Symbol.for("bar") === Symbol.for("bar"); // true,證明了上面說的
Symbol("bar") === Symbol("bar"); // false,Symbol 函數每次都會返回新的一個 symbol
var sym = Symbol.for("mario");
sym.toString;
上面例子是從火狐官方文檔拿出來的,提到注冊表這樣的東西,換言之,我們所有由Symbol.for創建的symbol都由一個內部對象所管理。
Symbol.keyFor
Symbol.keyFor方法返回一個已注冊的 symbol 類型值的key。key就是我們的傳參,也等于同于symbol的description屬性。
let s1 = Symbol.for("111");
console.log( Symbol.keyFor(s1) ) // "111"
console.log(s1.description) // "111"
let s2 = Symbol("222");
console.log( Symbol.keyFor(s2)) // undefined
console.log(s2.description) // "222"
let s3 = Symbol.for(111);
console.log( Symbol.keyFor(s3) ) // "111"
console.log(s3.description) // "111"
需要注意的是,Symbol.for為 Symbol 值登記的名字,是全局環境的,可以在不同的 iframe 或 service worker 中取到同一個值。
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('111') === Symbol.for('111')// true
Symbol.iterator
在es6中添加了for of
循環,相對于for in
循環,它是直接遍歷出值。究其原因,是因為數組原型上添加Symbol.iterator,它就是一個內置的迭代器,而for of
就是執行函數的語法。像數組,字符串,arguments, NodeList, TypeArray, Set, Map, WeakSet, WeatMap的原型都加上Symbol.iterator,因此都可以用for of
循環。
console.log(Symbol.iterator in new String('sss')) // 將簡單類型包裝成對象才能使用in
console.log(Symbol.iterator in [1,2,3] )
console.log(Symbol.iterator in new Set(['a','b','c','a']))
for(var i of "123"){
console.log(i) //1,2 3
}
但我們對普通對象進行for of
循環則遇到異常,需要我們自行添加。
Object.prototype[Symbol.iterator] = function {
var keys = Object.keys(this);
var index = 0;
return {
next: => {
var obj = {
value: this[keys[index]],
done: index+1 > keys.length
};
index++;
return obj;
}
};
};
var a = {
name:'ruby',
age:13,
home:"廣東"
}
for (var val of a) {
console.log(val);
}
Symbol.asyncIterator
Symbol.asyncIterator與for await of
循環一起使用,見上面異步一節。
Symbol.replace、search、split
這幾個靜態屬性都與正則有關,我們會發現這個方法名在字符串也有相同的臉孔,它們就是改變這些方法的行為,讓它們能接收一個對象,這些對象有相應的symbol保護方法。具體見下面例子:
class Search1 {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
console.log('foobar'.search(new Search1('bar')));
class Replace1 {
constructor(value) {
this.value = value;
}
[Symbol.replace](string) {
return `s/${string}/${this.value}/g`;
}
}
console.log('foo'.replace(new Replace1('bar')));
class Split1 {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
var index = string.indexOf(this.value);
return this.value + string.substr(0, index) + "/"
+ string.substr(index + this.value.length);
}
}
console.log('foobar'.split(new Split1('foo')));
Symbol.toStringTag
可以決定自定義類的 Object.prototype.toString.call的結果:
class ValidatorClass {
get [Symbol.toStringTag] {
return 'Validator';
}
}
console.log(Object.prototype.toString.call(new ValidatorClass));
// expected output: "[object Validator]"
此外,還有許多靜態屬性, 方便我們對語言的底層做更精致的制定,這里就不一一羅列了。
我們再看BigInt, 它就沒有這么復雜。早期JavaScript的整數范圍是2的53次方減一的正負數,如果超過這范圍,數值就不準確了。
console.log(1234567890123456789 * 123) //這顯然不對
因此我們非常需要這樣的數據類型,在它沒有出來前只能使用字符串來模擬。然后chrome67中,已經內置這種類型了。想使用它,可能直接在數字后加一個n,或者使用BigInt創建它。
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991);
// ? 9007199254740991n
const hugeString = BigInt("9007199254740991");
// ? 9007199254740991n
const hugeHex = BigInt("0x1fffffffffffff");
// ? 9007199254740991n
const hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111");
console.log(typeof hugeBin) //bigint
反射機制的完善
反射機制指的是程序在運行時能夠獲取自身的信息。例如一個對象能夠在運行時知道自己哪些屬性被執行了什么操作。
最先映入我們眼簾的是IE8帶來的get, set關鍵字。這就是其他語言的setter, getter。看似是一個屬性,其實是兩個方法。
var inner = 0;
var obj = {
set a(val){
console.log("set a ")
inner = val
},
get a{
console.log("get a ")
return inner +2
}
}
console.log(obj)
obj.a = 111
console.log(obj.a) // 113
image_1dojfhdi1vqbdqg1hr4mkt52h9.png-11.9kB
但在babel.js還沒有誕生的年代,新語法是很難生存的,因此IE8又搞了兩個類似的API,用來定義setter, getter:Object.defineProperty與Object.defineProperties。后者是前者的強化版。
var inner = 0;
var obj = {}
Object.defineProperty(obj, 'a', {
set:function(val){
console.log("set a ")
inner = val
},
get: function{
console.log("get a ")
return inner +2
}
})
console.log(obj)
obj.a = 111
console.log(obj.a) // 113
而標準瀏覽器怎么辦?IE8時代,firefox一方也有相應的私有實現:__defineGetter__
,__defineSetter__
,它們是掛在對象的原型鏈上。
var inner = 0;
var obj = {}
obj.__defineSetter__("a", function(val){
console.log("set a ")
inner = val
})
obj.__defineGetter__("a", function{
console.log("get a ")
return inner + 4
})
console.log(obj)
obj.a = 111
console.log(obj.a) // 115
在三大框架沒有崛起之前,是MVVM的狂歡時代,avalon等框架就是使用這些方法實現了MVVM中的VM。
setter與getter是IE停滯十多年瀦中添加的一個重要特性,讓JavaScript變得現代化,也更加魔幻。
但它們只能監聽對象屬性的賦值取值,如果一個對象開始沒有定義,后來添加就監聽不到;我們刪除一個對象屬性也監聽不到;我們對數組push進一個元素也監聽不到,對某個類進行實例化也監聽不到……總之,局b限還是很大的。于是chrome某個版本添加了Object.observe,支持異步監聽對象的各種舉動(如"add", "update", "delete", "reconfigure", "setPrototype", "preventExtensions"),但是其他瀏覽器不支持,于是esma委員會又合計搞了另一個逆天的東西Proxy。
Proxy
這個是es6大名鼎鼎的魔術代理對象,與Object.defineProperty一樣,無法以舊有方法來模擬它。
下面是它的用法,其攔截器所代表的操作:
let p = new Proxy({}, {//攔截對象,上面有如下攔截器
get: function(target, name){
// obj.aaa
},
set: function(target, name, value){
// obj.aaa = bbb
},
construct: function(target, args) {
//new
},
apply: function(target, thisArg, args) {
//執行某個方法
},
defineProperty: function (target, name, descriptor) {
// Object.defineProperty
},
deleteProperty: function (target, name) {
//delete
},
has: function (target, name) {
// in
},
ownKeys: function (target, name) {
// Object.getOwnPropertyNames
// Object.getOwnPropertySymbols
// Object.keys Reflect.ownKeys
},
isExtensible: function(target) {
// Object.isExtensible。
},
preventExtensions: function(target) {
// Object.preventExtensions
},
getOwnPropertyDescriptor: function(target, prop) {
// Object.getOwnPropertyDescriptor
},
getPrototypeOf: function(target){
// Object.getPrototypeOf,
// Reflect.getPrototypeOf,
// __proto__
// Object.prototype.isPrototypeOf與instanceof
},
setPrototypeOf: function(target, prototype) {
// Object.setPrototypeOf.
}
});
這個對象在vue3, mobx中被大量使用。
Reflect
Reflect與Proxy一同推出,Reflect上的方法與Proxy的攔截器同名,用于一些Object.xxx操作與in, new , delete等關鍵字的操作(這時只是將它們變成函數方式)。換言之,Proxy是接活的,Reflect是干活的,火狐官網的示例也體現這一點。
var p = new Proxy({
a: 11
}, {
deleteProperty: function (target, name) {
console.log(arguments)
return Reflect.deleteProperty(target, name)
}
})
delete p.a
它們與Object.xxx最大的區別是,它們都有返回結果, 并且傳參錯誤不會報錯(如Object.defineProperty)。可能官方認為將這些元操作方法放到Object上有點不妥,于是推出了Reflect。
Reflect總共有13個靜態方法:
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
更順手的語法糖
除了添加這些方法外,JavaScript底層的parser也大動手術,讓它支持更多語法糖。語法糖都可以寫成對應的函數,但不方便。總的來說,語法糖是想讓大家的代碼更加精簡。
新近添加如下語法糖:
對象簡寫,參看類的組織形式
擴展運算符(…
),用于對象的淺拷貝
箭頭函數,省略function關鍵字,與數學公式走近,能綁定this與略去return
for of(遍歷可迭代對象的所有值, for in是遍歷對象的鍵或索引)
數字格式化, 如1_222_333
字符串模板化與天然多行支持,如hello ${world}
冪運算符, **
可選鏈,let x = foo?.bar.baz;
空值合并運算符, let x = foo ?? bar;
函數的默認參數
總結
ECMAScript正在快速發展,經常會有新特性被引入,有興趣可以查詢babel的語法插件(https://www.babeljs.cn/docs/plugins),了解更詳細的用法。相信有了這些新特征的支持,大家再也不敢看小JavaScript了。
作者簡介:司徒正美,擁有十年純前端經驗,著有《JavaScript框架設計》一書,去哪兒網公共技術部前端架構師。愛好開源,擁有mass、Avalon、nanachi等前端框架。目前在主導公司的小程序、快應用的研發項目。
【END】
*請認真填寫需求信息,我們會在24小時內與您取得聯系。