2019年9月16日 星期一

網頁技術速學筆記 (三) : Javascript 基礎

在學過 HTML 與 CSS 基礎後, 應該先學習 Javascript 基本語法再來學習表單元件, 表單是互動網頁的主角, 而 Javascript 則是互動網頁的靈魂, 也是網頁前端 (瀏覽器) 上唯一的程式語言 (以前 IE 上除了 Javascript 外還可以用 VBScript, 但 IE11 以後微軟已經放棄在 IE 上支援 VBScript 了).




本系列之前的筆記參考 :

申請免費 PHP 虛擬主機 000a.biz 與 000webhost.com
網頁技術速學筆記 (一) : HTML 基礎
網頁技術速學筆記 (二) : CSS 基礎

此筆記主要是參考下列書籍文章整理而成 :
  1. Speaking Javascript (歐萊禮)
  2. 網頁設計必學的程式實作技術第二版 (博碩)
  3. 重新認識 JavaScript
關於 Javascript 程式的背景摘要整理如下 :
  1. Javascript 源自 1995 年網景工程師 Brendan Eich 為該公司的 Netscape 瀏覽器所設計的解譯式程式語言, 並於 1997 年被 ECMA 當作基礎制定了 ECMAScript 規範 ECMA-262. Brendn 後來創立了 Mozilla 基金會.
  2. Brendan Eich 在極短的時間 (約 10 天) 內寫出了 Javascript 架構, 目的是避免 Netscape 採用其它的程式語言. 語法 (基本型別與物件) 主要參考 Java; 字串, 陣列, 與正規表達式參考 Python 與 Perl; 一級函數 (first class function) 參考 Scheme; 原型繼承則來自 Self. 
  3. Javascript 的正式名稱為 ECMAScript, 事實上 Javascript 目前是 Oracle 擁有的註冊商標 (Oracle 購併 Sun 而取得), Mozilla 是少數獲准可公開使用 Javascript 名稱的單位. 
  4. Javascript 是一個動態 (dynamic) 與弱型態 (weakly-typed) 語言, 即定義一個變數時不需宣告其資料型別, 變數僅是一個指向記憶體的容器, 變數的資料類型與其值相關, 可以隨時改變所參考之資料型態. 動態語言是在執行時才進行變數之資料型別檢查, . 
  5. Javascript 是一個直譯語言 (Interprete), 原始碼不須經過編譯, 而是透過瀏覽器提供的執行環境 (即內建之 Javascript 直譯器) 即時將原始碼解譯成可執行之機器碼. 直譯語言通常無法獨立執行而是依賴於執行環境, 其效能取決於直譯器之速度. 
  6. Javascript 是一個物件導向 (Object-oriented) 語言, 但與一般以類別為基礎的物件導向語言 (例如 Java 與 C++ 等) 不同之處是, Javascript 的物件是以物件原型為基礎 (Prototype-based) 的, 它是以現成的 Prototype 物件來建立其他物件, 而不是以類別 (抽象的資料型態), 亦即在 Javascript 中類別的功能是用 Prototype 物件實例來達成. 
  7. Javascript 中的任何物件都有一個 protptype 屬性, 其值為一個 Prototype 物件, Javascript 所有物件均繼承自 Prototype 物件, 其 prototype 屬性就是用來擴充物件之屬性與方法. 採用這種方式的原因是可以減少大量物件耗用記憶體空間的情形. 
參考 :

https://zh.wikipedia.org/wiki/JavaScript
https://en.wikipedia.org/wiki/JavaScript
編譯語言 VS 直譯語言
動態型別的衰退
https://zh.wikipedia.org/wiki/類型系統


執行 Javascript 有兩種方式 :
  1. 瀏覽器 :
    瀏覽器是 Javascript 最早也是現成的執行環境, 只要在網頁中嵌入 Javascript 程式碼或連結外部 .js 檔即可. 
  2. Node.js 命令列 :
    需安裝 Node.js, 它提供一個命令列 (CLI) 介面可用互動方式執行 Javascript 程式碼, 無需依靠網頁與瀏覽器. 
Node.js 是使 Javascript 成為網站後端執行語言的功臣, 參考 :

Node.js 學習筆記 (一) : 安裝 Node.js

以下的範例如果與瀏覽器無關為了不占篇幅會使用 Node.js REPL 來測試, 否則即用網頁來測試.


一. Javascript 的載入與執行 : 

在網頁中載入 Javascript 的方法有三 :
  1. 在網頁標頭 head 內以 link 元素連結外部 js 檔 
  2. 在網頁標頭 head 內以 script 元素嵌入
  3. 在網頁主體 body 內 (也可以在 body 外) 以 script 元素嵌入 
注意, 外部 .js 檔純粹只是 Javascript 程式碼, 不可含有 <script>  </script> 標籤.

<!DOCTYPE html>
<html>
<head>
  <title>CSS 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link src="js/myscript.js">      
  <script>
    <!-- Javascript codes --> 
  </script>
</head>
<body>
  <script>
    <!-- Javascript codes --> 
  </script>
</body>
</html>

Javascript 程式碼的執行方式有兩種 :
  1. 依序執行 (sequential) :
    按照載入順序決定執行先後, 在 head 中嵌入或連結的 Javascript 程式碼會在網頁主體載入完成之前被執行, 而嵌入在 body 中的程式碼則要等到網頁被載入後才執行.
  2. 事件驅動 (event-driven) :
    程式碼寫在函式中, 當使用者在網頁中因輸入觸發滑鼠或鍵盤事件呼叫函式時執行. 
互動式網頁就是使用表單搭配事件驅動達成的.


二. 註解與輸出 : 

Javascript 的註解方式有兩種 :
  1. 單行註解 : 在 // 後面的程式碼會被視為註解而不被解譯
  2. 多行註解 : 在 /* 與 */ 之間的程式碼會被視為註解而不被解譯
善用註解可使程式碼易於維護.

程式在開發階段除錯常需要輸出相關變數之值, Javascript 可透過下列四種方法在瀏覽器中輸出訊息 :


 Javascript 輸出方式 說明
 alert(str) 在輸出視窗中顯示變數 str 之值
 document.write(str)  在網頁內容 (body) 輸出變數 str 之值
 document.getElementByID(myID).innerHTML(str) 將 id=myID 的網頁元素內容以 str 取代
 console.log(str) 在瀏覽器主控台輸出變數 str 之值


其中 alert() 與 document.write() 最方便, 因為可直接使用, 不像 innerHTML 需要在網頁中準備一個具 id 屬性的元素; 而 console.log() 則要開啟瀏覽器的主控台, 在 Chrome 瀏覽器按 F12 即可開啟 Console 主控台.

這裡用到了瀏覽器 DOM 樹狀模型的最上層物件 window, 它代表了瀏覽器視窗, 而其子物件 document 則代表了網頁文件 (.htm). alert() 是 window 物件的方法, 正確來說應該用 window.alert(), 但使用最上層物件的方法與屬性可省略 window 不寫.

呼叫 document.getElementById() 可取得指定 id 的元素物件, 其 innerHTML 屬性即為元素之內容 (即夾在開始與結束標籤之間的字串). 呼叫  document.write() 會在網頁主體 body 內插入元素.


範例 : 註解與輸出測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <p id="output"></p>   <!--先繪製-->
  <script>
    /* 輸出測試 */
    document.write("<b>Hello World!</b>"); //後繪製
    document.getElementById("output").innerHTML="Hello World!"; //動態更改
    alert("Hello World!");  //彈出視窗
    console.log("Hello World!");   //主控台
  </script>
</body>
</html>




此例網頁中有一個 id=output 的 p 元素, 載入時並無內容, 而是利用元素的 innerHTML 屬性填進去的. 此 p 元素是 body 內原有的, 因此會顯示在上面, 而用 document.write() 寫進 body 內的會排在後面.


