2021年8月26日 星期四

TensorFlow.js 學習筆記 (一) : Javascript ES6 的新語法 (上)

最近在看 Tensorflow.js 的書時發現裡面用了許多 Javascript ES6 的新語法, 因為很陌生而感到有點挫折, 為了能繼續讀下去, 所以就先抽點時間學習一下. 

參考書籍 :

# Javascript 函數活用範例速查辭典 (博碩 2015, 山田祥寬)
Javascript Tensorflow.js 人工智慧教本 (碁峰 2020, 陳會安)


1. 範疇, 區域變數與全域變數 :

範疇 (scope) 是指變數在程式中的作用範圍 (可存取到), 在此範圍之外存取該變數會出現 Undefined 或 Cannot be referenced 錯誤. 在 ES6 之前都以關鍵字 var 宣告變數, 其作用範圍是以函式為分界, 函式內所宣告的變數只能在函式內存取, 不可在函式外存取, 若函式內宣告了一個與全域變數同名之區域變數, 則存取該變數會優先參照區域變數, 例如 : 



<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>ES6 測試</title>
 </head>
 <body>
 <script>
   var a=1;              //全域變數
   function test(){
     var a=2;            //區域變數
     var b=3;            //區域變數
     document.write("存取函式內變數 a=" + a + "<br>");    //存取區域變數 a=2
     document.write("存取函式內變數 b=" + b + "<br>");    //存取區域變數 b=3 
     }
   test();
   document.write("存取函式外變數 a=" + a + "<br>");      //存取全域變數 a=1
   //document.write("存取函式內變數 b=" + b + "<br>");   //ReferenceError
 </script>
 </body>
</html>

此例變數 a 在函式內外均有定義, 但在函式內會優先參照區域變數, 不會存取外面的全域變數. 而只定義於函式內的區域變數 b 只能在函式內存取, 在函式外存取會出現 "ReferenceError: b is not defined" 錯誤訊息. 結果如下 :

存取函式內變數 a=2
存取函式內變數 b=3
存取函式外變數 a=1

但是在函式內可以存取外部之全域變數, 只要函式內沒有定義同名區域變數即可, 因為 Javascript 解譯器如果在函式內找不到 var 變數的定義, 預設會自動向外尋找同名的 var 變數, 也就是自動上升為全域變數, 例如 : 



<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>ES6 測試</title>
 </head>
 <body>
 <script>
   var a=1;
   function test(){
     document.write("存取函式外變數 a=" + a + "<br>");  //無同名區域變數, 存取全域變數
     }
   test();
   document.write("存取函式外變數 a=" + a + "<br>");  //存取全域變數
 </script>
 </body>
</html>

結果如下 : 

存取函式外變數 a=1
存取函式外變數 a=1

但如果函式內有宣告同名 var 變數, 則必須在宣告後再存取該區域變數, 若在宣告前存取並不會向上提升存取外部全域變數, 例如 : 



<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>ES6 測試</title>
 </head>
 <body>
 <script>
   var a=1;
   function test(){
     document.write("存取函式內變數 a=" + a + "<br>");  //不會向上提升
     var a=2;    //宣告區域變數
     document.write("存取函式內變數 a=" + a + "<br>");  //存取區域變數
     }
   test();
   document.write("存取函式外變數 a=" + a + "<br>");  
 </script>
 </body>
</html>

此例在函式內尚未定義區域變數前即存取變數 a, 這時不會向上提升存取外部全域變數, 但也不會出現錯誤, 而是輸出 undefined, 結果如下 : 

存取函式內變數 a=undefined  
存取函式內變數 a=2
存取函式外變數 a=1

