全棧必備JavaScript基礎
1995年,誕生了JavaScript語言,那一年,我剛剛從大學畢業(yè)。在今年RedMonk 推出的2017 年第一季度編程語言排行榜中,JavaScript 排第一,Java 第二,Python 反超 PHP 排第三,PHP 第四,C# 和 C++ 并列第五。RedMonk 排名的主要依舊是各種編程語言在 Stack Overflow 和 GitHub 上的表現(xiàn),比如編程語言在 Stack Overflow 上的討論數(shù)量,在 GitHub 上的代碼量等。盡管有一定的片面性,還是說明了JavaScript 應用的廣泛性。從全棧的角度看,Javascript 是必備的一種編程語言。
ECMAScript 和 JavaScript 的關系
JavaScript 誕生于Netscape,但是在1996年,微軟發(fā)布了與JavaScript 兼容的JScript,面對兼容和發(fā)展的需要,網(wǎng)景公司的先賢們努力加入了 ECMA International 標準化組織,致力于JavaScript 的標準化,命名為ECMAScript。后來,由于歷史的原因, JavaScript標準的開發(fā)主體變成了Mozila基金會。
簡單地,ECMAScript 是JavaScript語言的標準規(guī)范,就像C++的標準相對于C++語言那樣。
JavaScript 是怎樣的語言
在mozilla 開發(fā)者網(wǎng)站上是這樣描述JavaScript的:
JavaScript (JS) is a lightweight interpreted or JIT-compiled programming language with first-class functions.
意思是說JavaScript 是一個輕量級解釋或即時編譯的函數(shù)式語言,里面有很多的概念,輕量、解釋、編譯、即時編譯、函數(shù)式。在老碼農(nóng)看來,簡單起見,理解為擴展語言較為方便。
一般的編程語言都有著自己相對獨立的執(zhí)行環(huán)境,但是JavaScript的執(zhí)行環(huán)境依賴在宿主環(huán)境中,宿主環(huán)境尤其是客戶端的宿主環(huán)境提供了更多統(tǒng)一的環(huán)境變量,比如瀏覽器中的window,document等。實際上,JavaScript 和DOM 是可分的,對于不同的運行環(huán)境,有著不同的內(nèi)置宿主對象。JavaScript作為擴展語言在內(nèi)置的宿主環(huán)境中運行,全局對象在程序啟動前就已經(jīng)存在了。
JavaScript的時空基礎
從空間觀的角度看,JavaScript包括數(shù)據(jù)結構,操作符,語句與表達式,函數(shù);從時間的角度看,包括作用域,處理方式,模塊與庫。
數(shù)據(jù)結構
JavaScript 中包含的六種基本類型:
- Boolean
- Null
- Undefined
- Number
- String
- Symbol (ECMAScript 6)
其它全是對象。值是有類型的,變量是沒有類型的,類型定義了值的行為特征,變量在沒有持有值的時候是undefined。 JavaScript對值和引用的賦值/傳遞在語法上沒有區(qū)別,完全根據(jù)值的類型來判定。
對于對象的屬性和方法而言,全局變量和全局函數(shù)是全局對象的屬性,全局對象相當于宿主對象的根對象。需要注意是屬性的屬性中那些不可變對象的實現(xiàn)方式:
- 對象常量: 結合writable和configurable:false 可以創(chuàng)建一個真正的常量屬性
- 禁止擴張:Object.preventExtensions(..)來禁止一個對象添加新屬性并保留已有屬性
- 密封: 在 Object.seal(..) 后不能增,刪,改 該屬性
- 凍結: Object.freeze(..) 會禁止對于對象本身及任意直接屬性的修改
數(shù)據(jù)類型的判定可以通過 contructor,instanceof, isPrototypeOf等方法實現(xiàn),對于鴨子類型的判定還可以使用 in 的相關操作。符號并非對象,而是一種簡單標量基本類型。
JavaScript 中的強制類型轉換總是返回基本類型值,將對象強制轉換為String 是通過ToPrimitive抽象操作完成的,而toJSON()是返回一個能夠被字符串化的安全的JSON值。
操作符
操作符是空間元素連接的紐帶之一,JavaScript操作符包括算術,連接,相等,比較,邏輯,位,類型判斷,條件,new,delete, void,",", ".", "[]"等。
在JavaScript中以操作符進行操作往往都附帶著類型轉換。
一元運算符+ 是顯式強制類型轉換,而~是先轉換為32位數(shù)字,然后按位反轉。|| 和&& 更應該算是選擇器運算符,其返回值不一定是布爾值,而是兩個操作數(shù)其中的一個值。一般先對第一個操作數(shù)進行toBoolean強制類型轉換,然后再執(zhí)行條件判斷。例如:a||b 理解成a?a:b 更通俗。對&& 而言,如果第一個是真值,則把第二個作為返回值,a&&b 理解成a?b:a 。
== 和=== 都會對操作數(shù)進行類型檢查,并執(zhí)行隱性類型轉換,需要注意的是:
- 如果兩邊的值中有true或false,千萬不要使用==
- 如果兩邊有[],””或者0,盡量不要使用==
這里是Github上關于各種相等性的矩陣:
語句與表達式
操作符與變量/常量等連接形成了語句和表達式,例如表達式a+1中的null 被強制轉換為0。 語句包括聲明與塊,控制語句有判斷,循環(huán),break,continue,return,異常等。每個語句都有一個結果值,哪怕是undefined。
正則表達式是非常重要的一類表達式,主要使用RegExp類,執(zhí)行方法test效率高,exec 會得到一個結果對象的數(shù)組。
逗號運算符可以把多個獨立的表達式串聯(lián)成一個語句,{ }在不同情況下的意思不盡相同,作為語句塊,{ ..} 和for/while循環(huán)以及if條件語句中代碼塊的作用基本相同。{a,b} 實際上是{a:a,b:b}的簡化版本。
try..catch..finally 中,如果finally中拋出異常,函數(shù)會在此處終止。需要注意的是,如果此前try中已經(jīng)有return設置了返回值,則該值會被丟棄。finally中的return也會覆蓋try和catch中的return的返回值。
函數(shù)與作用域
函數(shù)就是具有運算邏輯的對象,匿名函數(shù)不利于調(diào)試,回調(diào)函數(shù)是一種控制反轉。所有的函數(shù)(對象)都具有名為prototype的屬性,prototype屬性引用的對象是prototype對象;所有的對象都含有一個隱式鏈接,用以指向在對象生成過程中所使用的構造函數(shù)的prototype對象。
匿名函數(shù)沒有name 標識符,具有如下缺陷:
- 代碼更難理解
- 調(diào)試棧更難追蹤
- 自我引用(遞歸,事件(解除)綁定,等)更難
如果function是聲明的第一個詞,那就是函數(shù)聲明,否則就是函數(shù)表達式。立即執(zhí)行函數(shù)表達式形如:(function …)( )
時空密不可分,作用域是時空連接的紐帶之一。作用域包括全局,函數(shù),塊級作用域。作用域是根據(jù)名稱查找變量的一套規(guī)則,遍歷嵌套作用域鏈的規(guī)則簡單:引擎從當前執(zhí)行作用域逐級向上查找。閉包可以理解為具有狀態(tài)的函數(shù)。
函數(shù)作用域指屬于這個函數(shù)的全部變量都可以在整個函數(shù)的范圍內(nèi)使用或復用。塊作用域形如 with, try/catch, ES6 引入了let,const等。
動態(tài)作用域并不關心函數(shù)和作用域是如何聲明以及在何處聲明的,只關心它們從何處調(diào)用的。詞法作用域是定義在詞法分析階段的作用域,詞法作用域查找會在第一個匹配的標識符時停止。作用域鏈是基于調(diào)用棧的,而不是代碼中的作用域嵌套。ReferenceError 是與作用域判別失敗相關,而TypeError則是作用域判別成功,但是對結果的操作非法或不合理。
this 提供了一種優(yōu)雅方式來隱式“傳遞”一個對象引用。 this 即沒有指向函數(shù)的自身,也沒有指向函數(shù)的作用域,是在函數(shù)被調(diào)用時發(fā)生的綁定,它指向什么完全取決于函數(shù)在哪里被調(diào)用。如果分析this綁定的話,可以使用調(diào)試工具得到調(diào)用棧,然后找到棧中的第二個元素,就是真正的調(diào)用位置。
this 的綁定規(guī)則有:
- 默認綁定:獨立的函數(shù)調(diào)用,嚴格模式不能將全局對象用于默認綁定
- 隱式綁定:把函數(shù)調(diào)用中的this 綁定到函數(shù)引用中的上下文對象
- 顯式綁定:通過call()和apply()方法可以直接指定this的綁定對象。其中,硬綁定是一種顯式的強制綁定,ES5中提供了內(nèi)置方法Function.prototype.bind, API中調(diào)用的上下文和bind的作用一樣。
- new 綁定,構造函數(shù)只是一些使用new操作符調(diào)用的函, 使用new 來調(diào)用函數(shù)的操作過程大致如下:
- 創(chuàng)建一個全新的對象
- 這個新對象會被執(zhí)行[[Prototype]]鏈接
- 這個新對象會綁定到函數(shù)調(diào)用的this
- 如果函數(shù)沒有返回其他對象,那么new表達式中的函數(shù)調(diào)用會自動返回這個新對象
如果同時存在多種綁定,那么綁定的優(yōu)先級大致如下:
- 由new調(diào)用綁定到新創(chuàng)建的對象
- 由call 或者apply(或bind)調(diào)用綁定到指定的對象
- 由上下文對象調(diào)用綁定到那個上下文對象
- 默認在在嚴格模式下綁定到undefined,否則綁定到全局對象
更安全地使用this 綁定的做法是傳入一個特殊的對象,把this 綁定到這個對象。需要注意的是,箭頭函數(shù)不使用this的4種規(guī)則,而是根據(jù)外層(函數(shù)或全局)作用域來決定this。
還要注意一點,eval 和 with 會導致作用域變化而引起性能下降,盡量不要使用。eval() 函數(shù)中的字符串是代碼,用來執(zhí)行動態(tài)創(chuàng)建的代碼,嚴格模式有自己的作用域,還存在安全隱患;with 是重復引用一個對象中的多個屬性的快捷方式,通過將一個對象的引用當作作用域來處理,會改變作用域范圍。
處理和執(zhí)行方式
JavaScript引擎本身沒有時間概念,只是一個按需執(zhí)行任意代碼片段的環(huán)境,事件調(diào)度總是由包含它的宿主環(huán)境來執(zhí)行。一旦有事件需要運行,事件循環(huán)隊列就會運行,直到隊列清空,用戶交互、IO和定時器等事件源會向事件隊列加入事件。
由于JavaScript的單線程特性,很多函數(shù)的代碼具有原子性。
回調(diào)函數(shù)封裝了程序的延續(xù)性,常見設計是分離回調(diào)(一個用于成功通知,一個用于出錯通知)。另一種回調(diào)模式是“error-first”,可能受到防御式編程的影響,NodeJS API 采用了此類的風格,如果成功的話,這個參數(shù)就會被清空。需要注意的是,回調(diào)函數(shù)的嵌套往往稱為回調(diào)地獄。
Deferred是一種將異步處理串聯(lián)書寫并執(zhí)行的機制,Deferred對象是一種具有unresolved,resolved,rejected 中某一種狀態(tài)的對象。Deferred內(nèi)部機制是先注冊回調(diào)函數(shù),Deferred對象狀態(tài)發(fā)生變化時執(zhí)行該函數(shù),是一種提高代碼可讀性的機制。
Deferred對象的狀態(tài)遷移只能發(fā)生一次,以then(),done(),fail(),always(),pipe()指定后續(xù)函數(shù)的方法,通過when()來并行處理,將Deferred 對象中的一部分方法刪除后得到是Promise對象,對狀態(tài)的管理由最初創(chuàng)建該Deferred對象的所有者來執(zhí)行。
Promise 封裝了依賴于時間的狀態(tài),從而使得本身與時間無關,Promise 可以按照可預測的方式進行,而不用關心時序或底層的結果。一旦Promise決議完成,就成為了不變值,可以安全地吧這個值傳遞給第三方,并確保不會改變。
Promise 是一種在異步任務中作為兩個或更多步驟的流程控制機制,時序上的this-then-that。 不僅表達了多步異步序列的流程控制,還是一個從一個步驟到下一個步驟傳遞消息的消息通道。事件監(jiān)聽對象可以當成是對promise 的一種模擬,對控制反轉的恢復實現(xiàn)了更好的關注點分離。
判斷是否是Promise 值的示例代碼如下:
- if(
- p !==null &&
- ( typeof p ===“object” || typeof p ===“function”) && typeof p.then===“function”)
- {
- console.log(“thenable”);
- }
- else{
- console.log(“not thenable”);
- }
生成器是一類特殊的函數(shù),可以一次或多次啟動和停止,并不非的一定要完成,生成器把while true 帶回了Javascript的世界。其中,yield 委托的主要目的是代碼組織,以達到與普通函數(shù)調(diào)用的對稱。從生成器yield出一個Promise, 并且讓這個Promise 通過一個輔助函數(shù)恢復這個生成器,這是通過生成器管理異步的好方法之一。
需要注意的是,如果在Promise.all([..]) 中傳入空數(shù)組,會立即完成, 而Promise.race([..]) 則會掛住。 在各種Promise庫中,finally ( .. ) 還是會創(chuàng)建并返回一個新Promise的。
模塊與庫
模塊和庫是JavaScript 時空中的另一紐帶,提高了代碼的復用性和開發(fā)效率。
模塊充分利用了閉包的強大能力,從模塊中返回一個實際的對象并不是必須的,也可以直接返回一個內(nèi)部函數(shù),例如:jQauery 和 $標識符就是jQuery 模塊的公共API。
模塊有兩個必要條件:
- 必須有外部的封閉函數(shù),該函數(shù)必須至少被調(diào)用一次
- 封閉函數(shù)必須返回至少一個內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或修改私有的狀態(tài)
import 可以將一個模塊的一個或多個API導入到當前作用域中,并分別綁定在一個變量上;module 則將整個模塊的API 導入并綁定到一個變量上, export 將當前模塊的一個標識符導出為公共API。
大多數(shù)模塊所依賴的加載器/管理器本質(zhì)上是將這種模塊定義封裝進一個API。基于函數(shù)的模塊并不是一個能被靜態(tài)識別的模式(編譯器),API定義只有在運行時考慮進來。但是ES6 模塊的API 是靜態(tài)的,必須被定義在獨立的文件中。
JavaScript 中的庫浩如煙海,這里僅對JQuery做簡要說明。JQuery壓縮后大約31k,輕巧靈活,通過鏈式語法實現(xiàn)邏輯功能,通過CSS3選擇器及自定義選擇器獲取元素,支持插件,可擴展性高。
JQuery中 的特色函數(shù)——$ ,可以抽取與選擇器匹配的元素,或者創(chuàng)建新的DOM元素,將已有的DOM元素轉換為jQuery對象,對DOM構造完成后的事件監(jiān)聽器進行設定等等。JQuery 對DOM,樣式,AJAX 均可有效處理。
通過擴展JQuery.fn 就可以創(chuàng)建JQuery的插件,code.google.com/apis/libraries 給出了很多JQuery 的插件信息。
利用JavaScript 的時空觀,可以對這一語言有一些基本的梳理。就語言本身而言,關鍵字是不能回避的,對JavaScript 關鍵字,在StackOverFlow中有人給出了如下詩一樣的總結:
- Let this long package float,
- Goto private class if short。
- While protected with debug case,
- Continue volatile interface。
- Instanceof super synchronized throw,
- Extends final export throws.
- Try import double enum?
- -False, boolean, abstract function.
- Implements typeof transient break!
- Void static,default do,
- Switch int native new,
- else, delete null public var,
- In return for const, true, char,
- …… finally catch byte.
客戶端應用
一門語言所被使用的廣泛程度取決于使用的場景,正如PHP被廣泛采用那樣,互聯(lián)網(wǎng)應用不僅是JavaScript 的家鄉(xiāng),而且是它大展身手的最重要場所,沒有JavaScript 的Web應用幾乎絕跡了。
web應用中使用JavaScript有拖拽操作,異步讀取,鍵盤訪問 和動畫效果等基本功能。對于清晰地使用JavaScript實現(xiàn)Web應用而言,理解瀏覽器網(wǎng)頁處理過程是必要的。一般地,瀏覽器先分析HTML,然后構造DOM樹,再載入外部Javascript 文件以及CSS文件,接下來載入圖像文件等外部資源,最后在分析Javascript后開始執(zhí)行至全部完成。
在HTML中加載JavaScript的方式有多種:
- <script> 標簽,在body 結束標簽前寫
- 讀取外部JavaScript 文件,讀取完就開始執(zhí)行,瀏覽器可以緩存
- onload 事件加載
- DOMContentLoaded是在完成HTML解析后發(fā)生的事件,也可以用于加載JavaScript
- 動態(tài)載入,這樣JS在載入時不會阻斷其他操作,如
- var script = document.createElement(‘script’);
- script.src = ‘my-javascript.js’;
- document.getElementsByTagName(‘head’)[0].appendChild(script)
window對象是JavaScript所能操作的最高層對象,其中的屬性包括navigator,location,history,screen,frames,document,parent,top,self 等。
DOM 是一種API,完成對HTML/XML 的樹形結構訪問,如標簽,元素,節(jié)點等。節(jié)點可以通過ID,標簽名,名稱和類名進行檢索,例如:
- var element = document.getElementById(“abel”)
- var allelements = document.getElementByTagName(‘*’)
由于返回的是NodeList對象,性能較差,可以通過 var array = Array.prototye.slice.call(allelements)轉換為array 后處理。節(jié)點的訪問可以通過XPath 進行靈活的訪問,當然,Selector API 比XPath更簡單且同樣靈活,例如:
- var a_label = document.querySelector(‘#abel’)
- var b_all = document.querySelectorAll(‘div’)
如果先修改DocumentFragment,再對實際的document對象操作,DOM 的操作性能會較高一些。
事件偵聽器的設定可以制定HTML元素的屬性,也可以指定DOM元素的屬性,還可以通過EventTarget.addEventListenser()進行指定。事件的處理包括捕獲,目標處理和事件冒泡三個階段,捕獲的過程是:
- window -> document -> html -> body -> div -> button
然后處理器執(zhí)行,冒泡向上傳播的過程是遍歷DOM樹,需要注意的是 focus 不會冒泡。
DOM2中的標準事件有HTMLEvent,MouseEvent,UIEvent和MutationEvent。DOM3 中的事件更多:UIEvent,F(xiàn)ocusEvent,MouseEvent, WheelEvent, TextEvent,KeyboardEvent 和compositionEvent等,還可以通document.createEvent來自定義事件。
通過JavaScript 對CSS樣式變更的方法有通過className 屬性變更class名,通過classList屬性更改class名(其中classList 是H5對DOM TokenList接口的實現(xiàn)),還可以更改Style 屬性或者直接更改樣式表。通過JavaScript可以對屏幕位置(screenX,screenY),窗口位置(clientX,clientY),文檔坐標(pageX,pageY,由瀏覽器自行實現(xiàn)的),特定元素內(nèi)的相對位置(layerX,layerY 或offsetX offsetY)進行修改。通過JavaScript可以對表單中的元素,控件和內(nèi)容進行驗證,可用于驗證的事件有submit,focus,blur,change,keydown/up/press,input。使用表單而不產(chǎn)生頁面跳轉的方式可以是指向到一個 (0,0 )的空iframe。
對于動畫而言,css的動畫性能一般要更好一些。
AJAX 在Web應用中是不可或缺的,簡單地說,是一種不發(fā)生頁面跳轉就能異步載入內(nèi)容并改寫頁面內(nèi)容的技術,主要通過 XMLHttpRequest 對象的創(chuàng)建,實現(xiàn)通/異步通信,處理超時和響應。AJAX有著跨源限制,實現(xiàn)跨源通信的方式有:JSONP,iframe hack,window.postMessage() 以及 XMLHttpRequest Level 2。
HTML5+CSS3+JavaScript的綜合使用才可能成就一個Web應用。H5中的 History API 使用了window屬性的history對象監(jiān)聽popstate事件,用于恢復頁面狀態(tài)的處理。ApplicationCache 在html標簽的manifest 屬性中指定了緩存清單文件的路徑,必須通過text/cache-manifest 這一MIME type 來發(fā)布緩存清單文件,注意清單中的CACHE,NETWORK,和FALLBACK 的區(qū)分。
通過navigator.onLine 可以獲知網(wǎng)絡狀態(tài),還可以通過online/offline事件來偵聽連接狀態(tài)的切換時機。online/offline事件是document.body 觸發(fā)的,并傳給document對象和window對象。
- <p> network is : <span id = “indicator”> (state unknown) </span> </p>
- <script>
- {
- function updateIndicator = document.getElementById(‘indicator’);
- indicator.textContext = navigator.online?’online’:’offline’;
- }
- document.body.onload = updateIndicator;
- document.body.ononline= updateIndicator;
- document.body.onoffline = updateIndicator;
- </script>
DataTransfer 是Drag Drop API 的核心,在所有拖拽事件的事件對象中,都有該屬性,主要是接收數(shù)據(jù)。拖拽文件從瀏覽器保存到桌面:event.dataTransfer.setData(‘DownloadURL’,’MIMETYPE: 文件url’)例如:
- <a href=“http://a.b.c/abel.pdf”
- data-downloadurl = “application/pdf:abel.pdf:http://a.b.c/abel.pdf”
- class=“dragout” draggable = “true”>download </a>
- <script>
- var files = document.querySelectorAll(‘.dragout’);
- for (var i = 0,file; file =files[i];i++) {
- file.addEventListener(‘dragstart’,function(event){
- event.dataTransfer.setData(“DownloadURL”,this.dataset.downloadurl);
- },false);
- }
- </script>
FileAPI 通過FileReader 讀取文件,也可以讀取dataURL,F(xiàn)ileReaderSync 用于同步讀取文件內(nèi)容,可以在Web Worker 中使用。
Web Storage 為所有源共享5M空間,localStorage 和sessionStorage 的區(qū)別在于數(shù)據(jù)的生命周期。cookie 最大4k,發(fā)請求時一起發(fā)送,保存會話等重要信息。indexedDB 可以歸為文檔型數(shù)據(jù)庫, 作為客戶端存儲又一選擇。
- var indexdb = window.indexDB||window.webkitIndexedDB||window.mozIndexedDB;
Web worker 是H5 的新特性,是宿主環(huán)境(瀏覽器)的功能,JavaScript 本身是不支持多線程的。專用的worker 與創(chuàng)建它的程序之間是一對一的關系。
Web worker 能在另外的線程中創(chuàng)建新的Javascript 運行環(huán)境,使JavaScripts可以在后臺處理。主線程和工作線程分離,無法使用對方環(huán)境的變量。工作線程無法引用document對象,需要通過消息收發(fā)完成數(shù)據(jù)傳遞。 在主線程創(chuàng)建工作線程,大約向var worker = new Worker(‘work.js’)這樣 在主線程中停止worker的方式是worker.terminate(); worker 自身停止的方式是 self.close();worker 中 可以通個 importScripts 方法,在工作線程內(nèi)讀取外部的文件。
了解了這些基礎方式和方法,僅僅是Web應用中JavaScript開發(fā)的第一步吧。
服務端應用
技術系統(tǒng)總是又著向超系統(tǒng)進化的趨勢,JavaScript 也不例外。
JavaScript 應用于服務端的開發(fā)源于2009年初出現(xiàn)的CommonJS,后來成為為了服務器端javaScript的規(guī)范?;贘avaScript沒有模塊系統(tǒng)、標準庫較少、缺乏包管理工具等現(xiàn)狀,CommonJS規(guī)范希望JavaScript可以在任何地方運行,以達到Java、C#、PHP這些后臺語言具備開發(fā)大型應用的能力。CommonJS是一種思想,它的終極目標是使應用程序開發(fā)者根據(jù)CommonJS API編寫的JavaScript應用可以在不同的JavaScript解析器和HOST環(huán)境上運行,例如編寫服務端應用,命令行工具,基于GUI的桌面應用和混合應用編程等,詳情參加 www.commonjs.org 。
NodeJS可以理解成CommonJS規(guī)范的一種實現(xiàn),而且是部分實現(xiàn)。NodeJS以V8作為JavaScript的實現(xiàn)引擎,通用的異步處理事件循環(huán),提供了一系列非阻塞函數(shù)庫來支持實踐循環(huán)特性。同時,NodeJS提供了高度優(yōu)化的應用庫,來提高服務器效率,例如其http 模塊是為快速非阻塞式http服務而用C語言重寫的。另外,NodeJS還有shell的命令行工具,通過包系統(tǒng)實現(xiàn)擴展,擴展列表可以詳情參見: GitHub.com/node/wiki/modules。
JavaScript 中的主要實現(xiàn)引擎包括:IE采用的JScript,F(xiàn)irefox采用的SpiderMoneky,Chrome 采用的V8,Safari采用的webkit中的 javacriptcore燈。如果要對引擎有進一步的了解,可以研讀一下javascriptcore等相關的源代碼。
V8 是NodeJS 中的核心引擎,NodeJS的系統(tǒng)架構大致如下:
與瀏覽器相對應,Node 中的全局變量可以通過 Object.keys(global); 獲得, 看一看NodeJS中的 “hello world” 程序:
- var http = require('http');
- http.createServer(function (req,res){
- res.writeHead(200,{'Content-type':'text/plain'});
- res.end('Hello Node.js \n');
- }).listen(1234,"127.0.0.1");
- console.log('Server running on http://127.0.0.1:1234/');
幾行代碼就實現(xiàn)一個簡單web server, 使Pythoner 們聯(lián)想到了 Tornado, 它們都走在單線程異步IO的路上。
NodeJS 提供了對https 的支持,可以通過openssl 生成證書的方式大致是:
- openssl req -new -x509 -keyout key.pen -out cert.perm
使用證書的示例如下:
- var fs =require(‘fs’);
- var options = {
- key: fs.readFileSync(‘key.perm’);
- cert:fs.readFileSync(‘cert.perm’);
- }
NodeJS支持socket 和文件處理,配合系統(tǒng)擴展可以使用各種模版語言?;贜odeJS的實際在業(yè)界非常廣泛,比如面向websocket的IM系統(tǒng),各種web應用網(wǎng)站等等。
鑒于微服務架構的興起,也誕生了基于Node的微服務架構——Seneca,它使用完備的模式匹配接口來連接各個服務,從代碼中將數(shù)據(jù)傳輸抽象出來,使編寫具有高擴展性的軟件變得相當容易。Seneca 沒有使用依賴注入,但是在處理控制反轉上相當靈活,沒有關鍵字和強制的字段,只需一組鍵值對,用于模式匹配的引擎中。具體參考實現(xiàn),可以參考《node.js 微服務》一書。
基于JavaScript的全棧
如果在整個應用系統(tǒng)中主要使用JavaScript編程語言作為技術棧,那么也可以成為基于JavaScript 的全棧,關于全棧的論述可以參加《全棧的技術棧設想》和《再談< 全棧架構師>》兩篇文字。例如MEAN架構,即MongoDB + Express + Angular + Node,MEAN 技術棧代表著一種完全現(xiàn)代的 Web 開發(fā)方法:一種語言運行在應用程序的所有層次上,從客戶端到服務器,再到持久層。借助JavaScript的測試框架,比如MochaJS、JasmineJS 和 KarmaJS,可以為自己的 MEAN 應用程序編寫深入而又全面的測試套件,據(jù)說MEAN有取代LAMP/LNMP的的趨勢,但還需保持謹慎。
引擎的差異
正像Java 那樣,盡管又著虛擬機規(guī)范,但各個JVM的實現(xiàn)還是有著些許的不同,JavaScript 也是如此。JavaScript各引擎中同樣存在著少量的限制,例如:
- 字符串常量中允許的最大字符數(shù)
- 作為參數(shù)傳遞到函數(shù)中的數(shù)據(jù)大小(棧大小)
- 函數(shù)聲明中的參數(shù)個數(shù)
- 函數(shù)調(diào)用鏈的最大長度
- 以阻塞方式在瀏覽器中運行的最大時間
- 變量名的最大長度
- 盡管如此,JavaScript 在瀏覽器中的表現(xiàn)還是基本上可信的。
從軟件到硬件
實際上,JavaScript已經(jīng)嵌入到了從機器人到各種家電等各種各樣的設備中。這里隆重推薦我非常敬佩的好友——周愛民老師,他在Ruff(南潮信息科技)做的事情就是JavaScript 在物聯(lián)網(wǎng)上的進一步應用。
Ruff 是一個可以讓開發(fā)者實現(xiàn)敏捷開發(fā)智能硬件的系統(tǒng)平臺。它包含了Ruff SDK、Ruff OS,Rap Registry等。從技術上講,Ruff 是一個 JavaScript 運行時,專為硬件開發(fā)而設計。Ruff 對硬件進行了抽象,使用了基于事件驅(qū)動、異步 I/O 的模型,使硬件開發(fā)變得輕量而且高效。硬件抽象層,使得操作硬件猶如普通程序庫,降低了硬件領域進入門檻。
Ruff 為開發(fā)者提供了完善的開發(fā)服務。從項目生產(chǎn)、軟件包管理、應用管理、外設管理到固件管理等一系列現(xiàn)代軟件開發(fā)方式,PC 端完成開發(fā),無需燒板子,提升開發(fā)者的開發(fā)效率。Ruff 還提供了完善的測試框架,支持 assert、test、mock 等模塊,在開發(fā)機上測試邏輯,硬件測試也能 TDD。
官網(wǎng)(ruff.io) 上給出的示例如下:
- $.ready(function() {
- $('#led-0').turnOn();
- });
打開電路板上的一個LED 燈,就是如此的簡單。目前,一個 Ruff 硬件同時只能運行一款 Ruff 應用,它將擁有自己獨立的進程,著可能也受到JavaScript自身的限制吧。
關注性能
性能是全棧關注的一個重要維度,那句“過早優(yōu)化是萬惡之源”實際上是我們對高德納先生的斷章取義,原文是這樣的:
我們應該在例如97%的時間里,忘掉小處的效率;過早優(yōu)化是萬惡之源。但我們不應該錯過關鍵的3%中的機會。
實際上是非關鍵路徑上的優(yōu)化是萬惡之源,問題在于如何確定我們的代碼是否在關鍵路徑上。不論節(jié)省的時間多么少,花費在關鍵路徑上的性能優(yōu)化都是值得的。
對于性能優(yōu)化工具,用于JavaScript源代碼壓縮有 google Closure complier,packer,YUI compressor,JSmin等。頁面的性能優(yōu)化工具有YSlow 和Page Speed等。實際上,任何有意義且可靠的性能測試都是基于統(tǒng)計學上的合理實踐。 就JavaScript 代碼本身的性能而言,benchmarkjs 是一個很好的工具,而jsperf.com 提供了對JavaScript 執(zhí)行環(huán)境的性能測試。
總之,JavaScript 是一個具有強大生命力的語言,前端框架更是日新月異,從Angular,Vue,到React, 乃至React Native,給人以目不暇接的感覺,但是,老碼農(nóng)覺得基礎認識還是非常必要的,勿在浮沙筑高塔。
【本文來自51CTO專欄作者“老曹”的原創(chuàng)文章,作者微信公眾號:喔家ArchiSelf,id:wrieless-com】