2016年5月25日 星期三

Javascript 的 Function 及 Object

函式 及 物件

在 JavaScript 程式裡, Object (物件)是一個容器,Function(函式)就好比是靈魂(程序),主宰及運作整個生命體的就是程式.物件是由特性(property)所形成的個體,特性以 關鍵字:值 (key:value) 的方式配對成雙,相互用逗點隔開, 大括號 { } 框住全部特性形成物件.而物件要取用特性時用名稱加句點語法直接引用.也只有物件及原生體(name of the primitive values:詳見後記)可以用句點語法取用特性.對於函式而言,他的名稱同時也會被賦予成一個物件.而函式是用小寫關鍵字function來定義.我們可以隨便定義一個函式就可明瞭, 如以下的空心函式 :

                 function my( ) { }        // 先宣告一段函式, 名稱是 my, 無內容.
                 my.name    //  'my',   name:'my' 這是一個 my 物件的一個特性

任何一個函式的最後必定會傳回(return)一個值(value),即使沒有寫在函式裡頭,Javsscript仍然會傳回值叫作undefined,函式的傳回值簡稱函數值(function value).因此當定義完一個函式,裡頭有三個意義: 函式物件, 函數值, 函式, 以 function my( ) { } 這個例子來說 my 是函式物件, my( ) 是函數值,整個敘述  my( ) { } 才是函式, 如果以中文語意來看,函式名稱 my 到底是物件還是函式或是函數值,見仁見智,可以說既是物件也是函式又可能是函數值,很容易讓人混淆.有個新名詞叫作方法(method),他代表著函式的功能,因此以下文章引用'函式把它當成函式功能來看,如果是'函式體',那指的就是'函式物件'.單獨稱'函數值'時才是函式的回傳值.一定要區別,否則會搞混.最後要注意的是只有物件才能擔任繼承者或是被繼承的角色,比較特別的是原生雖非物件但也會繼承到一些特性(後記再加以說明).函式方法一旦被繼承或是自行創造,也在物件中扮演著特性的一部份,實際上函式方法最後所得到的函數值才是物件的特性.因此函式體是可以繼承或被繼承的,而原生體的數值(primitive value)是一個常數(immutable)是無法改變的.

所有函式(注意是物件)全部都繼承來自Function.prototype,實際上Function.prototype就是所有函式的始祖簡稱函式祖(注意是物件).函式祖有著prototype,bind,call,apply,name ...等等的特性.因此任何一個函式只要宣告完成,函式(注意是物件)一定會繼承到prototype,bind,call,apply,name ...等等這些隱藏的特性.定義物件可以簡單用 {key:value, ... }方式直接定義產生新的物件, 而且用中文來描述特性也沒問題:

                a={
                          '住址':'台灣',
                          '性別':'男生' ,
                          '姓名':'中文名',
                 };
                 console.log(a.住址+a.性別+a.姓名);

當物件指定一個名稱後,它只是該物件的一個別名, 所有利用別名來改變特性都會直接影響到物件實體的特性.例如:
           
                myo = {x:3, y:4} ;
                myn = myo;             // an alias, different name but same object
                myn.x = 1                // myo = { x:1, y:4}
                myo.y = 2                // myn = { x:1, y:2}
                myn === myo           // true

上述 myn 是 myo 的一個別名, 物件實體只有一個. 運算符號 '===' 可以用來確認實體是否相同.透過 Object( )  函式也可以用來產生新物件,例如:
                
                myo =  Object({x:3,y:4})

或是:
               myo = new Object({x:3,y:4})  // alias, same object but different name
               Object(myo) === new Object(myo)   // Identical operation
               myo === Object(myo)  // using alias, return alias

上面可以看到比較特殊的, 當傳進去的是別名(alias), Object( ) 及 new Object( )並不會另外產生一個實體,而是直接回傳物件的別名, 有點意外!

              {x:1} === Object({x:1})      // false. differenct object
              {x:1} === new Object({x:1}) // false. different object
        Object({x:1}) === new Object({x:1}) // false. different object

由上面可以知道當用常數物件時, Object 及 new Object ( ) 會產生新的物件, 但如果是用變數,他只是一個別名,對別名而言,就直接回傳.
            
透過Object.create( )除了產生時實體外,同時也繼承了物件的特性, 例如要繼承函式祖(注意是物件)的特性,可以這樣做:

                 notFunction=Object.create(Function.prototype)