以上範例演示 ES5 的 var 變數之區塊範疇是以函式為分界, 定義一個函式或呼叫一個函式運算式 (用小括號包起來的匿名函式) 即建立一個區塊範疇. 在 ES6 中則除了函式範疇外, 又新增了以 block 為分界的 let 變數, 只要是有大括號的地方 (函式, if 判斷式, 或單純大括號區塊等) 就是 let 變數的可存取邊界, 即它屬於區塊內的區域變數, 外部無法存取, 例如 : 



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      var a=1;   
      let b=1;
      {
        document.write("存取 var 變數 a=" + a + "<br>");    //自動上升為全域變數
        //document.write("存取 let 變數 b=" + b + "<br>");  //ReferenceError      
        var a=2;      //更改全域變數
        let b=2;       //區域變數 (與外部之 b 無關)
        document.write("存取 var 變數 a=" + a + "<br>");   //輸出 2
        document.write("存取 let 變數 b=" + b + "<br>");    //輸出 2
      }
      document.write("存取 var 變數 a=" + a + "<br>");     //輸出 2
      document.write("存取 let 變數 b=" + b + "<br>");      //輸出 1
    </script>
  </body>
</html>

此例在區塊內外都定義了同名的 var 變數 a 與 let 變數 b, 區塊內的 var 變數會自動上升為全域變數, 而 let 變數則否, 且必須在存取前先宣告, 否則會有 "ReferenceError: Cannot access 'b' before initialization" 錯誤訊息 (被註解的那列), 結果如下 : 

存取 var 變數 a=1
存取 var 變數 a=2
存取 let 變數 b=2
存取 var 變數 a=2
存取 let 變數 b=1  

可見區塊內的區域變數 b 不會影響外部的區域變數 b. 

除了大括弧與函式形成的區塊範疇外, if 判斷式也會形成一個範疇, 例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      var a=1;
      let b=1;
      if (true) {     // if 判斷式區塊
        var a=2; //向上提升為全域變數
        let b=2; //區域變數
        document.write("存取內部 var 變數 a=" + a + "<br>");
        document.write("存取內部 let 變數 b=" + b + "<br>");
        }
      document.write("存取外部 var 變數 a=" + a + "<br>");
      document.write("存取外部 let 變數 b=" + b + "<br>");
    </script>
  </body>
</html>

此例的 if 判斷式形成一個區塊範疇, 內部同名的 var 變數會自動備提升為全域變數, 因此與外部那個同名變數是同一個實體, 但 let 變數則否, 結果如下 : 

存取內部 var 變數 a=2
存取內部 let 變數 b=2
存取外部 var 變數 a=2
存取外部 let 變數 b=1

同名之 let 變數在同一區塊內只能宣告一次, 重複宣告會出現 SyntaxError 語法錯誤 (反觀 var 變數則允許重複宣告), 例如 : 



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      var a=1;
      let b=1;   
      document.write("存取 var 變數 a=" + a + "<br>");
      document.write("存取 let 變數 b=" + b + "<br>");
      var a="Hello";       //var 變數可重複宣告
      //let b="Hello";     //SyntaxError : 重複宣告同名 let 變數
      b="Hello"             //更改 let 變數之值
      document.write("存取 var 變數 a=" + a + "<br>");
      document.write("存取 let 變數 b=" + b + "<br>");
    </script>
  </body>
</html>

此例被註解的那列因為重複宣告同名的 let 變數, 這會出現 "SyntaxError: Identifier 'b' has already been declared" 錯誤訊息. 結果如下 : 

存取 var 變數 a=1
存取 let 變數 b=1
存取 var 變數 a=Hello
存取 let 變數 b=Hello

位於最上層的不管是 let 或 var 變數, 在下層區塊中只要沒有同名均可存取得到, 例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      let b=1;
      function test(){
        if (b==1){
          var a=2;    //區域變數
          b=2;          //全域變數
          let c=2;     //區域變數
          document.write("if 區塊內 c=" + c + "<br>"); 
          }
        document.write("函式內 a=" + a + "<br>");
        document.write("函式內 b=" + b + "<br>");
        //document.write("函式內 c=" + c + "<br>"); //not defined (if 區塊之區域變數)
        }
      test();
      //document.write("函式外 a=" + a + "<br>"); //not defined (if 區塊之區域變數)
      document.write("函式外 b=" + b + "<br>");
      //document.write("函式外 c=" + c + "<br>"); //not defined (if 區塊之區域變數)
    </script>
  </body>