三. 變數 : 

1. 識別字 (identifier) : 

Javascript 的變數 (函數與物件名稱也適用) 名稱稱為識別字有如下限制 :
  • 有分大小寫, hello 與 Hello 是不同的識別字. 
  • 第一個字元必須是英文字母, 底線 _, 或錢號 $, 其餘字元也必須英文字母, 底線或數字, 不可用其他字元 (例如特殊字元). 
  • 不可使用保留字, 例如 for, if 與 then 等.
Javascript 的保留字有下列 44 個 :


 arguments break case catch
 class const continue debugger
 default delete do else
 enum export extends false
 finally for function  if
 implements import in instanceof
 interface let new null
 package private protected public
 return static super switch
 this throw true try
 typeof var void while


另外下列三個特殊值雖非關鍵字, 但也不能拿來當識別字用 :
  • NaN (非數字)
  • undefined (未賦值之變數值)
  • Infinity (無限大)
簡言之, 識別字不可用數字開頭, 字元中間不可有空白, 也不能使用特殊字元例如 -, @, $, #, %, ? 或句點等 (句點是物件的運算子). 最好避免使用英文字母 O, L 的小寫與 I 因其與數字的 0, 1 容易混淆.

下面都是不合法的變數名稱 :

2at (數字開頭)
chi nese (有空白)
interest% (有特殊符號)
my.name (有特殊符號)

Javascript 使用 var 宣告變數, 可在宣告時同時賦值, 沒有賦值的變數其值為 undefined. Javascript 是動態 (dynamic) 與弱型別 (weakly-typed) 語言, 宣告變數時不需指定所儲存之資料的型別, 且變數可隨時改為不同之資料型態, 例如 :

var a;                       //未賦值預設為 undefined
var b=100;              //b 參考數值
b="hello world";    //b 參考字串


2. 變數的有效範圍 (scope) : 

Javascript 的變數有效範圍以函數為界, 在函數外面的最上層變數稱為全域變數 (global variable), 它可以在程式中的任何地方被存取; 函數之參數以及用 var 宣告的變數均為區域變數, 只能在函數內存取, 例如 :

> var a=10;
> function foo(b) {
... var c=100;
... return a+b;                //a 在函數內找不到, 就往外找到全域變數 a=10
... }
> foo(30)                  //傳回 10+30=40
40
> b                            //函數外無法存取函數內之區域變數
ReferenceError: b is not defined     //b 是區域變數 (參數) 在外部為未定義
> c                            //函數外無法存取函數內之區域變數
ReferenceError: c is not defined     //c 是區域變數在外部為未定義

在函數內用 var 宣告的變數為區域變數, 若與外部變數同名, 則函數將存取區域變數而非全域變數, 例如 :

> var a=10;
> function foo(b) {
... var a=20;      //用 var 宣告為區域變數, 不會影響外部全域變數
... return a+b;    //取用區域變數 a, b
... }
> foo(30)      //傳回 20+30=50
50
> a               //全域變數不受影響仍為 10
10 

函數內的區域變數最好都用 var 宣告, 否則它會被向上提升至全域等級, 若同名將影響外部全域變數之值, 可能使程式出現意外的結果, 例如 :

> var a=10;
> function foo(b) {
... a=20;              //沒有用 var 宣告會被提升, 可能影響全域變數之值
... return a+b;
... }
> foo(30)
50
> a                  //全域變數 a 被函數內的 a=20 改變了
20

此例在在 foo() 內執行 a=20 時因為沒有用 var 宣告 a, 因此會向上提升存取外部的全域變數, 使得全域變數 a 的值也被改成 20, 這有點像是 Python 中在前面加一個 global, 變成 global a=20 的效果.


四. 資料型別 : 

Javascript 的資料型別共 6 種, 可分為基本型別與參考型別 2 類 :


 Javascript 資料型別 說明
 基本型別 (primitive) 數值 (number), 字串 (string), 布林 (boolean), undefined
 參考型別 (reference) 陣列 (array), 物件字面值 (object literal), 正規表達式, null


參考型別也稱為物件型別, 事實上在 Javascript 中基本型別以外的都屬於物件型別. Javascript 的基本型別設計來自 Java. 基本型別與物件主要的差別在於, 基本型別是不可變的 (immutable), 而物件是可變的 (mutable), 因為它們儲存在不同的記憶體分區裡, 因此比較時基本型別是比較其值, 而物件則是比較其參考.

以記憶體儲存位置來說, 基本型別資料儲存在堆疊記憶體 (stack memory) 區, 變數的位址儲存的就是變數的值, 因此變數與其值是在一起的; 而參考型別與 null 則儲存在動態指配的堆積記憶體 (heap memory) 區, 其位址 (稱為參考指標) 會傳到堆疊區內的變數中儲存, 因此變數與資料是分離的, 亦即堆積中的物件資料是透過堆疊中的參考指標間接存取的. 因為存在這種差別, 呼叫函數時, 若引數是基本型別則採用傳值呼叫, 傳到函數內的是外部變數的複本; 若引數是參考型別則是傳參考 (址) 呼叫, 在函數內會存取到同一份資料.

Javascript 有兩個特殊的值 : null 與 undefined, 它們都是無的意思, null 表示 no object (無物件), 而 undefined 表示 no value (無值). null 是一個特殊的 object 型別, 因為它並未指向堆積中的任何物件 (無物件). 而 undefined 則是用來代表未初始化的變數, 函數缺少的參數, 或者物件中缺少的屬性. 另外, 函數也被視為是一種特別的物件, 雖然 typeof 會傳回 "function".

變數的型別可用內建函數 typeof() 或保留字 typeof 檢視, 例如 typeof(a) 或 typeof a, 傳回值為字串, 參考 :

https://pjchender.blogspot.com/2016/07/javascript-typeof.html

Javascript 所有資料型別的 typeof() 傳回值摘要如下表 :


 資料型別 範例 typeof a 傳回值 (字串)
 數值 (整數) var a=123; number
 數值 (浮點數) var a=3.14159; number
 數值 (浮點數) var a=6.62607015e-34 number
 字串 var a="Hello World" string
 布林值 var a=true; boolean
 布林值 var a=false; boolean
 物件 var a={}; object
 物件 var a=new Object(); object
 陣列 var a=[]; object
 陣列 var a=new Array(); object
 函數 var a=new function() {}; function
 undefined var a; undefined
 null var a=null; object


注意 null 的 typeof 傳回值為 'object', 但它並未指向堆積中的任何物件, 這是 Javascript 中的一個無法修正的 bug.


範例 : 資料型別的 typeof 測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <p id="output"></p>
  <script>
    /* 資料型別的 typeof 測試 */
    var a;    //未初始化之變數
    document.write("typeof a=" + typeof a + "<br>");
    document.write("typeof 123=" + typeof 123 + "<br>");
    document.write("typeof 3.14159=" + typeof 3.14159 + "<br>");
    document.write("typeof 6.62607015e-34=" + typeof 6.62607015e-34 + "<br>");
    document.write("typeof 'Hello World'=" + typeof 'Hello World' + "<br>");
    document.write("typeof true=" + typeof true + "<br>");
    document.write("typeof false=" + typeof false + "<br>");
    document.write("typeof {}=" + typeof {} + "<br>");
    document.write("typeof new Object()=" + typeof new Object() + "<br>");
    document.write("typeof []=" + typeof [] + "<br>");
    document.write("typeof new Array()=" + typeof new Array() + "<br>");
    document.write("typeof function(){}=" + typeof function(){} + "<br>");
    document.write("typeof undefined=" + typeof undefined + "<br>");
    document.write("typeof null=" + typeof null + "<br>");
  </script>
</body>
</html>




