從前端到全棧:JavaScript逆襲之路

JavaScript如何作到上天入地無所不能?JavaScript真的能一統江湖嗎?javascript

背景

近年來,前端技術突飛猛進,前端已經不只僅是網頁,更多的開始由狹義向廣義發展。 前後涌現出了具有後端能力的node,具有移動開發能力的react native,具有遊戲渲染能力的cocos2d-js,以及iOS上的熱修復技術JSPatch等等新技術。 咋一看,幾乎各個端都被JavaScript攻陷,大有一統江湖之勢。 究竟,JavaScript如何作到上天入地無所不能?JavaScript真的能一統江湖嗎?html

故事要從JavaScript的由來講起。前端

亂世出英雄:JavaScript的誕生

高能瞎扯淡版,正經臉的同窗能夠忽略

有人的地方就有江湖,有江湖的地方就有紛爭。
故事要從當年的瀏覽器之戰提及。
時間回到1994年,
(→ 那時候我仍是個寶寶~ #天真臉#)
景兄弟橫空出世,並自帶神器網景導航,戰鬥力爆表,勢如劈竹,瞬時間威震天下。
一出世就武裝到牙齒,武力值這麼高還自帶兵器,這個科學嗎?
港真,我也以爲不科學,也許跟熊孩子哪吒、女漢子雅典娜是一個品種吧?
這一切北方的老前輩微軟大溼,都看在眼裏,不甘天下盡歸景兄弟這個初出茅廬的毛孩子,大溼積澱多年,潛心修煉一年,終於帶着大殺器IE 1.0出關了,誓於景兄弟爭個高低。
自此景兄弟的網景導航 VS 微軟大溼的IE 的軍備戰爭開始。
景兄弟仔細掂量,微軟大溼財大氣粗,內功深厚,臣妾實在是辦不到啊啊啊啊啊啊。
景兄弟緊急召集門人商議對策,有一門人曰:」以咱們微薄之力硬磕,是萬萬使不得的。現在咱們,一是宜施行合縱之策,抱大腿,組成聯盟!二是避其鋒芒,出奇招致勝。「
因而景兄弟依照此策略,一方面找到了當時德高爲重的另外一位前輩SUN,組成了開發者聯盟。
(微軟大溼:握草,聯盟都粗來了,那我是否是得搞個部落?)
另外一方面,景兄弟找到了鍛造大師布蘭登,請布大師幫忙升級兵器網景導航,大師就是大師,不費吹灰之力就完成了強化升級,然而布大師突發奇想,原本這是近距離攻擊兵器,要是有多一個遠距離攻擊的能力那豈不是更好?Just do it. 想罷大師就加了一個遠距離攻擊的feature。因而有了自帶遠距離攻擊能力的網景導航2.0。景兄弟一看這麼流弊內心甚是歡喜,不過遠距離攻擊的技能叫作LiveScript,感受不是特別Fashion。特然想到這不是跟SUN前輩聯盟嘛,SUN家的Java正是獨霸武林之時。不如把名字改爲跟Java有關,蹭一把東風,蹭點光環。一拍腦殼,JavaScript!!!衆門人一聽:」好好好,JavaScript 流弊炫酷吊炸天!「
果真第一節下半場,景兄弟攜強化過的網景導航2.0 戰個痛快,那是槓槓的!人家一問,你咋還能遠程攻擊,你這個遠程攻擊用的是啥?答曰:JavaScript。「JavaScript,必定是跟SUN家Java是一個系列產品,必定很流弊!」#光環加成,各類膜拜臉#
微軟大溼虧了一場,痛定思痛,也要搞遠程攻擊功能,果真不久,就祭出了一樣帶有遠程攻擊能力的IE 3.0,鑑於景兄弟的遠程攻擊叫作JavaScript,J開頭的感受應該比較流弊,因此微軟大溼的叫作JScript。
而後戰爭就從地面貼身肉搏戰,開始逐步升級到了遠距離核戰爭。
正所謂,城門失火,殃及池魚。這麼打下去苦逼的是搬磚的頁面仔,就是我這種,處處都是雷區,無處下腳。
最後到了1997年,「聯合國安理會祕書長」艾瑪(ECMA)出來調停,多方簽署了「核不擴散條約」,約束各類遠程攻擊武器的使用,這才走上了正軌。

背景:

1995年SUN開發了Java技術,這是第一個通用軟件平臺。Java擁有跨平臺、面向對象、泛型編程的特性,普遍應用於企業級Web應用開發和移動應用開發。Java也伴隨着互聯網的迅猛發展而發展,逐漸成爲重要的網絡編程語言。名聞遐邇。 
1994年Netscape公司成立,並推出了本身的瀏覽器的免費版本 Netscape Navigator,很快就佔有了瀏覽器市場。到了 1995 年,微軟公司開始加入,並很快發佈了本身的 Internet Explorer 1.0。 
1995年,當時在Netscape就任的Brendan Eich(布蘭登·艾克),正爲Netscape Navigator 2.0瀏覽器開發的一門名爲LiveScript的腳本語言,後來Netscape與Sun Microsystems組成的開發聯盟,爲了讓這門語言搭上Java這個編程語言「熱詞」,將其臨時更名爲「JavaScript」,往後這成爲大衆對這門語言有諸多誤解的緣由之一。 
JavaScript最初受Java啓發而開始設計的,目的之一就是「看上去像Java」,所以語法上有相似之處,一些名稱和命名規範也借自Java。但JavaScript的主要設計原則源自Self和Scheme。JavaScript與Java名稱上的近似,是當時Netscape爲了營銷考慮與SUN達成協議的結果。 
因此,JavaScript和Java其實沒有半毛錢關係。html5

JavaScript推出後在瀏覽器上大獲成功,微軟在不久後就爲Internet Explorer 3.0瀏覽器推出了JScript,以與處於市場領導地位的Netscape產品同臺競爭。JScript也是一種JavaScript實現,這兩個 
JavaScript語言版本在瀏覽器端共存意味着語言標準化的缺失,對這門語言進行標準化被提上了日程,在1997年,由Netscape、SUN、微軟、寶藍等公司組織及我的組成的技術委員會在ECMA(歐洲計算機制造商協會)肯定定義了一種名叫ECMAScript的新腳本語言標準,規範名爲ECMA-262。JavaScript成爲了ECMAScript的實現之一。ECMA-262 第五版,便是ES5。java

> ECMA-262,包括ES5, ES6等是一個標準,JavaScript是ECMAScript的一個實現。node

完整的JavaScript實現應該包含三個部分:react

在網景導航2.0和IE 3.0出現以後的幾年間,網景和微軟公司不停的發佈新版本的瀏覽器,支持更多的新功能。自此拉開了瀏覽器之戰的序幕。這場瀏覽器之戰到如今還在繼續,如下一張圖看清楚過程。android

從瀏覽器之戰能夠看出,各家瀏覽器比拼的大體兩個方面視覺體驗(渲染排版)和速度(腳本運行)。ios

> 因此一個完整的瀏覽器組成,至少包含兩個部分:git

補充一個市面常見瀏覽器的內核和JavaScript引擎搭配:

其餘JavaScript引擎,Rhino,由Mozilla基金會管理,開放源代碼,徹底以Java編寫,能夠看作SpiderMonkey的Java版。 注意:webkit不僅僅只是一個排版引擎,webkit = 排版引擎 + JavaScript引擎。 > 因此,JavaScript是動態語言,它的運行都是基於JavaScript引擎,引擎大都是由靜態語言實現C++、Java、and so on。JavaScript的能力也是由引擎賦予。無論是瀏覽器環境中是window,亦或是node環境中的process,均是由引擎提供。 (番外:Mozilla的人不知道爲啥特別喜歡猴子,常常以猴子命名技術,因此看到帶Monkey的,十有八九估計是他們搞的。)

諾曼底登錄:JavaScript Binding/Bridge 橋接技術

在瀏覽器環境中,DOM、BOM、window對象、setTimeout/setInterval,alert,console等方法均不是JavaScript自身具有的能力,而是瀏覽器native實現,而後經過JavaScript引擎注入到JS運行的全局上下文中,供JS使用。 鑑別方式,在調試器console中打出來,帶有[native code]的便是:

講道理:

  1. JavaScript運行 → 依賴於JavaScript引擎 ← 瀏覽器集成了JavaScript引擎,同時經過JavaScript引擎注入native代碼工JS腳本使用
  2. 發散一下思惟,只要有JavaScript引擎,就能運行JS腳本,無論有沒有瀏覽器!只是缺乏瀏覽器提供的alert,window等方法。
  3. 既然瀏覽器能夠往JavaScript引擎中注入代碼,賦予JS腳本在網頁中特殊的能力,同理咱們能夠本身集成JavaScript引擎,本身定義本身的方法往JavaScript引擎中注入,賦予JS更多更強的自定義能力! 
    注入的關鍵是:值類型相互對應,Obj映射class的一個實例,function映射一個句柄或者引用

JavaScript數值型中的坑

JavaScript內部,全部數字都是以64位浮點數形式儲存,即便整數也是如此。因此,1與1.0是相同的,是同一個數。 
這就是說,在JavaScript語言的底層,根本沒有整數,全部數字都是小數(64位浮點數)。容易形成混淆的是,某些運算只有整數才能完成,此時JavaScript會自動把64位浮點數,轉成32位整數,而後再進行運算。因爲浮點數不是精確的值,因此涉及小數的比較和運算要特別當心。儘可能避免使用JavaScript作精準計算和密集計算。

根據國際標準IEEE 754,JavaScript浮點數的64個二進制位,從最左邊開始,是這樣組成的。

  • 第1位:符號位,0表示正數,1表示負數
  • 第2位到第12位:儲存指數部分
  • 第13位到第64位:儲存小數部分(即有效數字) 符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度。 IEEE 754規定,有效數字第一位默認老是1,不保存在64位浮點數之中。也就是說,有效數字老是1.xx...xx的形式,其中xx..xx的部分保存在64位浮點數之中,最長可能爲52位。所以,JavaScript提供的有效數字最長爲53個二進制位(64位浮點的後52位+有效數字第一位的1)。

內部表現公式:(-1)^符號位 * 1.xx...xx * 2^指數位

精度最多隻能到53個二進制位,這意味着,絕對值小於2的53次方的整數,即-(253-1)到253-1,均可以精確表示。 而大部分的後端語言,C++、Java、Python等的long型都是能夠支持到64位,所以long型數據從後端語言傳給JavaScript會發生低位截斷。遇到這種狀況通常使用String處理,如須要在JavaScript中作long型計算,須要自行實現計算器。

有了自行往JavaScript引擎中注入的想法,接下來就是分析可行性。 大部分是JavaScript引擎是使用C++編寫,若是本身的程序使用的是C++能夠很方便的進行注入,若是是OC,可使用OC和C++混編的形式。 其餘語言怎麼破? 要在一門靜態語言上與動態語言JavaScript相互調用,最便捷的方式是找到一個這門語言實現的JavaScript引擎(開源),直接進行集成,注入。若是沒有,則須要使用多一層橋接,把這門語言的接口暴露給C++,再由C++實現的JavaScript引擎將接口注入供JavaScript使用。

服務端集成思路&實踐:

nodeJS中的橋接

咱們都知道nodeJS,可是nodeJS的運行依賴於Google的V8 引擎,V8是C++實現,底層使用C++實現底層功能,好比網絡,數據庫IO,對外暴露一個構造器接口注入到上下文中,注意此處暴露的只是一個構造器接口而不是一個建立完的實例。而後實現了一個require的hook函數。當使用require加載一個JS模塊時,跟網頁中使用AMD 的require並沒有異樣,當使用require加載系統庫,既是C++的模塊時,會調用暴露出來的構造器接口,獲得一個實例對象。無論是裝載JS模塊仍是裝載C++模塊,獲得的均可以看作是一個Module Object,node會將裝載完的模塊緩存到bindingcache中,下次在別處的代碼中使用require裝載模塊時,就會先去bindingcache中查找,若是找到了則返回該module object,若是沒找到再執行上面的裝載流程。 這就是node的基本原理:C++封裝底層操做,經過V8注入,使得JS腳本有網絡和IO能力

基於Spring的橋接

以上說到的幾個都是C++層面的應用,那麼經典的Java怎麼玩?是否是Java就必須是靜態語言的玩法,沒有辦法像C++之類的,可使用JS的動態特性? 固然不是。這個時候,咱們須要提及前面介紹過的一個JS引擎 Rhino,Rhino是徹底由Java編寫,可想而知,Rhino幾乎就是爲Java應用而生的。 用法是這樣:

  1. 首先在咱們的Java應用中集成Rhino;
  2. 全部的IO操做,網絡操做等,都封裝成service,並提供增刪改查,setter && getter等多種方法
  3. 經過spring,把這些service bean注入到Rhino中;
  4. 把業務邏輯寫到JS代碼中,JS代碼調用多個已注入的Java service處理業務邏輯,拼裝數據返回!

好處:修改業務邏輯不須要修改Java代碼,也就是不須要從新編譯和部署,只須要刷新下跑在Rhino中的JS代碼便可。以往Java應用的一個痛點是部署,須要從新編譯,打包,部署重啓服務器,如今以這種形式開發,能夠達到服務端的熱更新和熱部署。既能夠享有Java服務的穩定性和可靠性,又能夠享有JS的靈活性。 這種技術和用法在差很少十年前就有過,前EMC的工程師基於EMC著名的商業產品Documentum,設計了一套Java開源的中小企業CMS系統Alfresco,在該系統中實現了這種技術,這種技術基於spring,叫作spring-surf,作了一個膠水層。能夠看作小十年前的node吧。

Demo,使用spring-surf框架的系統中一個webscript模塊

  1. categorynode.get.xml定義URL攔截器和權限控制;

  2. .get指明是處理GET請求,RESTful;

  3. 在categorynode.get.js中調用已注入的Java Bean處理業務邏輯;

  4. 若爲網頁請求返回.html.ftl,若爲Ajax,返回.json.ftl;

(此處配套使用的是FreeMarker模板引擎)

> categorynode.get.desc.xml

<webscript>  
  <shortname>category node</shortname>
  <description>Document List Component - category node data webscript</description>
  <url>/slingshot/doclib/categorynode/node/{store_type}/{store_id}/{id}/{path}</url>
  <url>/slingshot/doclib/categorynode/node/{store_type}/{store_id}/{id}</url>
  <format default="json">argument</format>
  <authentication>user</authentication>
  <transaction allow="readonly">required</transaction>
  <lifecycle>internal</lifecycle>
</webscript>

> categorynode.get.js

model.categorynode = getCategoryNode();

function getCategoryNode(){  
   try{
      var items = new Array(), hasSubfolders = true,
         evalChildFolders = args["children"] !== "false";
      var catAspect = (args["aspect"] != null) ? args["aspect"] : "cm:generalclassifiable",
         nodeRef = url.templateArgs.store_type + "://"
                    + url.templateArgs.store_id + "/"
                    + url.templateArgs.id,
         path = url.templateArgs.path,
         rootCategories = classification.getRootCategories(catAspect),
         rootNode, parent, categoryResults;
      if (rootCategories != null && rootCategories.length > 0) {
         rootNode = rootCategories[0].parent;
         if (path == null) {
            categoryResults = classification.getRootCategories(catAspect);
         } else {//...}

         for each (item in categoryResults) {
            if (evalChildFolders) {
               hasSubfolders = item.children.length > 0;
            }

            items.push( {
               node: item,
               hasSubfolders: hasSubfolders,
               aspect: catAspect
            });
         }
      }
      return ({ items: items });
   }
   catch(e) { return; }
}

> categorynode.get.html.ftl

<#include "categorynode.get.json.ftl">

==> categorynode.get.json.ftl

<#escape x as jsonUtils.encodeJSONString(x)>  
{
   "totalResults": ${categorynode.items?size?c},
   "items":
   [
   <#list categorynode.items as item>
      <#assign c = item.node>
      {
         "nodeRef": "${c.nodeRef}",
         "name": "${c.name}",
         "index": "${c.properties["lem:index"]!"0"}",
         "description": "${(c.properties.description!"")}",
         "hasChildren": ${item.hasSubfolders?string},
         "aspect": "${item.aspect!""}",
         "userAccess":
         {
            "create": ${c.hasPermission("CreateChildren")?string},
            "edit": ${c.hasPermission("Write")?string},
            "delete": ${c.hasPermission("Delete")?string}
         }
      }<#if item_has_next>,</#if>
   </#list>
   ]
}
</#escape>

移動端集成思路&實踐:

React Native中的橋接

React Native目前也是異常火爆,RN程序的運行依賴於Facebook的RN框架。在iOS、Android的模擬器或是真機上,React Native使用的是JavaScriptCore引擎,也就是Safari所使用的JavaScript引擎。可是在iOS上JavaScriptCore並無使用即時編譯技術(JIT),由於在iOS中應用無權擁有可寫可執行的內存頁(於是沒法動態生成代碼),在安卓上,理論上是可使用的。JavaScriptCore引擎也是使用C++編寫,在iOS和安卓中,JavaScriptCore都作了一層封裝,能夠無須關心引擎和系統橋接的那一層。iOS/Android系統經過JavaScriptCore引擎將定製好的各類原生組件注入,如:listview,text等。

Cocos2d-JS中的橋接

cocos2dx是遊戲開發中很是經常使用的遊戲渲染引擎,有一系列的產品,如:cocos2dx(C++),cocos2d-lua(lua), cocos2d-js(JavaScript)等多個產品。其中最新退出的是cocos2dx的JS版本的cocos2d-js,編寫遊戲渲染特效代碼相比於C++和lua很是方便。對於作須要常常更新的渲染場景,C++是靜態語言,每次修改都須要從新編譯才能運行,顯然是不合適的。天然也就想到了腳本語言,lua和js,二者有些相似,都是動態語言,只須要集成一個運行引擎,提供一個運行的容器便可運行,同時經過引擎注入底層方法供腳本調用便可。lua好處是精簡,語法精簡,引擎頁很小很精簡,因此不可避免的代碼量會比js多,同時學習成本比較高。js的好處是有ECMAScrtpt的核心,語法比較豐富,同時有支持一些高級屬性。在cocos2d-js中,cocos2dx(C++)集成了SpiderMonkey(C++)做爲JS運行引擎,中間作了一個膠水層既是JS Binding,經過引擎注入了一個cc的全局對象,映射的是底層C++的一個單例C++實例。表面上寫的是JS代碼,實際上操做的是底層的C++。cocos2d-js是代碼能夠運行在多種環境中,當運行的網頁環境中時,使用的是cocos2d-html5引擎,底層操做的是canvas;當運行在客戶端上時,使用的是cocos2dx引擎,底層操做的是C++,再由C++去操控openGL作繪製和渲染。提供相同的API,對開發者幾乎是透明無差別的,開發者只須要關注實現效果便可。達到一套代碼,多端運行(網頁端,客戶端)。

JSPatch技術中的橋接

JSPatch是目前比較流行的iOS上的熱修復技術,JSPatch 能作到經過 JS 調用和改寫 OC 方法最根本的緣由是 Objective-C 是動態語言,OC 上全部方法的調用/類的生成都經過 Objective-C Runtime 在運行時進行,咱們能夠經過類名/方法名反射獲得相應的類和方法。JSPatch 的基本原理就是:JS 傳遞字符串給 OC,OC 經過 Runtime 接口調用和替換 OC 方法。 
關鍵技術之一是 JS 和 OC 之間的消息互傳。JSPatch裏包含了,一個JS引擎JavaScriptCore(Safari,React Native用的同款)。用到了 JavaScriptCore 的接口,OC 端在啓動 JSPatch 引擎時會建立一個 JSContext 實例,JSContext 是 JS 代碼的執行環境,能夠給 JSContext 添加方法,JS 就能夠直接調用這個方法。本質上就是經過JavaScriptCore引擎注入,暴露OC的方法供JS調用來實現動態修改OC的反射。

Demo,iOS熱更新,熱修復:

  1. 集成JavaScriptCore引擎;
  2. 經過引擎,橋接JS和OC;
  3. 經過JS修改OC反射。
require('UIView');  
//1,在全局查找 UIView變量,查到則返回,
//  查不到則生成 UIView = { __clsName: "UIView" }
//2,將類名經過橋接層,傳給OC
//3,OC經過runtime方法找出這個類全部方法,返回給橋接層
//4,橋接層爲JS生成對應的JS 屬性/方法
var demoView = UIView.alloc().init();  
demoView.setBackgroundColor(require('UIColor').grayColor());  
demoView.setAlpha(0.2)

詳細的JSPatch技術介紹請移步:https://github.com/bang590/JSPatch/wiki

關於JavaScript引擎: 
在iOS 或 android 上可以運行的JavaScript 引擎有4個:JavaScriptCore,SpiderMonkey,V8,Rhino。下面這個表格展現各個引擎在iOS 和 Android 的兼容性。

由於iOS平臺不支持JIT即時編譯,而V8只有JIT模式,因此V8沒法在iOS平臺使用(越獄設備除外,想體驗iOS JIT的同窗能夠自行越獄)。 因此,目前能夠作到橫跨iOS和Android雙平臺的JS引擎,只有兩款,便是SpiderMonkey和JavaScriptCore。 JavaScript引擎會受不少東西影響,好比交叉編譯器的版本、引擎的版本和操做系統的種類等。 
至於如何選擇,能夠參考:《Part I: How to Choose a JavaScript Engine for iOS and Android Development》

至此,JavaScript從立足於前端,到征戰全端的逆襲之路,能夠總結爲「攜引擎以令天下」。 不足之處,還請各位看官輕拍~

參考文章:

bang590/JSPatch中問參考文檔
Cocos2d-JS | Cocos2d-x官方參考文檔
Alfresco官方參考文檔
《Browser Wars: The End or Just the Beginning?》
《Part I: How to Choose a JavaScript Engine for iOS and Android Development》
《React Native 從入門到源碼》

相關文章
相關標籤/搜索