</html>


此例分別用 var 與 let 定義了兩個外部變數, 兩者在內部並無定義區域變數, 所以就自動上升為外部全域變數, 結果如下 :

if 區塊內 c=2
函式內 a=2
函式內 b=2
函式外 b=2

另外我在 "Javascript Tensorflow.js 人工智慧教本" 這本書的第 12 章看到用迴圈比較 let 與 var 變數差異的範例, 測試如下 :


 
<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      for (var i=0; i<5 ; i++){   //以 var 變數進行迭代
        setTimeout(function(){
          document.write("第 " + i + "次<br>");
          }, 100);      //100 毫秒後執行回呼函式
        }
    </script>
  </body>
</html>

此例使用 var 變數 i 進行迭代, 並於迴圈中呼叫 setTimeout() 定時程式在延遲 100 ms 後呼叫回呼函式, 由於 var 變數會自動提升為全域變數, 因此 100 ms 後迴圈早已跑完, 此時 i 已經變成 5, 所以會輸出 5 個 "第 5 次", 結果如下 : 

第 5次
第 5次
第 5次
第 5次
第 5次

如果用 let 變數進行迭代結果就不一樣了, 例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      for (let i=0; i<5 ; i++){              //以 let 變數進行迭代
        setTimeout(function(){
          document.write("第 " + i + "次<br>");
          }, 100);        //100 毫秒後執行回呼函式
        }
    </script>
  </body>
</html>

此例以 let 變數 i 進行迭代, 由於 i 是區域變數, 其值被鎖在區塊內, 延遲 100 ms 後呼叫回呼函式時才會取用該次迭代之 i 值, 所以輸出不同的 i, 結果如下 : 

第 0次
第 1次
第 2次
第 3次
第 4次

有了 let 真方便, 終於克服 var 變數自動上升問題. 在還沒有 ES6 的 let 之前解決此問題的辦法是使用立即執行的函式呼叫, 將全域變數當作參數傳遞進去, 參考 :


例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      for (var i=0; i<5 ; i++){
        (function(j){setTimeout(function(){
          document.write("第 " + j + "次<br>");
          }, 100)})(i);     // 將迭代變數 i 傳入立即執行的匿名函式的參數 j
        }
    </script>
  </body>
</html>

此處將 setTimeout() 放進一個立即執行的匿名函式中, 並將迴圈迭代變數 i 傳給其參數 j, 然後用 j 來當輸出變數即可解決因迭代變數自動上升為全域變數所造成的問題, 結果與使用 let 變數一樣 :

第 0次
第 1次
第 2次
第 3次
第 4次

總之, var 變數與 let 變數主要的差別是區塊範疇不同與可否重複宣告同名變數而已, 除了範疇考量外, 基本上大部分以前用 var 變數的地方都可以用 let 取代. 

另外兩者還有一個差別, var 變數會成為 window 物件的屬性, 而 let 變數則否, 例如 : 



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      var a=1;    
      let b=1;    
      document.write("window.a=" + window.a + "<br>");
      document.write("window.b=" + window.b + "<br>");
    </script>
  </body>
</html>

此例分別宣告了一個 var 與 let 變數, 其中 var 變數 a 會成為 window 物件的屬性, 而 let 變數 b 則否 (顯示 undefined), 結果如下 : 

window.a=1
window.b=undefined

所以 var 變數非必要最好不要常用, 因為它會加重瀏覽器的記憶體需求, 特別是在進行向量運算時, 最好使用 let 變數與 const 常數. 


2. 常數關鍵字 const :

ES6 以前 Javascript 沒有常數的語法, 只是在慣例上用大寫識別字標示此為常數而已 (但保留了 const 這關鍵字). 但在 ES6 時則引進了 const 關鍵字來宣告常數 (注意, 常數須在宣告時同時賦值), 其值不可變更, 更改一個常數的值將產生 "TypeError: Assignment to constant variable" 錯誤, 例如 :