可見未初始化 (宣告但未賦值) 的變數, 其值為 undefined. 陣列, 物件, 以及 null 三者 typeof 都會傳回 "object", 事實上用 new 產生的物件都是 "object". 其中 {} 與 new Object() 都是 Javascript 建立物件的方式, 而 [] 與 new Array() 則是建立陣列的方式.

基本資料型別 (primitives) 有三種 :


 資料型別 說明 對應物件
 數值 (number) 整數例如 123, 0x7b 浮點數例如 .001 或 0.001 或 1.23e-19 Number
 字串 (string) 可用雙引號貨單引號, 例如 "hello" 或 'hello' String
 布林 (boolean) true (=1) 或 false (=0) Boolean


基本型別資料有如下特點 :
  • 值與屬性是不可變的 (immutable)
  • 比較兩個基本型別變數時是比較其值
基本型別的屬性是來自其包裹器物件所附加的, 例如字串的包裹器物件為 String, 它會將字串包裹起來成為物件而擁有 String 物件之屬性與方法 :

> var str='hello'
> str.length
5
> str[0]
'h'
> str[0]='k'         //企圖將第一字元改成 'k' : 無效
'k'
> str                   //str 仍然是 'hello', 沒有改變
'hello'                   
> str.length=3    //企圖將 length 屬性改變 : 無效
3
> str.length     
5     

兩個不同基本型別變數, 只要值相同, 則 == 與 === 比較運算都會傳回 true :

> var a='Tony'
> var b='Tony'
> a==b
true
> a===b
true
> var a='123'    //數值字串
> var b=123
> a==b             //a 會被強制轉型為數值 123 後再比較
true 
> a===b           //型別不同傳回 false
false

數值字串則會自動強制轉型後再比較其值, 因此 == 也會傳回 true, 但 === 則傳回 false, 因為型別不同.


(1). 數值 :

Javascript 的數值雖然分為整數與浮點數, 但其實都是浮點數, 使用全等運算子 === 比較 10 與 10.0 傳回 true :

> 10 === 10.0
true

Javascript 有一個包裹器物件 Number() 可用來將其它資料型別轉成數值, 例如數值字串可轉成數值, 但非數值字串則無法轉換而傳回 NaN (not a number), 例如 :

> Number("3.14159")
3.14159
> typeof Number("3.14159")    //傳回 "number" 型別
'number'
> Number("abc")          //非數值
NaN
> Number("abc123")    //字串必須全為數值才能轉換
NaN

整數可用不同基底 (base) 的進位制來表示 :


 進位基底  說明
 十進位 由 0~9 數字表示, 不需冠碼
 二進位 以 0b 或 0B 開頭其餘為 0 與 1, 例如 0B1100 為 10 進位的 12
 八進位 以 0 開頭其餘為 0~7, 例如 012 為 10 進位的 10
 十六進位 以 0x 或 0X 開頭其餘為 0~9 與 a/A~f/F, 例如 0x7B 等於 10 進位的 123


關於數值用法摘要如下 :
  • 不論整數字面值 (literal) 用哪一種進位表示, 經過運算後都是傳回 10 進位值, 例如 alert(0xFF) 會傳回 255 . 
  • 整數變數可用 Number 物件的 toString(base) 方法來轉換成其他基底進位制來表示, 只要將目標基底 base 當參數傳進去即可 (未傳參數預設是傳回 10 進位). 
  • toString() 只能用於變數, 不可用於字面值 (literal), 亦即 123.toString(16) 是不允許的, 字面值必須先放到小括弧內強制運算後才能呼叫 toString(), 例如 (123).toString(16).
  • ECMAScript 不支援八進位值, 因為常會被誤解為十進位數, 故為了安全性盡量不要用八進位表示數值
下面為整數在不同基底之間的互轉範例 :


範例 : 整數基底轉換測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <p id="output"></p>
  <script>
    /* 整數基底轉換測試 */
    var a=123;
    document.write(a + "(10進位)=" + a.toString(2) + "(2進位)<br>");
    document.write(a + "(10進位)=" + a.toString(8) + "(8進位)<br>");
    document.write(a + "(10進位)=" + a.toString(16) + "(16進位)<br>");
    var a=0b1100;
    document.write(a + "(10進位)=" + a.toString() + "(10進位)<br>");
    document.write(a.toString(2) + "(2進位)=" + a.toString(10) + "(10進位)<br>");
    var a=0x7b;
    document.write(a + "(16進位)=" + a.toString() + "(10進位)<br>");
    document.write(a.toString(16) + "(16進位)=" + a.toString(10) + "(10進位)<br>");
    document.write("0xFF(16進位)=" + (0xFF).toString(10) + "(10進位)<br>");
  </script>
</body>
</html>




可見 toString() 沒有傳參數的話預設是轉成 10 進位.

浮點數可使用科學表示法, 其中指數基底用 e 或 E 均可, 後面為冪次, 表示為 10 的幾次方, 例如 1.23e2 等於 1.23*100=123. 但是由於 10 進位的浮點數在轉成 2 進位數時有些是無限循環, 儲存時需捨去或進位而造成些許誤差.


範例 : 數值資料測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <p id="output"></p>
  <script>
    /* 數值測試 */
    document.write(123 + "<br>");
    document.write(3.14159 + "<br>");
    document.write(6.62607015e-34 + "<br>");
    document.write(6.62607015E-34 + "<br>");
    document.write("0.1+0.1=" + (0.1+0.1) + "<br>");
    document.write("0.1+0.2=" + (0.1+0.2) + "<br>");
    document.write("0.1+0.3=" + (0.1+0.3) + "<br>");
    document.write("0.2+0.4=" + (0.2+0.4) + "<br>");
    document.write("0.1+0.7=" + (0.1+0.7) + "<br>");
  </script>
</body>
</html>




可見某些浮點數相加結果與預期有些微誤差, 解決辦法是將浮點數利用字串處理分成整數與小數部分, 全部用整數來處理, 參考 :

詳解Javascript中被你忽略的浮點數運算的坑,來學習吧


(2) 字串 : 

字串可放在單括號或雙括號內, 下列情況需要同時使用單括號與雙括號 :
  • 字串中含有 apostrophe 例如 We're 或 I'm 等情形需使用雙括號, 例如 :
    "We're the world"
    "Tony's car"
  • 字串中含有屬性值時可用單或雙括號嵌套, 例如 :
    var html="<div class='blabla'></div>";
    var html='<div class="blabla"></div>';
除了交替使用單括號與雙括號外, 還可以用倒斜線 \ 跳脫括號, 這樣便能只使用一種括號, 例如 :
  • 含有 apostrophe :
    "We\"re the world"
    "Tony\"s car"
  • 字串中含有屬性值 :
    var html="<div class=\"blabla\"></div>";
    var html='<div class=\'blabla\'"></div>';
Javascript 語法定義了若干逸出序列 (escape sequence), 某些特定字元前面加上倒斜線即表示字元本身或特別之意義, 這些跳脫字元如下表所示 :


 逸出序列  說明
 \0 Null 字元 (=\u0000)
 \b 後退 (backspace) 字元 (=\u0008)
 \t 水平定位字元 (tab, =\u0009)
 \n 跳行字元 (=\u000A)
 \v 垂直定位字元 (=\u000B)
 \f 換頁字元 (=\u000C)
 \" 雙括號 (=\u0022)
 \' 單括號 (=\u0027)
 \\ 倒斜線 (=\u005C)
 \uXXXX Unicode 字元 (X 為 16 進位數)


但倒斜線僅對上表中的特殊字元有逸出效果, 對於一般字元則無作用, 倒斜線會直接被忽略.

字串可用 "+" 運算子串接, 例如 :

var str="Hello" + "World";

得到的字串 str 為 "HelloWorld".