上述物件 notFunction 利用Object.create( )只有繼承到函的特性,並沒有賦予函數功(就是說不能用 notFunction( ) 去執行一個命令, 或是呼叫 call 綁定物件,因此notFunction.call( )是不允許的.這就是為何要將函式體與函式方法分開的原因.如果要創造函式,還真可以利用原生函式Function( )來完成,它完全繼承了函式的特性.例如輸入兩個參數相加後回傳的函式:

                        a= Function('a,b','return a+b');
                        a(3,5)

創造物件時,可以用關鍵字 new 函式方法(注意是函式功能), 這種特殊的函式方法稱建構式(注意它是函式).new 建構式除了可以繼承函式祖(Function.prototype)外,並且用函式本身程序可以創造出新的特性,(再三要強調的一點是new建構式產生出來的是新物件或原生體而不是函式).如以下範例:

                 function fobj() { this.x=3; this.y=4;return 3; }
                 myn = new fobj()

上述的函數值(注意是 fobj 的傳回值 3)就變的沒多大的意義了. myn除了繼承了函式祖外,也用程序創造了 x:3 及 y:4 的特性.上述  'this' 是一個特殊的字眼(詳件後記),他是一個物件,要小心使用,this會因出現的位置不同,而解釋成不同的物件,函式是可以透過綁定(bind, call, apply)一個物件之後再讓程序(program)運作的.當然被綁定的物件必定要有一些規範(protocol), 以上面 fobj 例子來說, 他的運作是可以更改特性 x及y,因此特性 x:, y: 就成了存在物件裡必要的規範(protocol), 所以要先宣告一個物件含有 x和y的特性, 才能用函式 fobj 去綁定(bind)該物件,最後才讓程序去運作, 如果綁定( bind )之後加上命令符號 ( )可以讓綁定函式的程序直接去運行.綁定後 this 就變成是新物件,例如下例 fobj 綁定物件 a 的新程序: fobj.bind(a) ( )

                  function fobj() { this.x=3; this.y=4; }
                  a={x:5,y:6};
                  fobj.bind(a) ( )   // a = {x:3,y:4} , 'this' is equal to a

可以看到 a 的特性x和y都被更改了, 而 call 的用法是除了綁定物件外,緊接著就執行程序,因此後面不再加上命令符號 ( )

                  function fobj() { this.x=3; this.y=4; }
                  b={x:1,y:2};              
                  fobj.call(b)      // 'this' is equal to b

如果函式帶有參數要傳遞可以加在 ( ) 裏面, 當建構式利用參數來初始化特性時,它就成了初始化建構式. 這樣子無形中又替這種函式建立了另一套規範(protocol), 就是說除了物件必須有x及y兩個特性外,當呼叫時必須要指定兩個參數:

                  function iobj(x,y) { this.x=x;this.y=y; }
                  a={x:5,y:6}
                  iobj.call(a,7,8) // 'this' is equal to a

而 apply 不同於 call 的用法,在於他要用一個陣列將參數一次傳遞過去,就等於是使用另外一種規範(protocol: 物件要有屬性 x,y, 呼叫時要傳兩個參數, 將他們組合成陣列一次傳過來)的程序而已:

                  function iobj(x,y) { this.x=x;this.y=y; }
                  b={x:5,y:6}
                  iobj.apply(b,[7,8])   // 'this' is equal to b

要特別注意的是只有函式方法(注意是函式功能)才可以呼叫bind( )或call( )或apply( )程序綁定物件讓它有執行程序的能力,就好像是物件去執行新程序一樣.因此所有的函式方法必須定義好規範(protocol)才能正常運作. 否則能運作只是運氣好(lucky)而已.回過頭來看 new 建構式與 Object.create(函式體),同樣看似繼承自函式物件,但產生的物件卻是截然不同的.而物件可以透過 Object.getPrototypeOf( ) 取得上一代的原型加以驗證:

         function fobj( ) {this.x=1; return 3;}
         a = Object.create( fobj )    
         // a inherit from fobj, a pure object, no functionality.
         b = new fobj( )                      //  b is instance of fobj
         c  = new fobj( )                     //  c is another instance of fobj

         Object.getPrototypeOf(Function.prototype) === Object.prototype// true
         Object.getPrototypeOf(fobj)===Function.prototype   // true
         Object.getPrototypeOf(a) === fobj  // true,  a is inherited from fobj

         Object.getPrototypeOf(fobj.prototype) === Object.prototype // ture               
         Object.getPrototypeOf(b)=== fobj.prototype  // true
         Object.getPrototypeOf(c)=== fobj.prototype  // true

          a.name === fobj.name         // true
          a.prototype === fobj.prototype  // true


因為fobj用function宣告,繼承自Function.prototype也就是函式祖,函式祖又從Object.prototype繼承而來,因為Object.prototype是一切物件的始祖(簡稱祖先).由此可見fobj是祖先後二代的物件

                        函式體的物件承路徑圖:祖先->函式祖->函式體(fobj)->a

而a又繼承自fobj,但Object.create( )僅能繼承到函式而不會繼承函式功能,a單純就是一個祖先後三代沒有執行功能的物件.而a.prototype===fobj.prototype是因繼承而來,即使a.call===fobj.call,但仍不能呼叫a.call( ),因為綁定程序是給物件函式用的.很重要的觀念就是物件不能呼叫bindcallapply等程序,只有方法可以呼叫bind,call,apply程序來綁定物件讓它看起來像是物件在執行函式.再來看new的操作. new是操作於一個建構式,函式體裏面有一個特性稱為prototype,當函式一宣告完成,就會從祖先繼承一個空物件{},它有個專有名詞叫原型物件(prototype).顯然 b 及 c 繼承了這個原型物件,但要注意的是原型物件裡並不一定有prototype(大部份原型物件並沒有)這個特性存在的,可以想像成是b及c都是用Object.create(fobj.prototype)產生的繼承關係,因為內定原型物件是一個空物件,因此 b與 c 並沒有 prototype這個特性. 再做個實驗, 當建構式傳回物件時:

            function fobj( ) { var c=[ ]; c.a='a';c.d=function ( ) { console.log('d'); }; return c; }
            a=new fobj( )
            a   // [ a:'a', d:Function ]
            a.a // 'a'
            a.d( ) // 'd'

可見他真的傳回該陣列 c, 陣列是一個物件而不是繼承原型的空物件, 有點意外! 建構式return 物件時真的要小心!,當建構式不傳回物件時:
                        new 建構式的物承路徑圖:祖先->原型物件->b或c

由此來看new 建構式的操作程序是:

                       1. 繼承原型物件產生新物件.
                       2. 建構式程序修改新物件.
                       3. 如果 return 不是物件,則回傳上述新物件產生實體(instance).
                       4. 如果 return 後面接的是一個物件, 就直接回傳該物件.

因此 b 與 c 雖有相同的特性,但卻是分別獨立的個體.彼此更改特性並不會受到影響.要注意的是 fobj, 及 fobj.prototype 分別表示兩個不同的實體物件,彼此間並不一定有繼承關係(繼承路徑不同),不需太大著墨.僅須記住fobj是函式體,而fobj.prototype是原型物件.因為所有繼承者都會繼承這個原型物件,而原型就只有一個.這也就是為何當原型物件更動任何特性時,都會反應在繼承者身上.那可不可以將原型物件換掉呢,答案顯而易見是可以的, 但有可能因繼承關係而造成混亂.做個實驗:

           function fobj() {} // empty  function to construc an new object
           fobj.prototype={x:20,y:30,z:50}// use a defined object to be construct.
           a=new fobj();
           a.x+a.y+a.z // a is empty, but {x:20, y:30, z:50 }did exist

結論: 當建構式傳回的是物件時,new建構式直接傳回該物件,否則繼承函式的原型物件後回傳,有點像是Object.create(fobj.prototype),而原型物件在函式宣告後,只要在使用 new 建構式之前都是可以被替換掉的,原型物件所有特性也將會被繼承.Object.create(fobj)則只是繼承函罷了,繼承者並不俱備執行功能. fobj及fobj.prototype各自描述著不同的實體物件,fobj是函式體,記載著所有函數的特性,而fobj.prototype只是內其中的一個特性(稱為原型物件),視需要可以自行替換,千萬不要搞混了.

後記:原生值(Primitive values)

Javascript 內有 5 種原生值(Primitive values): 字串(Strings),數字(Numbers),布林值(Booleans), 未定義(undefined),空的(null). 它們是常數(immutable value),而不是物件.除了undefined及null外,原生也會從他始祖的原型物件繼承到原生的特性,例如字串會從字串祖(String.prototype)繼承字串特性,數字會從數字祖(Number.prototype)繼承數字特性,布林(邏輯)值則從布林祖(Boolean.prototype)繼承布林特性,就如同函式體(函式名稱)從函式祖(Function.prototype)繼承一樣.而函式體描述著函式的特性,以此類推,原生的名稱也應該能描述原生值的性,因此稱他為原生體.但原生體並沒有原型物件(prototype)這個特性.且原生體也無法經由 Object.getPrototypeOf( )取得被繼承者. 我們可以試著繼承各原生祖的原型物件而產生原生體,但無法產生原生.如同繼承函式祖無法產生函式的功能,試著繼承各原生祖:

       notFunction=Object.create(Function.prototype) // 這不是函式功能 { }

繼承其它原生祖:

       notString=Object.create(String.prototype)          // 這不是字串 { }
       notNumber=Object.create(Number.prototype)    // 這不是數字 { }
       notBoolean=Object.create(Boolean.prototype)   // 這不是布林值{ }

雖然空物件的原生體有繼承到原生的特性(property),但並未賦予原生(數字,字串,邏輯,函式).正如同前面所說,可以試著繼承函式祖,但無法賦予函式功能. 因此原生體與原生還是要分開來看才不會混淆.透過 typeof 可以知道到底是物件(object)還是原生(string, number, boolean, undefined),所有 new 建構式都會繼承原型物件,因此傳回物件是可以理解的. 但 instanceof null 傳回 object, 據說是知名的 bug, 實際上null是原生值而不是物件.

              typeof '123'  // 'string'
              typeof 123    // 'number'
              typeof true   // 'boolean'
              typeof false  // 'boolean'
              typeof undefined // undefined
              typeof null   //  'object'
              typeof new String('123')  // object
              typeof new Number(123)  // object
              typeof new Boolean(true)  // object


做個實驗:

               function b( ) {return 3; }
               a=b( )  // 函式b的函數值
               a         // 3

最後一行給名稱就可得到值,可見得原生值一定是函數值. Number( ) 就是數字構建函數,String( ) 就是字串構建函數,而Boolean()就是布林值構建函數.想像一下數字構建函數的運作原理:繼承數字祖產生一個數字原生體,該原生體其實是一個可執行函式體,正如上面的 a一樣, 開頭程式碼就指向 b( ),b( ) 就是構建函數, a 本身後面附有有一些描述內容(也就是數字原生體)包含數值及所繼承的各種特性,只要判斷所要取得的物件是什麼就指過去執行,否則傳回本身函數值.而構件函數在搭建原生值時一定是只有改變函數值後再將原生體回傳.所有的原生體都是獨立的實體且只有函數值及型態(字串,數字,布林)不同而已.

當原生值指定給一個新變數時,例如要將原生體a指定給c (c=a),由上面數字構建函數的思維來看.只能傳回函數值其實隱涵著函數值無法被改變(immutable),為了做到互不相干擾,複製一份原生體是唯一的方案. 因此指定新常數給變數就等於複製一份原生體(他其實是一段可執行函式加上原生體),所以這3指定給變數時, 所產生的變數並不是別名,而是一段真正可執行的函數原生體.原生值的比對是基於數值valueOf( )的比對,並非物件的比對.

               a=123  //123 是常數,產生可執行函數原生體來描述 a.valueOf( ) 等於 123
               // a 是常數(immutable)個體,因此無法設定像 a.valueOf() = 44
               b=a      // 既然 a 是函數原生體, 將原生體 a 複製一份給 b
               a===b // 常數的比對,等同於 a.valueOf( )===b.valueOf( )
               a=44    // 當 a 指定新常數時, b 並不會跟著改變
               a.toString( ) // 原生值 a 有繼承到一些方法像是 toString()
               b.toString( ) // 原生值 b 也是

物件 this

如果在函式宣告後面加上( ),並將整個函式用另一個 ( )框住,這種函式稱為立即賦值函式(Immediately Invoke Function Express 簡稱 IIFE), 函式兩邊的括號就是一個賦值敘述(evaluate expression 白話文的意思就是:先計算出來再說),後面一對( )等於就是命令執行的意思,當賦值敘述越放在內層,要優先運算出來(堆疊的概念:先進後出,後進則要先出),舉個算式為例:

                              ( ( (1+9)* 4 + 10 )*3+10 ) /10  =16
                             先從最內層算出   1+9=10
                             再算出外層           10*4+10 = 50
                             接著算出更外層   50*3+10 =160
                             最後才是               160/10 =16
接著再看:
                             (  function ( ) {y=5; }  ( )  )       // y=5  --- (1)
或是:
                             (  function ( ) {y=5; } ) ( )         // y=5  --- (2)
或是:
                             y=( function ( ) {return 5;} ) ( ) // y=5  --- (3)
     
在第1式函式宣告完加上賦值敘述等於讓它執行, 第2式函式宣告完經賦值敘述得到一個函式,後面 ( ) 才讓它執行.在第3式同樣函式宣告完加上賦值敘述得到一個函式,後面 ( ) 讓它執行,傳回函數值,最後才指定給新變數.這三種寫法都讓y=5,上述不只宣告函式還讓它立即執行,最後y都被賦予了一個數值5.上述函式裡可以看到,當變數沒宣告時,就直接取自整體物件的變數(JavaScript 在瀏覽器內執行時,window就代表是整體物件).如果再將this放在函式裡頭,很容易讓人混淆或誤解.this 如果在一般的函式裡頭,代表的就是整體物件.當變數x用var宣告後,它變成是內部區域變數(private variable),它不會影響到同名稱的整體變數(global variable).一般的函式利用this這個物件可以存取整體物件的變數(有點類似隔空抓物的感覺).以下實驗可以看到在第1式與第2式會得出相同結果,但第3式 x 值並不受影響,而y等於函式值 5:

              x=3;y=(function ( ) { x=4       ; return 5; }) ( );  // x=4, y=5
              x=3;y=(function ( ) { this.x=4; return 5; }) ( );  // x=4, y=5
              x=3;y=(function ( ) { var x=4 ; return 5; }) ( );  // x=3, y=5

new建構式也隱涵著立即賦值的意義,不同在於傳回值是一個繼承了函式內部原型物件的一個新物件(instance),而不再是函數值,同樣的,如果變數沒宣告,等同取自整體物件,從下面第1式就可明白.一旦用 new 建構式時, this 就不再是整體物件而是新物件,就是上述的新物件(instance),由下面第2式就可以明瞭,因此要將變數當區域變數,最好宣告成 var 如下面第3式一樣.最後面 new 建構式的函數值(return 5)就變的沒實質意義了.

         x=3;y=new function ( ) { x=4       ; return 5; };//x=4, y={ },global x
         x=3;y=new function ( ) { this.x=4; return 5; };//x=3, y={x:4},new x
         x=3;y=new function ( ) { var x=4 ; return 5; };//x=3, y={ },private x

當物件包含函數特性時this的運作情形又如何呢? 由以下可知,物件的特性被改掉了,但變數 x 沒變, 因此當物件執行函數時,就好像函式將物件綁定了,而this就是該物件:

        x=1;  a={x: 2,   setx:function() { this.x=3; } } ;   a.setx();

來看一個 return this 很有趣的例子, 先宣告一個空心函式.再替原型物件增添新函數,每個函數增添一名成員,最後都傳回 this.

          function f( ) { };
          f.prototype.setd = function(d) { this.d=1; return this; };
          f.prototype.sete = function(e)  { this.e=2; return this; };
          f.prototype.setn = function(n) { this.n=3; return this; };
          new f( ).setd(1).sete(2).setn(3)  // { d:1, e:2,n:3 }

最後一式, new 建構式開始回傳一個空物件 new f( ) -> { }, 該物件繼承自原型物件,後來又再原型物件添加了函數特性,此空物件也就繼承了setd:function ( ), sete:function ( ), setn:function ( )等等函數特性.透過句點語法取用第一個特性時設定新物件的 d 後傳回 this, 他就是新物件本身,緊接著第二次也是相同程序,最後得到 {d:1,e:2,n:3} 這個新物件


無名函式呼叫綁定函數 bind( ), 經賦值敘述後再用命令符號( )同樣可以立即執行,如果沒有綁定任何物件,  this 內定整體物件.

         x=3; ( function ( ) { x=4; } .bind( ) ) ( )        // global x
         x=3; ( function ( ) { this.x=4; }.bind( ) ) ( )  // global x
         x=3; ( function ( ) { var x=4; }.bind( ) ) ( )  // private x

另外兩個綁定函數 call( ) 及 apply( ),只要加上賦值敘述就能立即執行,無須增加命令符號( ):

         x=3; ( function ( ) { this.x=4; }.bind( ) ) ( ) //x=4
         x=3; ( function ( ) { this.x=4; }.call()    )     // x=4
         x=3; ( function ( ) { this.x=4; }.apply() )     // x=4

如果函數(bind( ), call( ), apply( ) ...等等)綁定的是新物件, 那 this 就是這個新物件.

         a={x:3 };( function ( ) {this.x=4; }.bind(a)   ) ( ) //a= {x:4 } new x
         a={x:3 };( function ( ) {this.x=4 ; }.call(a)    )    //a= {x:4 } new x
         a={x:3 };( function ( ) {this.x=4 ; }.apply(a) )    //a= {x:4 } new x

call( ) 與 apply( ) 綁定函數不同在於: call( )是綁定物件後,將參數跟隨物件一一列出再呼叫函式, 而 apply 則是綁定物件跟隨一個陣列後呼叫函式, 簡單來說call的參數數量是不定的,但 apply 的參數就只是一個陣列.最後要再三強調的是只有函式方法可以呼叫 bind( ), call( ), apply( ) 等程序來綁定物件執行該函式.

特殊函式體:函式口 Closure 

前面所說當變數沒宣告時,它會變成整體變數.有個例子很有趣,當變數是放在函式裡頭的宣告函式裏面時,如果該變數沒有用 var 宣告,那它是區域變數還是整體變數呢? 這個重點如果擺在'程序是否有被執行',就可發現端倪,要記住函式宣告後產生一個函式體,實際上並未執行,除非用任何賦值敘述( )讓他執行.先看以下範例:

             x=1;y=1;
            function a( ) { x=10; function f( ) { y=11; }          };                          
            function b( ) { x=20; function f( ) { y=21; } ; f( );};
            // 函數 a,b 剛宣告完, 並未執行, 因此 x 及 y 並未改變
            a( )
            // 執行函式體a,x改掉了,但函式體f宣告完還沒執行,因此y並未改變.
            b( )
            // 執行函式體b,函式f在函式b裡宣告完,緊接著就執行,於是y也改掉了
       
可見只要程序被執行過,如果沒用 var宣告,它還是取自整體變數, 如果將函式體傳回來,又會如何?看以下例子:

            x=1;y=1;
            function c( ) { x=30; function f( ) { y=31; } ; return f;  };
            d=c( )
            d( )
            // 函式體 f 最後還是被執行了, 因此 y 也被改了.

初學者可能會誤以為f( )在函式c裡頭宣告, 就用 c.f( ) 讓它執行. 雖然c是一個物件,但它並沒有f( )這個方法.因為函式體都從Function.prototype繼承而來,裏面特性除非是加在Function.protoype後面或者自行添加用c.d = function ( ) { }方式來創造特性,但這又是另外一回事.裏面的函式宣告只是一個程式的宣告, 就好比用var宣告變數是同樣的道理.因此可以將同樣的名稱在不同的函式宣告,互相並不會影響,因為該名稱被視為函式裡頭的區域變數,而區域變數是無法直接用句點語法取用到的.因此變數未經 var 宣告就是整體變數,這個原則仍舊成立.對於函式宣告而言,也是如此.如果傳回函式體而且攜帶著區域變數,又會如何呢子? 詳見以下範例:
         
                y=1;  function c(x) {var y=x; function f( ) { return y++;  }; return f; }
                inc=c(5)
                inc( )
                inc( )

在函式裡頭的 y 用 var 宣告, 因此它是區域變數,並不會影響到整體變數的 y. 函式c先初始化(y=5).再傳回函式體 f 給 inc, 此時 inc 並未執行, 透過下一步命令符號 inc( ) 執行後,區域變數 y 早已被初始成5,這個函式體會執行 return y++的動作, 就是傳回 y(=5) 後再將y加一,周而復始.可看到後面再執行inc()時都會累加一.你一定會有個疑問:外部函式停止了,所有區域變數不就消失了,為何區域變數還存活著呢? 這個就是著名的closure啦.我也不會解釋.反正真的這樣,也許要找出它的組合程式碼才會明瞭.這種攜帶著區域變數的特殊函式體簡稱 closure.只有指著一個函式的入口點,當加上命令符號( )後才會開始執行函式程序,因此翻譯成函式口,網路上翻譯成閉包,怪怪的說.看看如果將區域變數所指的的物件傳回,該物件會是如何呢?

                function aobj( ) { var a={x:1}; return a; }
                b=aobj( )

對於習慣c語言的來說,可能無法理解,程式結束後,區域變數不就全消失了,當區域變數指向物件後再傳回去,該物件竟然還活著!這就必須要重新思考JavaScript對於物件的記憶體管理,可能不是從堆疊(stack)記憶體去分配,而是使用了靜態(static)記憶體.合理猜測:區域變數是一個指標,從堆疊裡分配記憶體給指標使用,但物件則使用靜態記憶體,而指標會指向該物件,因此程式結束後,區域變數(指標)雖然消失了,但物件還保留在靜態記憶體中.這也許就是closure變數所指向的物件還活著的原因.因此一旦物件不使用時應該將它刪除(delete),將記憶體釋放出來.

結論:
1.函式內變數除非用 var 宣告成區域變數,否則它是整體變數
2.物件取用的特性是函數時, 該函數的式子裏面this就是該物件本身
3.當使用new建構式時,裏面的this及傳回物件就是新物件(從原型物件繼承).
4.只有函式可以呼叫 bind( ), call( ), apply( ) 程序來綁定物件執行該函式.
5.函式用綁定函數執行時, 如果綁定了新物件, this 就是新物件,如果沒有,它就是整體物件.

因此 this 代表的不是整體物件就是被綁定的物件.

2016年5月20日 星期五

何謂模倒數 modulus inverse

何謂模倒數,參考文章:  https://www.khanacademy.org/computing/computer-science/cryptography/modarithmetic/a/modular-inverses

要先明瞭何謂模序列.一個模的序列產生是用除法取其餘數, 故須先要有除數 n, 及被除數 x, 兩者做除法運算得出商數q及餘數r,而除數n就稱為模(modulus),模運算所產生的數必定在序列0 .. n-1之間的一個整數, 在c語言中用%當作模運算符號,因此  r = x % n , 0=<r<n-1, 我們可以定義模函數:

                                                    r = f(x) mod n = mod(x,n)

為了找尋它的反函數 f', 必須滿足 f'(r) = f('f(x)) mod n = x mod n, 但這不是模倒數的定義方式

再談談何謂倒數(inverse),一個數A的倒數就是1/A,記號改成 A-1(指數負一, 意思等同倒數, 並不是減一的意思),在模運算中不能用除法運算只能用乘法,而A的倒數滿足條件是

                                                  A*A-1 =1

因此對於模倒數,用上述相同的邏輯,一個模n的倒數R(mod n)就是R-1因為

                                                  (R*R-1)(mod n) =1 (mod n)


這個才是模倒數真正的定義,如同乘法運算所陳述的倒數語意,當乘上某一個數之後經運算等於1,該數就是此運算倒數,因模運算產生序列必定在模序列 0 .. n-1 之間,因此只要在該範圍內找尋d判斷  (R*d)%n==1 如果成立那d就是R的模倒數(modulus inverse), 但是當 n 數字太大時, 上述找尋將花太久的時間. 有個演算法叫作Extended Eculidean Algorithm可以改善這個問題:

p.s. 在模運算中的倒數, 須注意它並不是中文語意中的某個數的倒數,它只是一個名詞用語 modulus inverse. 在中文語意中一個整數N的倒數是 1/N, 它必定小於1, 但模運算中的倒數並非這個意思, 實際上模n倒數是一個介於序列0 .. n-1之間的一個整數, 如果模運算結果等於 0 那它必定是模n的倍數

用 javascript 簡單寫 RSA 編解碼

1. 先安裝 nodejs 及 npm,
     sudo apt-get install nodejs  npm
2. 再用 npm 安裝 big-integer
     npm install big-integer

3. 用 openssl 產生 2048 bits 密鑰
     openssl genesa 2048 -out my.pem
     openssl rsa -text -in my.pem -out my.txt

4. 將 d, n, e 從  my.txt 擷取出來並利用 tr 及 bc 轉成 16 進位大整數的字串供程式編解碼使用
5. 使用 geditor 寫 java 程式:

// myrsa.js
var bigInt = require("big-integer");

sd="7E134469882EC2FA67B3142B96240043BDDCB946956773301736A81E45B5378CB6C11506C6BD2B4367E200B5D31C9F05C81220C43E9E7F405A3FB64C951F559B00F7F1586AE251E1EFE6793B43FBF2A283883C4843FF593ADD0273ED2786545AE42539911A9415AB41686E367A838E4CD4F6C6A2AC5B41B821651DD857DB29110DDAF7C2044B1227696E5F1EEC6FCAECD021DC4AD318937DAF58130B5594308D8EE7F6CCBB6CEE7B9B71CC2002FE342609E0717EB4FA67B61D55D11DFBABC6933FDA203400405E71CE87450968216A8C883187B562207216F213B12931F8D8AD4DF2361BC3FC404C8B088F33C0F66B7CAD2EDD1D796182DDC51153B34EBE7335";
sn="00CCCC68E135CDAD3A1BA300BBE2AEA65BD45F73EAD963A4C087E1E4CF60050F0E9077C59F62A98BB1DF20B371756C0DF310056FD678FF11011EB486C683B6F1F5C66A93C21F532F5E7C6EF2AABBCD681F5C013E1437C28268CCCEF01D83C4DC7E462AB06D103EBE5DDDB88982000D0D113F8520AB16679B2834517E6D1605733B6E7486E9629F9DD8E366D16A1E7D6AC836795C1889B74319816550ADB4EB8E5EAE805217E28E8FCA61496AE4DC7FB5E5C2A03E77FEAFCCFEFFB1A6C9EE96F8E239F2912665BEC90E3D960A997172E0C30C12ABEA96BBEAD59DD65506619099AA1829AB3494B6A8FE694296CB5AB08961DD11FAC39A80026CB39E6E287D1ABBC1";
se="10001";
sx="123456789";
n=bigInt(sn,16);
d=bigInt(sd,16);
e=bigInt(se,16);
x=bigInt(sx,16);
console.log("原文:"+x.toString(16));
m=x.modPow(e,n);
console.log("加密:"+m.toString(16));
z=m.modPow(d,n);
console.log("解密:"+z.toString(16));

6. 執行程式
        nodejs myrsa.js

7. 輸出結果:
原文:123456789
加密: 67a35bb7ed50b1583283cd2f2500ab6db74ba3da462e5ddda9089b76ae68c0e1e224ec5383ee6aaa9807014644dc5d3ed438b4f966f940f010ec7eb37dee8ccb297aae95ba20a22bc64638577127b2dd8225db736f41e740d90b8f7d915ebea5721b9f0dc4929f0f31f591009d9480eab98e654a98365b29a8ca0efeb1b812977689b3ac33926b11318f17082304ab57668d18148166fb182c76c02b1dd6a125685f55388ce407793b6ea804896a3e84566135ad06eabd6bd3b151b5954e48d38510f47e3893c2d939ed087ae64db2d68e30cccf834b38d74728b9a34bec20545885e65167282cb57a16a1358a3d3fed9b57a9ad3d966706ee8d295f303e50fd
解密:123456789

8. 可看出最後的輸出等於原文 "123456789"

big-integer 大數運算的使用用法參考網址: https://www.npmjs.com/package/big-integer


後記:
1. 將明文(text stream)轉成16進制字串 (HEX string), 僅支援英文字母, 中文有些問題要解決
       text="IJKSDFJKEDFKDF";
       sx=""; for(i=0;i<text.length;i++) sx+=text.charCodeAt(i).toString(16)
2. 將16進制字串 (HEX string)轉回明文(text stream), 僅支援英文字母,中文有些問題要解決
        zx=sx;
mx=""; for(i=0;i<zx.length/2;i++)    mx +=  String.fromCharCode(  parseInt(zx.substr(2*i,2),16) );









2016年5月19日 星期四

用 c 呼叫 openssl library 產生一對公私鑰, 並示範 RSA 編解碼程序

1. 先下載安裝 libssl-dev 開發標頭檔及程式庫
sudo apt-get update
sudo apt-get install libssl-dev
2. 開始寫程式, 需引入標頭檔 openssl/rsa.h , 呼叫 RSA_generate_key, 可以產生一對的公私鑰(public and private key), 指數(exponent)用預設的 RSA_F4 就可, 暫時不需 callback, 呼叫 RSA_size(key) 可以知道目前 key 所需佔用的長度(bytes 數), 呼叫 RSA_public_encrypt()  是用公鑰來編碼, 呼叫 RSA_private_decryp() 是用私鑰來解碼, 如果成功他們都會傳會實際編/解碼後產生的長度, 最後結束時要呼叫 RSA_free(key) 釋放 key 所佔用的記憶體.

// rsademo.c      
       #include <stdio.h>
       #include <openssl/rsa.h>
       #include <string.h>
       main(){
              int               keylen, enclen, declen,i;
              char            enc[256],dec[256];
              char            *b="1234";
              RSA             *key=NULL;
              key=RSA_generate_key(2048,RSA_F4,NULL,NULL);
             // ...
enclen=RSA_public_encrypt(strlen(b),b,enc,key,RSA_PKCS1_PADDING);           declen=RSA_private_decrypt(enclen,enc,dec,key,RSA_PKCS1_PADDING);

              keylen=RSA_size(key);
              printf("keylen=%d\n",keylen);
              printf("enclen=%d\n",enclen);
              printf("declen=%d\n",declen);
             for(i=0;i<declen;i++) printf("%c",dec[i]);
              printf("\n");              

              RSA_free(key);

      }


3. 然後用 gcc 
編譯成可執行檔 ./rsademo
    # 可以直接聯結程式庫 ssl 及 crypto, 編譯成可執行檔
        gcc rsademo.c -lssl -lcrypto -o rsademo
    # 或者透過 pkg-config來產生程式庫連結, 將原始碼編譯成可執行檔
        gcc rsademo.c $(pkg-config --libs libssl openssl) -o rsademo
4. 最後執行 rsademo
        ./rsademo

後記:
1. 觀察解碼後的長度, 如果看到的比原字串長度多 1, 可能是 EOS (end of string)也被編/解碼的關係.


2. 編碼時記憶體空間需先分配好足夠的長度, 對於 2048 bits 的 key 的話, 最少需要 256 bytes(256*8=2048 bits), 避免程式當掉.


3. 編解碼輸入的長度(bytes 數)最長不可超過 key 所用的長度(bytes 數), 如果超過, 應該分段編碼(例如對於 2048 bits 編碼, 可以每 256 個為一個區塊), 
但對於使用 RSA_PKCS1_PADDING 它會插入 11 個 bytes 的數值形成 2048 bits 超大整數, 此時每個區塊明文長度最多就只能有245個,如果超過將會產生錯誤(回傳值為 -1).

4. RSA 編解碼困難點在於超大整數次方及模數的運算, 一段文碼(例如 2048 bits)將它的指數取 e 或 d 次方, 將會形成超級無敵非比尋常更大的整數, 之後再執行一個超大整數的除法運算最後再取餘數而得出暗碼或明碼. e 一般稱作公鑰指數(public exponent), 而 d 稱為私鑰指數(private exponent), 而 n 就稱為模數, 另外要注意一點是將原文 m 編碼時, 必須讓 m 小於n 否則會出錯, 通常將最高位元組設為 0 就可以辦到.

5. 產生 1024 bits 以下的 RSA 公私鑰, 來未有可能會被破解, 因此用 2048 bits 會較保險

6.  參考文章:http://www.di-mgt.com.au/rsa_alg.html , 節錄重點:

       RSA_F4 = 2^(2^4) + 1 = 65537 = 0x10001

     常用來當公鑰指數的 5 個質數分別是{  3, 5, 17, 257, 65537 }, 他共同特點是只有 2 bit 是 1, 同時也都是奇數, 可以讓指數函數的算術運算量變少, 常用的 RSA_F4 , 它就是一個 prime Fermat number, 另外選擇奇數的好處是可以讓驗證公因數時變得較簡單, 像是  (p mod e) !=1 而不是用 gcd(p-1,e)==1 的函數語法. 

     最後重要的結論, 簡單闡述了RSA演算法的編/解碼重要 3 個參數 (e,d,n ), (e,n) 是公鑰, 而 (d,n)是私鑰, m 是明文, c 是暗文(用公鑰 e 編碼過), e, d 是次方或稱指數(exponent), n 是模數(modulus), 編解碼是用 power modulus n 運算函數, 實際上 e, d 在power modulus n 運算當中是互為倒數(inverse)的反函數運算, 若 f(f'(x)) =x, 則 f 與 f' 互稱為反函數, 正如同 * 與 / 互為倒數運算是一樣的道理, 也就是說當編碼後再用反函數解碼就可以變回原文, 而 pmod(x,e,n) 的反函數正是 pmod(x,d,n) , 因此 pmod(  pmod(x,e,n), d,n )  = m = pmod( pmod(x,d,n),e,n )
而為了要找出 d, 參考文章: http://cryptofundamentals.com/rsa
             f(x) = pmod(x,e,n) -> f'(x) =pmod(x,d,n)
因此, 將 x 編碼成 c, 再將 c 還原成x 就是說:
            c=f(x)=pmod(x,e,n)  且 f'(c)=x=pmod(c,d,n)
或是說, 將 pmod 換成實際的 power modulus 運算, 定義 ^ 是次方(power)運算
            c= x^e mod n 且  x= c^d mod n
將 c 帶入後面式子, 則
            x = (x^e)^d  mod n  =x^ed mod n = pmod(x, ed, n ) 也就是說對所有的 m 來說:
            m^ed=[m^(ed-1)] *m  =m mod n 都要成立
如果說 ed-1 是 n-1 的倍數的話那麼
             ed-1=h * (n-1)
             m^(ed-1) =m^( h(n-1) ) =m^((n-1)h)
如果 n 是質數, 應用費曼定理可知 m^(n-1)=1 mod n
費曼定理:  gp-1 = 1 (mod p)  對於所有的質數 p 來說, 所有非 p 整數倍的數 g, 該方程式都成立,因此:
            med - 1 = m(n - 1)h = (mn - 1)h = 1h (mod n)
對於兩個大的質數 p, q  , 為了要滿足
                m^(ed-1)=m mod p 且 m^(ed-1) = m mod q ---> 這句話不能理解 ???
我們可以令 n=p*q, 也就是說將 n 分解成兩個質數, 利用 Extended Eclidean 演算法找出 d, 讓 ed-1 是 (p-1)*(q-1) 的倍數就可, 也就是說, 如此的話
                m^(ed-1) = m^( h*phi ) , phi=(p-1)*(q-1)
當我們將上述方程式, 帶入 mod phi 運算時, 可以知道因為 p, q 是質數的關係, 一定會滿足費曼定理. 讓 m^(h*(p-1)&(q-1)) mod (p-1)*(q-1) 等於 1
                ed =1 mod (p-1)*(q-1)

他的條件就是在 power modulus  n 的運算下, 為了滿足所有 x, 必須讓指數 e*d=1 mod n 運算之下才會成立, 因就可以找出 d, 而 d 大於 1但小於 phi=(p-1)*(q-1), 因此讓 d 在 2 .. phi 之間測試 e*d=1 mod phi 就可以找到. 但因為 e, d , phi 是大整數, 所要花的時間就會隨 d 及 phi 的數量級而加大, 這個找出 d 的運算函數就叫 inverse modulus.

                m = (me mod n)d mod n = (md mod n)e mod n
最後文章提到的 RSA 它的演算法也節錄下來:      

  • n = pq, where p and q are distinct primes.
  • phi, φ = (p-1)(q-1)
  • e < n such that gcd(e, phi)=1
  • d = e-1 mod phi.
  • c = me mod n, 1
  • m = cd mod n.
7. 參考以下兩篇文章, 因 bc 可以處理大整數運算, 我們用 bc 語法寫一段小程式來驗證,任取兩個質數 2213, 2663 以及一般常用的公鑰 e =RSA_F4 = 65537 為例 
http://www.codedata.com.tw/social-coding/rsa-c-implementation/
https://www.vidarholen.net/contents/blog/?p=24

      #!/usr/bin/bc
      # bc0.txt  for bc script
       define pmod(b,e,n) { if(e == 0 ) return 1; if(e == 1) return b%n; rest=pmod(b^2%n,e/2,n); if((e%2) == 1) return (b*rest)%n else return rest; }
        define modinv(e,phi) { for(d=2;d<phi;d++ )  if( ((e*d)%phi )==1 ) return d;  }
        e=65537     
        p=2213
        q=2663
        n=p*q
        phi=(p-1)*(q-1)
        d=modinv(e,phi)
        m=3320
        c=pmod(m,e,n)
        b=pmod(c,d,n)
        c
        b
        quit


將上述文字檔改成可執行檔, 交給 bc 執行
chmod 755 bc0.txt 
./bc0.txt 
或是用 pipe 方式餵給 bc 都可以看出編解碼的結果
cat bc0.txt | bc
以下是輸出結果, 由最後一行可知 b=3320 , 它等於原文 m=3320, 演算法看來是正確的
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
880575
3320

8. 使用 openssl 工具產生 private key 時若遇到 'unable to write 'random state' 警告訊息時, 極有可能是家目錄下的 .rnd 檔案( ~/.rnd ) 所有權是 root, 用 sudo rm -f ~/.rnd 將它刪除就可以了.

9. 參考文章: https://www.vidarholen.net/contents/blog/?p=24
   使用openssl 工具可產生2048 bits 私鑰密碼檔:
           openssl genrsa -out private.pem 2048
   使用openssl 工具可以秀出 private key 的內容:
           openssl rsa -text -in private.pem -noout
   其中 modulus 就是 n
            publicExponent 就是 e
            privateExponent 就是 d
            prime1 就是質數 p
            prime2 就是質數 q,
            exponent1 是 dp
            exponent2 是 dq
            coefficient 是 qinv
    他們都是 hex 編碼的大整數, 我們可以截錄下來放在一個檔案, 假設檔名設成 bc1.txt
00:b9:5f:63:3d:71:db:4d:25:f4:5c:4a:e1:51:68:
88:d4:0b:22:71:b2:f0:0a:bb:f6:1c:67:ca:8c:6b:
f0:44:10:f5:df:0b:b8:da:66:c5:36:27:45:54:67:
90:3a:85:9b:ef:12:72:93:1d:2b:2b:21:91:34:22:
74:48:9d:bc:da:26:ad:55:97:3a:31:78:7f:b9:6a:
32:fe:aa:aa:96:ca:a7:57:ee:aa:d6:fa:41:8c:99:
14:d5:4a:29:b2:8e:aa:d9:01:4d:bb:a0:83:b3:76:
ab:3d:dd:dd:2a:0c:34:cb:33:ea:95:80:63:9f:28:
b9:c2:d3:64:71:48:13:e9:9e:e1:ec:68:c9:9a:58:
12:3e:ea:e0:7f:4a:8b:76:75:b1:45:e8:80:06:f7:
b8:16:40:5f:76:b1:dd:1b:6b:29:ae:7b:ba:c7:6b:
b1:a5:64:43:df:dc:2c:07:9e:37:41:3e:01:14:42:
74:14:ff:04:83:76:a8:62:15:fe:17:2f:e9:a9:2b:
b0:e3:70:1a:cd:6b:88:90:dc:e1:ee:e6:70:93:6b:
9f:bf:fa:2b:d6:9c:08:40:0b:60:a0:72:59:17:d9:
eb:b8:cb:ba:95:bc:e5:cf:f9:6e:15:4b:51:bb:2c:
46:e7:21:80:5b:b8:7f:27:1b:8e:c0:73:8d:9e:3e:
68:41

想辦法在檔案前頭加入 ibase=16及換行符號, 將非數字的符號' :\n" (空白, 帽號, 換行 ...等字元)刪除, 並將小寫 a-f 改成用大寫 A-F 取代,最後頭加入換行符號, 之後再餵給 bc 來讓它解出 10 進位大數數,

            { echo 'ibase=16'; cat bc1.txt|tr -d ' ' | tr -d ':\n ' | tr a-f A-F; echo; } | bc

底下就是它的輸出結果:

23401123825161624750491762697897416491699515070048588881539043884916\
52614404727809769029861203264031498390186435703393639991286183531157\
12785557124600390505679222866777702774446692580862083629674398773226\
16285779332858629766558102331637938506289693092631211043031316873250\
92980542341603836495831764652366744692302703394520273759613507298621\
82930057253749148266690206642103206040566846317435137365132327503637\
04634378460871449128239211577985912753381865425918675766146074271491\
42328690031389528282509796743158462464410463960615431436676313304051\
56067895924160222235603357002674929377044727742368871154241543022564\
08641
超長的一個大數字

2016年5月14日 星期六

linux mint 架設 nginx https server 並支援 fastcgi

1. 先用 sudo 安裝 nginx http server 並支援 fastcgi
        sudo apt-get install nginx fcgiwrap

2. 修改 nginx 設定檔, 它位於 /etc/nginx/sites-available/default
    找尋 server {} 區塊, 以便增加 fastcgi 可支援 /cgi-bin/ 目錄, 加入以下設定, 並讓nginx doc root設定到 /usr/share/nginx/html 目錄:
   server {
     # ...
         root  /usr/share/nginx/html;       
         location /cgi-bin/ {
       gzip off;
      fastcgi_pass  unix:/var/run/fcgiwrap.socket;
      include /etc/nginx/fastcgi_params;
      fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    }
     #...
   }
   上述 fastcgi_param 指示當 http client 要求獲取 fastcgi_script_name時, 自動在該 fastcgi_script_name 前面增加 $document_root 目錄以便形成絕對位址找到正確路徑,此時 SCRIPT_FILENAME=$document_root$fastcgi_script_name. 


3. 同樣在 server區塊內, 可以增加  https  支援並監聽 443 port, 加入以下設定:
       listen 443 ssl;
ssl on;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

4. 產生 /etc/nginx/ssl 目錄, 並在該目錄下執行 openssl,  按照螢幕指令輸入資料, 便可產生兩個密鑰檔案  nginx.key 及  nginx.crt
      sudo mkdir /etc/nginx/ssl
      sudo cd /etc/nginx/ssl
      sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt

5. 重新啟動 nginx
      sudo service nginx restart

6. 如果成功, 使用瀏覽器開啟網址  https://127.0.0.1, 便可看到 nginx server 所顯示出的訊息

p.s.
1. nginx 的 config file 在 location 區塊可以利用 try_files 來取代 if 的語法, 或是用來找尋檔案, 但須注意的是只有最後一個參數會導致 nginx 內部重新導向(例如 cgi 程式等), 因此像是使用 script 所寫的檔案時就要小心, 僅適合放在最後一個參數, 若放在中間一旦被匹配到, 則導致 nginx 直接處理並將整個源碼(source code)傳送到客戶端(client). 因此最保險的作法是將源碼放在一個目錄(例如 /cgi-bin)裡, 再利用 location /cgi-bin { } 指引來設定該目錄存取方式, 在此區塊內不要使用 try_files,  如此一來存取到該目錄內的所有檔案就比較能獲得控制.
2. 在 config file 內每一行結尾要使用分號(;)做結束, 否則 service nginx restart 會顯示出錯誤訊息
3. 更新 nginx 到最新版讓它支援 http2, 目前最新版是1.10.0, 更新方法詳見文章: http://nginx.org/en/linux_packages.html#distributions
   (a)  下載 nginx key 檔案, 避免升級時產生警告,  並加入 apt-key
          sudo wget  http://nginx.org/keys/nginx_signing.key
          sudo apt-key add nginx_signing.key
       
   (b) 修改 /etc/apt/sources.list, 其中 codename 須依照系統自行修改, 我用的是 linux mint 17.3(用 unmae -a 查, 他其實是 ubuntu 14.04)所以 codename 是 trusty, 因此加入以下兩行文字
         deb http://nginx.org/packages/mainline/ubuntu/ trusty nginx
         deb-src http://nginx.org/packages/mainline/ubuntu/ trusty nginx
   (c) 接著再下達更新指令, 但更新前先備份好設定檔(例如 /etc/nginx/sites-available/default 等)
         sudo apt-get update
         sudo apt-get install nginx
   (d) 修改 /etc/nginx/sites-available/default 文件, 將 ssl 所需文字從剛剛備份檔複製貼上, 但須將listen 443 ssl 更改成  listen 443 ssl http2;並添加一行 ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
我的 /etc/nginx/sites-available/default 完整內容如下:

    server {
listen 443 ssl http2 default_server;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
   location /cgi-bin/ {
    gzip off;
fastcgi_pass  unix:/var/run/fcgiwrap.socket;
include /etc/nginx/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;
}
location / {
try_files $uri $uri/ =404;
}
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
     ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
   }

   (e) 重新啟動 nginx
           sudo service nginx restart
   (f) 查看目前 nginx 版本:
            nginx -v
   (g) 用瀏覽器瀏覽 http://127.0.0.1 時他會自動重新導向 https://127.0.0.1 完成!
   (h) 要檢視 http2 是否真的有在運作, 參考文章:  https://www.nginx.com/blog/early-alpha-patch-http2/ 可以安裝 http/2 and SPDY indicator,如果是 chrome browser 安裝 plug-in :
https://chrome.google.com/webstore/detail/http2-and-spdy-indicator/mpbpobfflnpcgagjijhmgnchggcjblin?hl=en
如果是 firefox 則安裝 plug-in  : https://addons.mozilla.org/en-us/firefox/addon/spdy-indicator/

4. 如果發現 cgi 程式無法存取硬碟時目錄時, 可能是權限不足, 用 sudo 建立目錄並開放權限給 www-data
    sudo mkdir /usr/share/nginx/html/cgi-bin
    sudo mkdir /usr/share/nginx/html/cgi-bin/db
    sudo chown www-data. /usr/share/nginx/html/cgi-bin/db