<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>ES6 測試</title>
 </head>
 <body>
 <script>
   const PI=3.14159;    //宣告常數時必須賦值
   //PI=3.14;  //TypeError: Assignment to constant variable   
   document.write("常數 PI=" + PI + "<br>");
 </script>
 </body>
</html>

結果如下 :

常數 PI=3.14159

此例若將 PI=3.14 這列前面的註解拿掉將產生錯誤而無法產生如上結果. 

雖然常數不可改變, 但對於物件實體而言, 其屬性仍然是可更改的, 不可更改的只是物件參考本身而已, 例如 : 



<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>ES6 測試</title>
 </head>
 <body>
 <script>
   const math={k:'PI', v:3.14159};      //建立常數物件
   document.write("常數 math=" + Object.entries(math) + "<br>");
   //math={k:"PI", v:3.14};       //不可更改常數物件參考    
   math.k='pi';         //可以更改常數物件屬性值
   math.v=3.14;      //可以更改常數物件屬性值
   document.write("常數 math=" + Object.entries(math) + "<br>");
 </script>
 </body>
</html>

此例使用 Object.entries() 方法來顯示物件內容 (鍵值扁平化, 例如 k1, v, k2, v2, ...), 因為直接用 document.write() 輸出物件將只看到 [Object] 而已. 呼叫 console.log() 也可以顯示物件內容, 但要開啟開發者頁面才行. 結果如下 : 

常數 math=k,PI,v,3.14159
常數 math=k,pi,v,3.14

可見常數物件的 key 及 value 都被更改了. 

宣告為 const 的變數或常數無法像 var 變數那樣可以動態修改其值, 例如 :



<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>ES6 測試</title>
 </head>
 <body>
 <script>
   const PI=3.14159;
   document.write("常數 PI=" + PI + "<br>");
   //var PI=3.14;       //SyntaxError    
   document.write("PI=" + PI + "<br>");
 </script>
 </body>
</html>

此例若將 var PI=3.14 這列的註解拿掉將出現 "Identifier 'PI' has already been declared" 的語法錯誤. 結果如下 : 

常數 PI=3.14159
PI=3.14159


3. for of 迴圈 :

Javascript 的 for in 迴圈其 in 的對象是陣列的索引, 不是元素值, 例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      var arr=['a', 'b', 'c'];
      for (let i in arr){            //迭代變數 i 是陣列索引
        document.write("index:" + i + " value:" + arr[i] + "<br>");
        }
    </script>
  </body>
</html>

此例用 for in 迴圈走訪陣列, 其迭代變數 i 代表陣列之索引, 其值須用 arr[i] 存取, 結果如下 : 

index:0 value:a
index:1 value:b
index:2 value:c

在 ES6 裡新增了 for of 迴圈來走訪類似陣列的物件 (含字串), 但與 for in 不同的是, 其迭代變數是陣列元素而非其索引, 例如 : 


 
<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      var arr=['a', 'b', 'c'];
      for (let e of arr){             //迭代變數 e 是陣列元素
        document.write(e + "<br>");
        }
    </script>
  </body>
</html>

此例使用 for of 迴圈走訪陣列, 其迭代變數 e 代表陣列元素值, 結果如下 : 

a
b
c

如果在 for of 迴圈中同時取得索引與元素值, 則須呼叫陣列的 entries() 方法, 此方法會傳回 [i, e] 的陣列迭代器物件, 走訪此迭代器即可同時存取元素值與其索引, 參考 :


例如 :



<!doctype html>
<html>
  <head>
   <meta charset="UTF-8">
   <title>ES6 測試</title>
  </head>
  <body>
    <script>
      var arr=['a', 'b', 'c'];
      for (let [i, e] of arr.entries()){     //走訪 arr.entries() 傳回之陣列迭代器
        document.write("arr[" + i + "]=" + e + "<br>");
        }
    </script>
  </body>
</html>

結果如下 : 

arr[0]=a
arr[1]=b
arr[2]=c



沒有留言 :