範例  : 字串逸出序列測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 字串逸出序列測試 */
    document.write("\0" + "<br>");   //不可見
    document.write("\b" + "<br>");   //不可見
    document.write("\t" + "<br>");    //不可見
    document.write("\n" + "<br>");   //不可見
    document.write("\v" + "<br>");   //不可見
    document.write("\f" + "<br>");    //不可見
    document.write('\"' + "<br>");      //雙括號
    document.write("\'" + "<br>");     //單括號
    document.write("\\" + "<br>");     //倒斜線本身
    document.write("\u3042" + "<br>");     //日文
    document.write("\uC5FD" + "<br>");   //韓文
    document.write("\u79AA" + "<br>");    //中文
  </script>
</body>
</html>


可見自 \0 至 \f 均不可見, 注意雙括號與單括號交替出現的技巧.

淺談JS中String()與 .toString()的區別


(3). 布林值 :

布林值用來表示邏輯中的真假, 因此它只有兩個字面值 : true 與 false. 下列的運算子的運算結果為布林值 :
  • 邏輯運算子
  • 關係運算子
Javascript 有一個包裹器物件建構子 Boolean() 可將其它資料型別轉成布林值, 除了下面的 6 個值傳回 false 外, 其餘均傳回 true :
  • undefined
  • null
  • false 
  • 0
  • NaN
  • '' (空字串)

例如 :

> Boolean(undefined)
false
> Boolean(null)
false
> Boolean(false)
false
> Boolean(0)
false
> Boolean(NaN)
false
> Boolean('')
false
> Boolean({})   //空物件
true
> Boolean([])    //空陣列
true


(4). null 與 undefined :

資料型別 undefined 與 null 都是字面值, 型別分別為 undefined 與 object, 兩者之真值均為 false, 亦即 if (null) {} 與 if (undefined) {} 均不會執行, 因為用 Boolean() 包裹器建構子強制轉型為布林值均傳回 false :

> Boolean(null)
false
> Boolean(undefined)
false

用 Number() 包裹器建構子強制轉型為數值時, Number(null) 會傳回 0, 而 Number(undefined) 則傳回 NaN (非數值) :

> Number(null)
0
> Number(undefined)
NaN

unndefined 會出現的場合如下 :
  • 未賦值的變數
  • 未傳入的參數
  • 無 return 的函數傳回值
  • 不存在的物件屬性
null 則出現在用正規式搜尋字串卻不匹配時, 例如 /a/.exec('xyz') 傳回 null.

檢驗一個變數是否為 null 或 undefined 要用嚴格相等性運算子 === 或 !=== :

if (a === undefined) {}
if (a === null) {}


範例 : 特殊資料型別 undefined 與 null 測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* undefined 與 null 測試 */
    var a;
    document.write("a=" + a + "<br>");
    document.write("Number(undefined)=" + Number(undefined) + "<br>");
    document.write("Boolean(undefined)=" + Boolean(undefined) + "<br>");
    document.write("a===undefined :" + (a===undefined) + "<br>");
    function foo(i) {return i;}
    document.write("foo()=" + foo() + "<br>");
    var obj={};
    document.write("obj.name=" + obj.name + "<br>");
    function bar() {var b=1;}
    document.write("bar()=" + bar() + "<br>");
    var ret=/a/.exec('xyz');
    document.write("/a/.exec('xyz')=" + ret + "<br>");
    document.write("Number(null)=" + Number(ret) + "<br>");
    document.write("Boolean(null)=" + Boolean(ret) + "<br>");
    document.write("/a/.exec('xyz')===null:" + (ret===null) + "<br>");
  </script>
</body>
</html>

結果如下 :

a=undefined
Number(undefined)=NaN
Boolean(undefined)=false
a===undefined :true
foo()=undefined
obj.name=undefined
bar()=undefined
/a/.exec('xyz')=null
Number(null)=0
Boolean(null)=false
/a/.exec('xyz')===null:true

可見當一個函數無傳回值時, 它就傳回 undefined.


五. 運算子 : 

Javascript 的敘述由運算子與運算元構成, 運算元就是上面介紹的資料型別, 而運算子就是計算的主體, 主要是算術與邏輯運算.


1. 算術運算子 : 

算術運算子即加減乘除求餘數等四則運算 :


 算術運算子 說明
 a + b 加法運算或字串串接, + 亦為正號運算子, 例如 +3
 a - b 減法運算, - 亦為負號運算子, 例如 -3
 a * b 乘法運算
 a / b 除法運算, 例如 5/2=2.5
 a % b 餘數運算, 即 a/b 之餘數, 例如 5%2 得 1
 ++a 遞增運算, 先遞增再賦值
 a++ 遞增運算, 先賦值再遞增
 --a  遞減運算, 先遞減再賦值
 a-- 遞減運算, 先賦值再遞減


算術運算子中比較需要注意的是 + 與 ++/-- 運算子 :
  • + 具有三種運算, 當運算元都是數值時為加法運算, 又可做為正數符號, +3 即 3, 還有字串串接運算功能. 如果運算元有數值與字串, 則會自左向右先進行數值計算直到遇到第一個字串運算元時, 會將前面與後面所有數值轉型為字串, 進行字串串接運算.
  • ++ 與 -- 為單元運算, 放在變數前面是先增減 1 再進行運算; 放在變數後面則是先以現值運算後再增減 1. 

範例 : 算術運算子測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 算術運算子測試 */
    document.write("10+3=" + (10+3) + "<br>");
    document.write("10-3=" + (10-3) + "<br>");
    document.write("10*3=" + (10*3) + "<br>");
    document.write("10/3=" + (10/3) + "<br>");
    document.write("10%3=" + (10%3) + "<br>");
    var a=10;
    document.write("a=" + a + "<br>");
    document.write("++a=" + (++a) + "<br>");
    document.write("a=" + a + "<br>");
    document.write("--a=" + (--a) + "<br>");
    document.write("a=" + a + "<br>");
    document.write("a++=" + (a++) + "<br>");
    document.write("a=" + a + "<br>");
    document.write("a--=" + (a--) + "<br>");
    document.write("a=" + a + "<br>");
  </script>
</body>
</html>

結果如下 :

10+3=13
10-3=7
10*3=30
10/3=3.3333333333333335
10%3=1
a=10
++a=11        //a 先增量為 11 再做 () 運算
a=11     
--a=10          //a 先增量為 10 再做 () 運算
a=10
a++=10        //a 先做 () 運算再增量
a=11             //a 已增量為 11
a--=11          //a 先做 () 運算再減量
a=10             //a 已減量為 10

可見 ++a 與 --a 都是先做增減量再做運算, 因此運算時都是增減後的新值; 而 a++ 與 a-- 都是先做運算再增減量, 故運算時都用原來的舊值.


2. 關係 (比較) 運算子 :

關係運算子又稱比較運算子, 用來比較運算元的值或型別, 運算元可以是基本型別或物件, 陣列等, 運算結果為 boolean 型別 (true/false), 主要用於迴圈與條件敘述中做為執行條件判斷 :


 關係運算子 說明
 a < b 小於
 a > b 大於
 a == b 等於 (值相等為 true)
 a <= b 小於等於
 a >= b 大於等於
 a != b  不等於 (值不相等為 true)
 a === b 全等於 (值與型別相等才 true)
 a !== b 不全等於 (值不相等者型別不同為 true)


關係運算中的運算元通常是數值資料, 主要是用來比大小; 但也可以是任何型別資料, 其中基本型別資料會自動被轉成數值或編碼後再進行比較, 規則如下 :
  • 數值字串會自動呼叫 parseInt() 或 parseFloat() 轉成數值後再比較. 
  • 兩個字串相比則是依序以字元表中的編碼值大小比較 (這會耗費較多時間). 
  • 布林值會轉換成數值再比較, false 轉成 0, 而 true 轉成 1. 
  • 物件與陣列 (參考型別) 則是比較參考位址是否相同. 
全等運算 === 除了兩個運算元值要相等外, 其型別或參考位址也必須相等才會傳回 true.


範例 : 關係運算子測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 關係運算子測試 */
    document.write("10 > 20 " + (10 > 20) + "<br>");
    document.write("10 < 20 " + (10 < 20) + "<br>");
    document.write("10 == 20 " + (10 == 20) + "<br>");
    document.write("10 >= 20 " + (10 >= 20) + "<br>");
    document.write("10 <= 20 " + (10 <= 20) + "<br>");
    document.write("10 != 20 " + (10 != 20) + "<br>");
    document.write("10.0 == 10 " + (10.0 == 10) + "<br>"); //值相同, true
    document.write("false > 1 " + (false > 1) + "<br>");  //false=0, false
    document.write("false == 0 " + (false == 0) + "<br>"); //值相同, true
    document.write("false === 0 " + (false === 0) + "<br>"); //型別不同, false
    document.write("true == 1 " + (true == 1) + "<br>"); //true=1, true
    document.write("'10' < 20 " + ('10' < 20) + "<br>");  //轉成 10 < 20, true
    document.write("'10' == 10 " + ('10' == 10) + "<br>"); //值相同, true
    document.write("'10' === 10 " + ('10' === 10) + "<br>"); //型別不同, false
    document.write("'10.2' > 10.1 " + ('10.2' > 10.1) + "<br>");
    document.write("'a' < 'b' " + ('a' < 'b') + "<br>"); //'a'=61,'b'=62
    var a={};
    var b={};  //a, b 指向不同物件, 參考位址不同
    var c=a;  //c, a 指向相同物件, 參考位址相同
    document.write("a === b " + (a === b) + "<br>"); //參考位址不同, false
    document.write("c === a " + (c === a) + "<br>"); //參考位址相同, true
  </script>
</body>
</html>

結果如下  :

10 > 20 false
10 < 20 true
10 == 20 false
10 >= 20 false
10 <= 20 true
10 != 20 true
false > 1 false
false == 0 true
true == 1 true
'10' < 20 true
'10.2' > 10.1 true
'a' < 'b' true

此例數字字串與數值相比時, 會自動將字串用 parseInt() 或 parseFloat() 轉成數值再相比; 兩個字串則直接用其 ASCII 或 unicode 相比.


3. 邏輯運算子 : 

邏輯運算子用來計算邏輯運算式, 其傳回值理論上是 true 與 false :


 邏輯運算子 說明
 a && b 邏輯 AND 運算 (a, b 均為 true 才為 true)
 a || b 邏輯 OR 運算 (a, b 任一為 true 即為 true)
 !a 邏輯 NOT 運算 (true 變 false, false 變 true)


不過因為運算元可以是任何資料型別, 不一定是布林值, 二元邏輯運算子 && 與 || 實際上是以所謂的短路 (short circuiting) 的方式計算傳回值的 :
  • && : 如果第一個運算元為 false 就傳回它, 否則傳回第二個運算元.
  • || : 如果第一個運算元為 true 就傳回它, 否則傳回第二個運算元.
亦即只要從第一個運算元的 true/false 值就決定了傳回值, 例如 :

> NaN && 123            //NaN 為 false 故傳回 NaN
NaN
> 123 && undefined   //123 為 true 故傳回 undefined
undefined
> 123 || 'hello'               //123 為 true 故傳回 123
123
> '' || 'hello'                   //空字串 '' 為 fasle 故傳回 'hello'
'hello'


範例 : 邏輯運算子測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 邏輯運算子測試 */
    document.write("2<3 && 5>4 " + (2<3 && 5>4) + "<br>");
    document.write("2>3 || 5>4 " + (2>3 || 5>4) + "<br>");
    document.write("2>3 || 5>4 " + !(5>4) + "<br>");
  </script>
</body>
</html>

結果如下 :

2<3 && 5>4 true
2>3 || 5>4 true
2>3 || 5>4 false


4. 位元運算子 : 

位元運算子是將運算元以二進位表示後對每一個位元進行左移或右移, 要注意的是, 左移時右邊空出的位子是補 0; 而右移時左邊空出的位子是補 1, 但無號右移是補 0, 對於正數而言, 有號與無號右移結果相同; 但對於負數而言, 有.

左移一位元在算術上等於乘以 2, 而右移一位元則是除以 2 (但只取商).


 位元運算子 說明
 a & b 將 a, b 對應位元進行位元 AND 運算
 a | b 將 a, b 對應位元進行位元 OR 運算
 a ^ b  將 a, b 對應位元進行位元 XOR 運算
 ~a  將 a 的每一位元進行位元 NOT 運算 (反相)
 a << b  將 a 向左移位 b 個位元 (右邊補 0)
 a >> b  將 a 向右移位 b 個位元 (左邊補符號位元 : 正補 0, 負補 1)
 a >>> b 將 a 向右無號移位 b 個位元 (左邊補 0)


範例 : 位元運算子測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 位元運算子測試 */
    document.write("0b0011 & 0b0110 = " + (0b0011 & 0b0110) + "<br>");
    document.write("0b0011 | 0b0110 = " + (0b0011 | 0b0110) + "<br>");
    document.write("0b0011 ^ 0b0110 = " + (0b0011 ^ 0b0110) + "<br>");
    document.write("~0b0000 = " + (~0b0000) + "<br>");
    document.write("16 << 1 = " + (16 << 1) + "<br>");
    document.write("16 << 2 = " + (16 << 2) + "<br>");
    document.write("16 >> 1 = " + (16 >> 1) + "<br>");
    document.write("16 >> 2 = " + (16 >> 2) + "<br>");
    document.write("-16 >> 2 = " + (-16 >> 2) + "<br>");
    document.write("5 >> 2 = " + (5 >> 2) + "<br>");
    document.write("-1 >>> 1 = " + (-1 >>> 1) + "<br>");
    var minus_one=0b01111111111111111111111111111111;
    document.write("-1 >>> 1 = " + minus_one.toString() + "<br&
  </script>
</body>
</html>

結果如下 :

0b0011 & 0b0110 = 2
0b0011 | 0b0110 = 7
0b0011 ^ 0b0110 = 5
~0b0000 = -1
16 << 1 = 32
16 << 2 = 64
16 >> 1 = 8
16 >> 2 = 4
-16 >> 2 = -4
5 >> 2 = 1
-1 >>> 1 = 2147483647
-1 >>> 1 = 2147483647

此例兩個整數 3 (0b0011) 與 6 (0b0110) 做 AND 運算 0b0011 & 0b0110 結果為 0b0010, 上面說過任何運算結果預設都是以 10 進制表示, 因此結果為 2. 這兩個數做 OR 運算結果為 0b0111=7; 做 XOR 運算 (不同才為 1) 得 0b0101=5. 整數 0 (0b0000) 做反相結果為 0b1111, 這在 2 的補數表示法裡為 10 進位的 -1 (將 1111 反向得 0000, 加 1 得 1, 故為 -1).

移位部分, 16 (=0b10000) 左移 1 位得 0b100000=32 (16 乘以 2), 左移 2 位得 0b1000000=64 (16 乘以 4); 16 (=0b10000) 右移 1 位得 0b01000=8 (16 除以 2), 右移 2 位得 0b00100=4 (16 除以 4); -16 (=0b110000) 右移 2 位得 0b111100=-4 (-16 除以 4, 負數右移左方補 1 故得 0b111100=-4, 因反相加 1 為 4 之故). 5 (0b0101) 右移 1 位為 0b0010=2, 此為 5/2 之商.

最後是無號右移, 對於正數而言, 有號與無號右移結果是一樣的, 但對負數而言, 無號右移結果會變成很大的正數, 因負數的 MSB (最左位元) 必為 1, 而無號右移左邊空出的位子是補 0, 此例中 -1 以 32 位元來說, 先從 +1 取反相再加 1 即得 -1 的 2 的補數表示 :

+1=0b00000000000000000000000000000001
1 的補數=0b11111111111111111111111111111110
2 的補數=0b11111111111111111111111111111111 (即 1 的補數加 1)
右移1 位=0b01111111111111111111111111111111 (此即程式中的 minus_one)

用 minus_one.toString() 檢驗結果是一樣的.

參考 :

javascript中負數算術右移、邏輯右移的奧祕探索


5. 指定運算子 : 

指定運算子用來將右邊的值指派給左邊的變數, 還可以與算術運算子, 邏輯運算子, 以及位元運算子結合為複合指派運算子 :


 指定運算子 說明
 a=2 指定 a 變數之值為 2
 a += 2 將變數 a 加上 2 後結果再指定給 a, 相當於 a=a + 2
 a -= 2 將變數 a 減掉 2 後結果再指定給 a, 相當於 a=a - 2
 a *= 2 將變數 a 乘以 2 後結果再指定給 a, 相當於 a=a * 2
 a /= 2 將變數 a 除以 2 後結果再指定給 a, 相當於 a=a / 2
 a %= 2 將變數 a 除以 2 取餘數再指定給 a, 相當於 a=a % 2
 a &= 2 將變數 a 與 2 做位元 AND 後再指定給 a, 相當於 a=a & 2
 a |= 2 將變數 a 與 2 做位元 OR 後再指定給 a, 相當於 a=a | 2
 a ^= 2 將變數 a 與 2 做位元 XOR 後再指定給 a, 相當於 a=a & 2
 a <<= 2 將變數 a 左移 2 位元後再指定給 a, 相當於 a=a << 2
 a >>= 2 將變數 a 右移 2 位元後再指定給 a, 相當於 a=a >> 2
 a >>>= 2  將變數 a 無號右移 2 位元後再指定給 a, 相當於 a=a >>> 2


範例 : 指定運算子測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 指定運算子測試 */
    var a=2;
    document.write("a = 2; " + "<br>");
    document.write("a += 2; //a=" + (a += 2) + "<br>");
    document.write("a -= 2; //a=" + (a -= 2) + "<br>");
    document.write("a *= 2; //a=" + (a *= 2) + "<br>");
    document.write("a /= 2; //a=" + (a /= 2) + "<br>");
    document.write("a %= 2; //a=" + (a %= 2) + "<br>");
    document.write("a &= 2; //a=" + (a &= 2) + "<br>");
    document.write("a |= 2; //a=" + (a |= 2) + "<br>");
    document.write("a ^= 2; //a=" + (a ^= 2) + "<br>");
    document.write("a <<= 2; //a=" + (a <<= 2) + "<br>");
    document.write("a >>= 2; //a=" + (a >>= 2) + "<br>");
    document.write("a >>>= 2; //a=" + (a >>>= 2) + "<br>");
  </script>
</body>
</html>

結果如下 :

a = 2;
a += 2; //a=4
a -= 2; //a=2
a *= 2; //a=4
a /= 2; //a=2
a %= 2; //a=0
a &= 2; //a=0
a |= 2; //a=2
a ^= 2; //a=0
a <<= 2; //a=0
a >>= 2; //a=0
a >>>= 2; //a=0


6. 條件運算子 (三元) : 

此為 Javascript 唯一的三元運算子, 其實它是 if else 分支邏輯條件式的迷你表示法, 它有兩種用法,  第一種為有三個運算式, 其中第一個運算式必須為布林運算式 :

b ? x : y 

依據布林式 b 的值為 true/false 選擇要執行運算式 x (true) 或 運算式 y (false). 這相當於 :

if (b) {x;}
else {y;}

還有另外一種用法是 x, y 為值或有傳回值的函數而非運算式 :

r=b ? x : y

這相當於 :

if (b) {r=x;}
else {r=y;}


 條件運算子 說明
 (a >= 60) ? r="pass" : r="fail" ? 前面的條件式為真時執行 ? 後面的運算式, 否則執行 : 後面的運算式
 r=(a >= 60) ? "pass" : "fail" ? 前面的條件式為真時傳回 ? 後面的值, 否則傳回 : 後面的值


範例 : 條件運算子測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 條件運算子測試 */
    var s1=59;
    var s2=60;
    var r1, r2;
    (s1 > 60) ? r1="pass" : r1="fail";
    r2=(s2 >= 60) ? "pass" : "fail";
    document.write("s1=" + s1 + " result=" + r1 + "<br>");
    document.write("s1=" + s2 + " result=" + r2 + "<br>");
  </script>
</body>
</html>

結果如下 :

s1=59 result=fail
s1=60 result=pass


7. 運算子優先順序 :

以上之運算子混合在一起運算時有先後之分, 其優先順序如下表 :


 運算子 說明
 1. () 括號運算子
 2. !, -, ++, -- 邏輯 NOT 運算子, 負號, 遞增, 遞減運算子
 3. *, /, % 乘, 除, 求餘數運算子
 4. +, -  加, 減法運算子
 5. <<, >>, >>> 左移, 右移,  無號右移運算子
 6. >, >=, <, <= 大於, 大於等於, 小於, 小於等於比較運算子
 7. ==, != 等於, 不等於比較運算子
 8. & 位元 AND 運算子
 9. ^ 位元 XOR 運算子
 10. | 位元 OR 運算子
 11. && 邏輯 AND 運算子
 12. || 邏輯 OR 運算子
 13. ? :  三元條件運算子
 14. =, op= 指定運算子


運算式的執行順序是由上而下, 由左向右依據上面的優先等級進行計算. 在算術運算中基本上就是先乘除後加減, 若要改變這個順序可用優先等級最高的小括號. 例如 :

7*3 >= 17-2 || 11+6*2 < 9

因算術運算子比邏輯與關係運算子優先, 因此會先進行算術運算 :

21 >= 15 || 11+12 < 9 

21>= 15 || 23 < 9

true || false

最後得到 true.

參考 :

運算式與運算子


六. 流程控制 (flow control) :

流程控制有兩種 : 分支 (branch) 與迴圈 (loop), 分支是一種選擇結構 (decision/selection structure), 利用檢查條件式傳回值為 true 或 false 來決定程式的執行方向; 而迴圈則是重覆結構, 用來重覆執行一段敘述.


1. 分支 (branch) : 

Javascript 的分支語法有 if else 與 switch case 兩種結構 :


 分支敘述 語法 1 語法 2 說明
 單一分支 if (條件式) {
    敘述;
    }
 if (條件式)  敘述; 單一敘述時可省略大括號
 雙重分支 if (條件式) {
    敘述1;
    }
 else {
    敘述2;
    }
 if (條件式)  敘述1;
 else 敘述2;
 單一敘述時可省略大括號
 多重分支 if (條件式1) {
    敘述1;
    }
 else if (條件式2) {
    敘述2;
    }
 else if (條件式3) {
    敘述3;
    }
 else {
    敘述4;
    }
 switch (表達式) {
     case 條件式1 : {
         敘述1;
         break;
         }
     case 條件式2 : {
         敘述2;
         break;
         }
     case 條件式3 : {
         敘述3;
         break;
         }
     default : {
         預設敘述;
          }
     }
 switch 的表達式可傳回 :
 1. 數值
 2. 字串
 3. 布林
 當全部 case 均不符時執行
 default 之預設敘述.
 每一個 case 須以 break 結束,
 否則將執行 default 預設



2. 迴圈 (Loop) :

Javascript 的迴圈有 for, while, 與 do while 三種結構 :


 語法 說明
 for (初始值; 條件式; 迭代式) {
  敘述;
  }
 從初始值開始, 若條件式為真則執行迭代式與迴圈內敘述.
 for (鍵 in 物件) {
  敘述;
  }
 迭代物件中的鍵執行迴圈中的敘述. 
 for (元素 in 陣列) {
  敘述;
  }
 迭代陣列中的元素執行迴圈中的敘述. 
 while (條件式) {
    敘述;
    } 
 條件式為真時執行迴圈中的敘述, 直到條件式為假.
 do {
    敘述;
    }
 while (條件式);
 先執行敘述再判斷條件式, 為真繼續執行迴圈直到條件式為假.
 此結構迴圈至少會執行一次


迴圈內可用 break 與 continue 保留字來停止迴圈或繼續下一迴圈.


七. 函數 :

函數是一組被封裝起來可以重複使用的程式碼區塊 (block), 是模組化設計必要的功能. 函數將其內部程式碼的變數與運算隱藏起來, 外部必須使用函數名稱呼叫, 函數會將運算傳回.

Javascript 是以物件為基礎的語言, 函數可以說是 Javascript 的全域方法 (global method). 函數雖然用 typeof 檢驗會傳回 'function', 其實函數也是一種物件, 一種可以被呼叫的特殊物件, 又被稱為可呼叫的值 (callable values). 函數也可以作為物件的屬性, 這時稱為該物件的方法 (method).

Javascript 的函數被稱為第一級函數 (first class function), 意思是 :
  • 函數可以存放在變數, 陣列, 與物件中
  • 可以當作一個參數傳遞給另一個函數
  • 可以當作另一個函數的回傳值
  • 函數具有屬性 (因為函數是一種特殊物件)
這些特性使得函數跟基本型別的資料類型沒兩樣.


1. 自訂函數 :

Javascript 自訂函數之方法有三 :
  • 函數宣告 (function declaration)
  • 函數運算式 (function expression)
  • 使用 new Function() 建構子函數建立
函數名稱命名規則與變數識別字規則相同, 傳入參數若有多個則以逗號隔開, 如果沒有傳入參數則小括弧內是空的.

函數若有回傳值則在函數最後面用關鍵字 return 傳回, 但只能傳回一個值, 若有多個值要傳回須用陣列或物件. 執行完 return 後函數即將控制權交回給呼叫者 (caller), 後面的敘述不會被執行, 因此 return 是函數最後一個敘述. 如果沒有回傳值就不需要 return, 或者只用 return 即可.

函數內的變數若用 var 宣告屬於區域變數, 其作用域只限於函數內, 函數外無法存取. 在函數內可以存取外部變數 (全域變數), 但若同名時會存取到函數內的區域變數, 而非外部變數.


 函數建立方式 語法 範例
 函數宣告 function 函數名([參數]) {
   敘述;
   [return 傳回值]
   }
 function sum(a, b) {
    return a+b;
    }
 函數運算式 var 變數名=function [函數名]([參數]) {
   敘述;
   [return 傳回值]
   };
 var s=function(a, b) {return a+b};
 var s=function sum(a, b) {return a+b};
 new Function() var 變數名=new Function(['參數'], '敘述'); var sum=new Function('a', 'b', 'return a+b');


這三種定義函數的方法在作用域上不同, 只有用函數宣告建立的函數可在宣告前呼叫, 用函數運算式與 new Function() 所建立的函數, 必須在建立函數後才能呼叫, 例如 :


範例 : 函數的範疇測試

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <p id="output"></p> 
  <script>
    /* 函數的 scope 測試 */
    document.write("sum1(10,1)=" + sum1(10,1) + "<br>");
    function sum1(a,b) {
      return a+b;
      }
    document.write("sum1(10,1)=" + sum1(10,1) + "<br>");
    //document.write("sum2(20,2)=" + sum2(20,2) + "<br>"); //sun2 未定義
    var sum2=function(a,b) {
      return a+b;
      }
    document.write("sum2(20,2)=" + sum2(20,2) + "<br>");
    //document.write("sum3(30,3)=" + sum3(30,3) + "<br>"); //sun3 未定義
    var sum3=new Function('a', 'b', 'return a+b');
    document.write("sum3(30,3)=" + sum3(30,3) + "<br>");
  </script>
</body>
</html>

執行結果如下 :

sum1(10,1)=11
sum1(10,1)=11
sum2(20,2)=22
sum3(30,3)=33

此例中的 sum2() 使用函數運算式建立的, sum3() 則是用 new Function() 建立的, 這兩個都必須在函數定義之後才能呼叫. 如果將上面被註解掉的兩行分別取消, 則會出現 "TypeError: sumx is not a function" 的錯誤 :





呼叫函數時若未傳入參數, 則缺漏的參數值為 undefined, 例如 :

> function foo(a) {console.log(a);}    //定義函數
> foo(2)       //傳入參數 2 輸出 2
2
undefined
> foo()         //未傳入參數輸出 undefined
undefined


(1). 位置性參數 : 

Javascript 函數的參數具有位置對應, 沒有傳入之參數其值為 undefined, 進行數值運算會得到 NaN, 例如下列求兩數和函數 :

> function sum(a, b) {    //求兩數和
... return a+b;
... }
> sum()              //參數 a, b 缺漏, 值均為 undefined, 和為 NaN
NaN
> sum(10)          //參數 a 缺漏, 10 + undefined= NaN
NaN
> sum(10, 20)    //無參數缺漏
30


(2). arguments 物件 :

Javascript 為函數提供了一個 arguments 物件, 讓我們可用陣列的索引方式來存取所傳入之參數. 此 argument 物件雖然狀似陣列, 但只是一個附有索引的物件而已, 除了索引功能外不能當一般陣列使用.


範例 : 自訂函數測試 : 等差數列和

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 自訂函數測試 */
    function sum(n) {
      var total=0;
      for (var i=0; i<=n; i++) {total += i;}
      return total;
      }
    var n=100;
    document.write("1+2+3+ ... +" + n + "=" + sum(n) + "<br>");
  </script>
</body>
</html>

結果如下 :

1+2+3+ ... +100=5050

此例傳入參數為基本型別 (數值), 因此屬於傳值呼叫.


遞迴函數 (recursive function) 是指在函數內呼叫自己的函數結構, 例如計算階乘 (factorial) 時就會用到遞迴, 所謂階乘是指一個正數連乘比自己小 1 的數直到 1 為止之乘積 :

n!=n*(n-1)*(n-2)*(n-3)* ..... *2*1

由於階乘運算具有重複性, 因此用遞迴方式最簡潔. 但注意 : 遞迴會耗費 CPU 與堆疊記憶體資源, 除非問題適合用遞迴 (例如階乘或盒內塔問題) 解決, 否則盡量少用.


範例 : 自訂函數測試 : 用遞迴求階乘

<!DOCTYPE html>
<html>
<head>
  <title>Javascript 測試</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <script>
    /* 自訂函數測試 */
    function factorial(n) {
      if (n<=0) {return 1;}
      return n*factorial(n-1);     //呼叫自己
      }
    var n=5;
    for (var i=1; i<=20; i++) {
      document.write(i + "!=" + factorial(i) + "<br>");
      } 
  </script>
</body>
</html>

結果如下 :

1!=1
2!=2
3!=6
4!=24
5!=120
6!=720
7!=5040
8!=40320
9!=362880
10!=3628800
11!=39916800
12!=479001600
13!=6227020800
14!=87178291200
15!=1307674368000
16!=20922789888000
17!=355687428096000
18!=6402373705728000
19!=121645100408832000
20!=2432902008176640000

參考 :

# 預設參數( Default parameters )


2. 內建函數 : 

Javascript 本身已內建了一些常用的函數如下表, 均為全域方法 (類似 Java 的類別方法), 可直接呼叫, 不須透過物件 :


 Javascript 常用內建函數 說明
 parseInt(str [, base]) 將字串 str 轉換成基底為 base 的整數
 parseFloat(str) 將字串 str 轉換成浮點數
 typeof(obj) 傳回物件 obj 的資料型別 (字串), 也可用 typeof obj 
 escape(str) 將傳入之字串進行 URL 編碼後傳回 (URL 字串)
 unescape(URLstr) 將傳入之 URL 字串解碼為原始字串後傳回
 isFinite(exp) 檢查運算式 exp 是否可用 eval() 運算, 傳回 true/false
 isNaN(x) 檢查變數 x 是否為非數值, 傳回 true/false
 eval(exp) 計算算術運算式 exp
 void(func) 使函數 func 的傳回值無效, 例如 void(0) 使超連結無效


其中 parseInt() 與 parseFloat() 用來將字串轉換成整數與浮點數 :

另外, 瀏覽器的 wndow 物件提供了四個計時器方法 :


 常用 window 物件方法 說明
 setTimout("func", ms) 設定計時器在 ms 毫秒後呼叫函數 func 一次, 傳回 timerID
 clearTimeout(timerID) 清除計時器 timerID
 setInterval("func", ms) 設定計時器每 ms 毫秒呼叫函數 func 一次, 傳回 timerID
 clearInterval(timerID) 清除計時器 timerID


這四個方法正常要用 window.setTimeout() 呼叫, 但因為 window 物件為最上層物件, 可省略物件名稱 window 直接呼叫方法, 故常被認為是 Javascript 的全域方法.


八. 物件 :   

Javascript 的資料不是基本型別 (primitive) 就是物件. 物件包括下列三種 :
  • 物件字面值 : 例如 obj={a:1, b:2, c: 3}
  • 陣列 : 例如 arr=[1, 2, 3]
  • 正規表達式 : 例如 /abc/
物件之特性如下 :
  • 屬性是可變的 (mutable)
  • 以參考 (位址) 進行比較
這與基本型別是截然不同的, 物件內容預設是可變更的, 例如 :

> var a={};             //空物件
> var b={};             //空物件
> a.name='Tony';    //可任意新增屬性
'Tony'
> a.name
'Tony'
> a.name='Kelly';   //可隨時更改屬性值
'Kelly'
> a.name
'Kelly'

儲存區域也不同, 基本型別資料存放在堆疊區, 比較的是值 (內容), 只要型別相同, 值也相同, 則全等比較 (===) 會傳回 true :

> var a='Tony';
> var b='Tony';
> a==b               //值相同
true
> a===b             //值 & 型別相同
true

物件則存放於堆積記憶區, 在堆疊區中的物件變數儲存的是指向此物件的參考位址, 因不同物件位址不同, 因此 == 與 === 比較均傳回 false, 但若與複製參考變數比較則傳回 true :

> var a={name:'Tony'}    //a 儲存物件之參考位址
> var b={name:'Tony'}    //b 儲存物件之參考位址
> a==b          //兩個不同物件, 參考位址不同
false
> a===b        //兩個不同物件, 參考位址不同
false
> var c=a      //c 儲存與 a 一樣的參考位址 (複製參考)
> a==c          //a 與複製參考 c 內容相同 (指向同一物件)
true
> a===c        //a 與複製參考 c 內容與型別相同 (指向同一物件)
true


1. 物件字面值 (literal) :

物件封裝了多個資料與函數於一體, 物件的字面值用 {} 定義, 例如下面的物件定義了兩個屬性 firstName 與 lastName, 以及一個方法 getFullName() :

var obj={
  firstName:'Tony',                   //屬性
  lastName:'Huang',                 //屬性
  getFullName: function() {    //方法
  return this.firstName + ' ' + this.lastName;
  };

屬性與方法可用 . 運算子存取, 例如 obj.firstName 會傳回 'Tony', 而 obj.getFullName() 會傳回 'Tony Huang'. 以下是在 Node.js 的 REPL 介面的測試結果 :

> var obj={
... firstName:'Tony',
... lastName: 'Huang',
... getFullName: function() {
..... return this.firstName + ' ' + this.lastName;
..... }
... };
undefined
> console.log("%j", obj)     //%j 為以物件格式輸出
{"firstName":"Tony","lastName":"Huang"}
undefined
> console.log("%s", obj.firstName)     //%j 為以字串格式輸出
Tony
> console.log("%s", obj.lastName)
Huang
undefined
> console.log("%s", obj.getFullName())
Tony Huang
undefined


2. 陣列 : 

陣列是索引與值的映射 (mapping), 其中索引是零起始的整數 (範圍 0~2**32-1), 而值稱為陣列的元素 (element), 元素必須為同質, 亦即需為相同資料型別. 陣列是一種物件, 可用 length 屬性存取陣列長度, 例如 arr.length; 存取陣列元素則使用中括號與索引, 例如 arr[0].

(1). 建立陣列的方法 : 

建立陣列的方法有三 :
  • 陣列字面值 : 將元素以逗號隔開放在中括號內, 例如 :
    var arr=['a', 'b' ,'c'];
    var arr=[];   //建立一個長度為 0 的空陣列
    arr[0]='a';    //新增元素
    arr[1]='b';    //新增元素
    arr[2]='c';    //新增元素
  • 呼叫 Array() 函數 :
    var arr=Array(3);    //建立一個長度為 3 的空陣列
    arr[0]='a';    //新增元素
    arr[1]='b';    //新增元素
    arr[2]='c';    //新增元素
  • 使用 new Array() 建構子 :
    var arr=new Array(3);
    arr[0]='a';    //新增元素
    arr[1]='b';    //新增元素
    arr[2]='c';    //新增元素
陣列是一種物件, 物件是可變的 (mutable), 可任意調整其長度或增減其元素, 因此可先建立空陣列後再新增元素.

使用字面值建立陣列時, 最後元素後面的一個逗號會被忽略, 但若有一個以上的逗號卻會被認為是空元素, 例如 :

> var arr=['a', 'b', 'c',]      //'c' 後面的逗號會被忽略
> arr.length
3
> var arr=['a', 'b', 'c', ,]    //'c' 後面的兩個逗號會被認為有一個空元素
> arr.length
4 

其次, 雖然呼叫 Array() 或使用 new Array(), 也可以像字面值那樣直接將元素傳入 () 內來初始化陣列, 例如 :

var arr=Array('a', 'b', 'c');  或者 var arr=new Array('a', 'b', 'c');

但最好不要這樣子用, 因為如果只傳入一個參數時必須為正整數, 因為ㄏJavascript 引擎會認為這是在設定陣列長度而非元素, 但若傳入兩個以上參數時卻會被解讀為元素, 因此用法上很容易造成混淆 :

> var arr=Array(3)      //傳入一個正整數 : 陣列長度
> arr.length
3
> var arr=Array(3, 5)  //傳入兩個以上整數 : 陣列元素
> arr.length
2
> var arr=Array(-3)    //傳入一個參數時, 必須為正整數
RangeError: Invalid array length   

建立二維陣列字面值的方式是將一維陣列當作其元素即可, 例如 3*3 陣列 :

var arr=[[1, 2, 3], [4, 5, 6], [7, 8, 9]];

或者用兩層 for 迴圈 :

var arr=[];
var n=0;
for (var i=0; i<3; i++) {
   arr[i]=[];
   for (var j=0; j<3; j++) {
       arr[i][j]=++n;
       }
    }
   
存取二維陣列用雙重中括號, 例如 arr[1][2].   

參考 :

# 從 ES6 開始的 Javascript 學習生活 (電子書)
JavaScript 程式設計與應用:用於網頁用戶端 (電子書)
你不可不知的 JavaScript 二三事系列 (iT 幫幫忙教材)
# switch 語法
有號數字表示法
預設參數( Default parameters )
JavaScript 中利用typeof 檢驗運算元所代表的型別
你懂 JavaScript 嗎?
[Javascript] NaN是什麼?
Decimal to Hexadecimal converter
淺談JS中String()與 .toString()的區別
Javascript 基礎打底系列 (二) - null、undefined、NaN 的差異與檢查方式

沒有留言 :