只在此山中,雲深不知處


聽首歌



© 2018 by Shawn Huang
Last Updated: 2018.5.27

JavaScript簡介

JavaScript是用在網頁設計的語言,是客戶端的語言,使用的IDE與網頁設計的相同即可, Notepad++還不錯用。程式碼可以: 原則上寫成檔案為佳,方便管理並可重複使用,剛開始練習時可以使用另外兩種方式。

輸出


為了看到程式結果以及方便debug,我們需要輸出些文字來看內容或結果,一般可以用以下方式:
註解: 單行註解 => //; 區塊註解 => /**/

Statement: 敘述句是一個完整的程式指令,需於句後加上分號;來表示結束。=> console.log("Hello");

strict mode


使用"use strict";來表示使用strict mode,這可以強制我們寫比較安全的程式碼,部分不好的程式撰寫習慣會變成真正的錯誤。 在Strict Mode不允許以下寫法:

變數

JavaScript的變數型態包含Number,Boolean,String,可以使用typeof()函數來得到變數型態。 說明如下: 其中Number可分為int與float。

宣告


JavaScript不需要宣告變數型態,

Number


Boolean


只有true跟false兩個值。

String



在定義變數時,儘可能給予初始值,如此可以在之後再看程式碼時了解定義的變數型態。例如:
let s = "";
let a = [];
let b = true;

運算子(Operators)

運算子是計算符號,讓我們可以做運算,計有

算數運算子(Arithmetic Operators)


計有以下數種:
  1. +: addition(可用於數字相加,數字+字串,字串+字串) >> console.log(10+10);
  2. -: subtraction >> console.log(10 - 1);
  3. *: multiplication >> console.log("5"*2);
  4. /: division >> console.log(10/3);
  5. %: modulus(remainder) >> console.log(10%3);
  6. ++: increment >>
    var x = 10;
    console.log(x++); // 10
    console.log(++x); // 12
    印出x++表示印x,++在後面,印出++x表示先計算加1再印出x。
  7. --: decrement 與++用法相同,只不過是減1。
  8. **: power >> console.log(3**5);
JavaScript的變數型態不明顯,所以可以計算例如"5"-2,雖然是字串-數字,但是因為字串內容是數字,依然可以計算。

指派運算子(Assignment Operators)


  1. = : >> x = 5
  2. +=: >> x += 5
  3. -=: >> x -= 5
  4. *=: >> x *= 5
  5. /=: >> x /= 5
  6. %=: >> x %= 5
+號可用於字串的相加,結果傳回字串。

比較運算子(Comparison Operators)


  1. ==: equal to(only value) >> "5" == 5 (true)
  2. ===: equal value and equal type
  3. !=: not equal >> "5" != 5 (false)
  4. !==: not equal value or not equal type
  5. >: greater than
  6. <: less than
  7. >=: greater than or equal to
  8. <=: less than or equal to
  9. ?: ternary operator >> 10 > 5 ? "Yes": "No"

在判斷兩者之值時,儘量多使用===替代==,以避免產生"5" == 5 (true)這樣的結果。

邏輯運算子(Logical Operators)


  1. &&: logical and
  2. ||: logical or
  3. ! : logical not

型態運算子(Type Operators)


  1. typeof: returns the type of a variable
  2. instanceof: returns true if an object is an instance of an object type

位元運算子(Bitwise Operators)


  1. &: and >> 0101 & 0100
  2. |: or >> 5|2
  3. ~: not >> ~5
  4. ^: xor >> 1^5
  5. <<: zero fill left shift >> 10 << 2 (*4)
  6. >>: signed right shift >> 10 >> 1 (/2)
  7. >>> zero fill right shift >> 10 >>> 1
JavaScript使用32bits位元計算。所以~5 = -6。

流程控制

流程控制可讓我們控制程式的進行,計有

if...else


if可單獨存在,else不可,需與if同時存在。語法如下:
if(condition){
    // do something
} 

if(condition){
    // do something
}else{
    // otherwise
}
>>
var v = 10;
if (v < 5) {
    console.log("v<5");
}else{
    console.log("v>5");
}

switch


語法如下:
switch(expression){
    case n:
        code block
        break;
    case n:
        code block
        break;
    default:
        cold block
} 

>>
var m = 5;
switch(m) {
    case 1:
        console.log("January");
        break;
    case 5:
        console.log("May");
        break;
    case 10:
        console.log("October");
        break;
    default:
        console.log("No related months");
}

while loop


while loop的語法如下:
while(condition){
    //code block to be executed
}

>>
var i = 0;
while (i < 5) {
    console.log(i);
    i++;
}
do...while可確保至少執行一次,語法如下:
do{
    //code block to be executed
}while(condition);

for loop


for loop的語法如下:
for(start;condition;pace){
    //code block to be executed
}

>>
for (var i=0; i<5; i++) {
    console.log(i);
}
>>
for (var i=0; i<10; i++) {
    if (i==3) continue;
    if (i==5) break;
    console.log(i);
}

函數

函數可以將一段程式碼包裹,便於偵錯及重複使用,需使用關鍵字function
以下開始將程式碼寫在檔案,此處檔名為js5.js
function hello(){
    console.log("Hello");
}	
hello();
記得在head標籤內加上<script src="js5.js"></script>才能連結上。
函數需要被呼叫才會執行,所以須加上最後一行,請記得要有小括號。

輸入參數與傳回值


函數可以有輸入值與傳回值,例如以下計算矩形面積的函數:
function area(w, h){
    console.log(w*h);
}	
area(10, 20);
重載網頁可以看到結果。如果我們想算的是兩個矩形的面積和呢?此時我們需要讓函數傳回數值, 關鍵字為return。將上例修改如下:
function area(w, h){
    return(w*h);
}
console.log(area(10, 20)+area(7, 12));
每次呼叫函數將會得到傳回的數值。

將函數當作變數


這是一個有趣的功能,可以使用一個變數來表示函數,例如將上例修改如下:
var a = function(w, h){
    return(w*h);
}
console.log(a(10, 20)+a(12, 12));
重載可以看到結果,可以看出此時變數a代表的是函數area()。

=>


這是一個簡寫的函數建立方式,省略function關鍵字,例如之前的hello函數,改寫如下:
var helloAgain = () =>{
    console.log("Hello Again.");
}
helloAgain();
若是有參數的情況,例如上述的area函數,改寫如下:
let squareArea = (w, h) =>{
    return(w*h);
}
console.log(squareArea(2,6) + squareArea(5,3));
可以看出省略了function關鍵字,改用=>符號替代。 箭頭函數與正規函數還是有差別,看看以下例子:

不定長度參數


若是輸入參數長度不定,可以使用不定長度參數(使用...表示),例如:
function test(className, ...grades){
    let sum = 0;
    for(let i of grades){
        sum += i;
    }
    console.log(className, "班,總分為", sum);
}

test("101", 48,78,89); // 101 班,總分為 215

Hoisting & Self-Invoking


這是關於函數呼叫的方式,Hoisting的意思是我們可以在函數宣告之前呼叫他,還是會執行,那是因為程式先編譯,所以在執行時已經知道有宣告了。例如:
for(let i = 0; i < 10; i++) // hoisting --> use function before it is declared.
    console.log("f("+i+")="+f(i));

function f(n){
    if(n==0){
        return 0;
    }else if (n==1){
        return 1;
    }else{
        return f(n-1)+f(n-2);
    }
}
f(n)是fibonacci sequence的函數,使用recursive方法,但是我們在宣告之前便使用該函數,不過還是可以執行,是為hoisting。

而self-invoking的意思是自呼叫,也就是不需要呼叫函數便執行,或稱為IIFE(Immediately-Invoked Function Expression)。其實是這樣,假如我們有一個函數名為abc,那麼當我們呼叫它時,要寫abc();其實也可以寫成(abc)();加到小括號內是一樣的。(就好像a+b跟(a+b)一樣的類似)。接下來我們把函數的內容替換掉名字,其實名字就是指函數本身,如下例:
(function (){
    console.log("Greeting.");
})();
因為馬上就呼叫執行,所以連名字都省了。再看一個例子:
(function (x){
    console.log(x +" square is " + x*x);
})(5);
此外,可以利用這個方式將函數模組化,例如:
var mo = (function(){
    let count = 0;
    var fun1 = function(){
        console.log(count);
    };
    var fun2 = function(k){
        return count+k;
    };
    return {
        f1: fun1,
        f2: fun2
    };
})();
mo.f1();
console.log(mo.f2(6));
console.log(mo.count); // undefined
這個例子中,變數mo相當於一個獨立的模組,傳回是其中定義的函數組合的物件,我們可以呼叫其內的方法,不過其內定義的變數無法讀取(類似private)。這個方式可以將相關的函數組合起來形成一個Namespace來使用。當然可以寫成物件形式,若是寫成物件形式,變數可以被改變。

Closure


Closure函數的寫法可以讓我們操控變數的影響範圍,或讓變數成為private,盡量不要宣告global variable免得與視窗變數產生衝突。
先看以下兩個程式範例:
function func(){
    var hello = "Hello, World.";
    function sayHello(){
        console.log(hello);
    }
    sayHello();
}
func();
function func1(){
    var hello = "Hello, World.";
    function sayHello(){
        console.log(hello);
    }
    return sayHello;
}
let f = func1();
f();
第一個函數內的sayHello()使用函數內變數,第二個函數傳回sayHello()函數,第二個方式為Closure,此寫法會讓下次呼叫時, 裡面的函數保持之前的設定。
再看以下例子:
function func2(){
    var i = 0;
    return function (){
        console.log(i++);
    }
}
var f2 = func2();
f2();
f2();
f2();
使用這個方式,每次呼叫f2()時,會保留之前的環境,所以i的值會每次加1。
<button onclick="f2()">Click(Press F12 see output)</button>

另一個寫法,使用let。
HTML:
<button id="but">Click</button>
<p id="show"></p>
Javascript:
'use strict';
let counter;
{
    let count = 0; 
    let increment = function () { 
        count++;
        console.log(count);
        return count;
    };
    counter = {
        increment: increment,
    }; 
}
let but = document.getElementById("but");
let show = document.getElementById("show");
but.addEventListener("click",(event)=>{
    show.innerText = counter.increment();
});

再看一個例子。先使用closure。

HTML:
<button onclick="g()">click</button>
<div id="diva">div</div>
Javascript:
let diva =document.getElementById('diva');

function gog(){
    let b = true;
    return function(){
        if (b){
            diva.style.border="2px solid red";
            b=false;
        }else{
            diva.style.border = "none";
            b=true;
        }
    }
}
let g = gog();

使用let。

HTML:
<button onclick="gog.g()">click</button>
<div id="diva">div</div>
Javascript:
let gog;
{
    let b = true;
    let g = function() {
        if (b){
            diva.style.border="2px solid red";
            b=false;
        }else{
            diva.style.border = "none";
            b=true;
        }
    }
    gog =  {
        g:g,
    }
}

Currying


就是把函數的一部分拆成另一個函數來傳回,直接看例子:
function greet(greeting, name) {
    console.log(`${greeting}, ${name}`);
}

greet("Hello", "Jenny"); // Hello, Jenny

接著把上例改寫,使其傳回一個函數:
function greet(greeting) {
    return (name) => {
        console.log(`${greeting}, ${name}`);
    }
}

var hi = greet("Hi");
hi("Jenny"); // Hi, Jenny
greet("Hello")("Mia"); // 也可以這樣寫 >> Hello, Mia
var morning = greet("Good Morning");
morning("Mary"); // Good Morning, Mary

通常來說,能夠把程式拆成許多小片段可以方便維護與重複使用,另一個好處是可以提高其彈性與可讀性。
可以一層一層加進去:
function greet(greeting) {
    return (name) => {
        return (ask) => {
            console.log(`${greeting}, ${name}. ${ask}`);
        }
    }
}

var good = greet("Good morning!")("Helen");
good("How are you?");

物件

JavaScript是物件導向程式(true?),所以可以建立物件。 建立方式舉例如下:
var Node = {
    x: 50,
    y: 50,
    demand: 100,
    isVisited: false
}

這是一個客戶點的物件,裡面只包含數個變數。

物件方法


物件內除了有變數,還可以加上方法,原則上就是加上函數,修改上例如下:
var Node = {
    x: 50,
    y: 50,
    demand: 100,
    isVisited: false,

    nodeInfo: function(){
        return this.x + " " + this.y + " " + this.demand + " " + this.isVisited;
    }//nodeInfo	
}
Node.x = 80;
Node["isVisited"] = true;
console.log(Node.nodeInfo());

說明: 函數也可以用來建立物件,如下:
function fun(a, b){
    this.a = a;
    this.b = b;
    this.afun =  () => {return this.a * this.b;}
}

var f = new fun(10, 20);
console.log(f.a + " " + f.b);
console.log(f.afun());

混合著使用。
function createObj(a, b){
    let obj={
        a: a,
        b: b,
        toString: function(){
            return `a:${this.a}, b:${this.b}`;
        }
    }
    return obj;
}

let o1 = createObj(1,2);
console.log(o1.toString());
let o2 = createObj(10, 20);
o2.b = 200;
console.log(o2.toString());

call & apply


call()跟apply()兩個函數是用來呼叫一個方法,而輸入參數是物件。例如以下的例子:
var nodeA = {
    x: 30,
    y: 50
}
var nodeB = {
    x: 10,
    y: 80
}
var arc={
    endpoint: function(){
        return this.x + " " + this.y;
    }
}
console.log(arc.endpoint.call(nodeA));
console.log(arc.endpoint.apply(nodeB));

More:
原則上函數只要加上小括號()便可以執行,使用call與apply也可以執行,似乎有點多餘。原則上call與apply主要是可以指定執行時的this值。基本語法如下:
fun.call(thisArg[, arg1[, arg2[, ...]]])
fun.apply(thisArg, [argsArray])
上例中的endpoint函數使用call或apply呼叫時,傳入的第一個參數就是執行時所需的this值,若是函數還有本身的輸入參數,直接寫在this參數之後即可,唯一不同是apply僅接受兩個輸入參數,也就是說第二個參數需為陣列。在此我們再做個簡單的例子。
function whatever(a, b){
	return a+b;
}
console.log(whatever.call(this, 3,6));
console.log(whatever.call(null, 3,6));
console.log(whatever.apply(this, [3,6]));
console.log(whatever.apply(null, [3,6]));
原則上就是執行一個簡單的函數,因為不需要特定的this值,所以兩種寫法都可以。

bind


bind跟call與apply類似,也是可以指定this值,但其效果是可以建立一個新的綁定了this值的函數(BF),呼叫BF會執行該wrapped function。語法如下:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
看個例子:
function whatever(a, b){
	return a+b;
}
let w = whatever.bind(this, 10);// or null
console.log(w(20));
函數w便是產生的BF,且已綁定第一個參數為10。類似currying的效果:
function curry(a){
	return function(b){
		return a+b;
	}
}
let c = curry(5);
console.log(c(7));
再一個例子,建立一個物件作為this的值,再使用bind()建立BF。
let obj = {
	x: 1,
	y: 2
}
function toBind(a, b){
	return a*this.x+b*this.y;
}
let bf = toBind.bind(obj);
console.log(bf(1,2));

more practice
astring = "Global string";
function printString(){
    console.log(this.astring);
}

class AClass{
    constructor(a, b) {
        this.astring = a;
        this.b = b;
    }
    print() {
        printString.call(this); // this 指稱此物件
    }
}
ac = new AClass("Object string", "Whatever");

printString.call(this); // this 指稱window, 亦可使用apply
printString.call(ac); // 直接傳入物件表示呼叫物件內變數(等同於print()內的call(this)
ac.print();
// 使用bind來連結
var bindFun1 = printString.bind(this);
var bindFun2 = printString.bind(ac);

bindFun1(); // Global String
bindFun2(); // Object String

new


除了上述的方式建立物件,也可以使用new關鍵字來建立,舉例如下:
var nodeC = new Object();
nodeC.x = 11;
nodeC.y = 11;
console.log(arc.endpoint.call(nodeC, "C", 11));
看結果可知nodeC為一新建立的物件。

如果我們使用等號(=)將一個物件指派給另一個變數,原則上是傳址,也就是此時兩者為同一物件, 改變其中一個的數值會跟著改變另一個。例如:
var nodeD = nodeA;
nodeD.x = 22;
nodeD.y = 22;
console.log(arc.endpoint.call(nodeD, "D", 222));
console.log(arc.endpoint.call(nodeA, "A", 333));
將nodeA指派給變數nodeD,改變nodeD的x,y值,根據結果可知nodeA的x,y值也跟著更改了。

for in


若是要traverse整個物件,可以使用for in語法,例如:
for (i in Node){
    console.log(i);
}
這個方式可以很便利的依次取得物件中的所有變數名稱(當然包含方法)。若是要得到變數的值,可以將
console.log(i);
修改為
console.log(Node[i]);
這是前述取得物件變數值得方式,那為何不使用Node.i呢?那是因為取得的i的資料型態是字串,所以會出錯。如下例:
let arr = [1,2,3];
let k = 0;
for (i in arr){
    console.log(typeof(i)); // string
    k = k + i;
}
console.log(k); // 0012

adding & deleting properties


JavaScript允許我們自外部加上屬性,即使原來物件中並無定義也可以,例如修改之前的nodeC物件如下:
nodeC.demand = 123;
for (i in nodeC){
    console.log(i + "-->" + nodeC[i]);
}
可以看到物件多了一個demand的屬性。
若是要加入新的方法也可以,舉例如下:
nodeC.info = function(){
    return this.x + " " + this.y + " " + this.demand;
}
console.log(nodeC.info());
新的方法info()被加入到nodeC內了。
可以刪除屬性,使用delete關鍵字,例如:
delete nodeC.demand;
for (i in nodeC){
    console.log(i + "-->" + nodeC[i]);
}
如果是繼承而來的物件,則只會刪除此物件內屬性,若是被繼承的物件,則繼承的物件內屬性都會被刪除。

constructor


通常我們還是希望對於同一類別的物件建立一個設計圖,此稱constructor,然後根據此類別來產生物件。在JavaScript使用關鍵字function來達成。例如:
function TheNode(id, x, y, demand){
    this.id = id;
    this.x = x;
    this.y = y;
    this.demand = demand;
    this.info = function(){
        return this.id + " " +this.x + " " + this.y + " " + this.demand;
    };
}
var na = new TheNode("A", 100, 100, 100);
var nb = new TheNode("B", 200, 200, 200);
console.log(na.info() + "\t" + nb.info());
根據此方式便可以建立物件。但是不可以在外部增加constructor的屬性,如此物件無法取得該屬性。

class


JavaScript尚可使用class關鍵字來建立物件。 >>
class ANode{
    constructor(id, x, y){
        this.id = id;
        this.x = x;
        this.y = y;
        this.demand = 100;
    }
    info(){
        return this.id + " " +this.x + " " + this.y + " " + this.demand;
    }
}
var nodeOne = new ANode("One", 11, 12);
let nodeTwo = new ANode("Two", 22, 25);
nodeTwo.demand = 200;
console.log('one->', nodeOne.info(), 'two->' ,nodeTwo.info());
Something more about this:
this在物件中指的便是instance本身。在JS中,若不是在物件中,指的便是Window,如果是strict mode指的變數undefined。this的值是可以被改變的,例如:
'use strict';
function test(a,b) {
    console.log(this, a, b);
}
test(1,2); // undefined 1 2
test.call(undefined, 1, 2); // undefined 1 2
test.apply(undefined, [1, 2]); // undefined 1 2
test.call("value of this", 1, 2); // value of this 1 2
test.apply(100, [1, 2]); // 100 1 2
test.bind("value of this")(1,2); // value of this 1 2
上例中可以看出this的default value是undefined,而call與apply的差別則是apply僅接受兩個參數,第二個參數為arguments的array(兩者的第一個參數皆為this)。而bind較為不同則是傳回一個this被重新定義的新函數(也就是說test.bind("this value")會傳回一個新的函數,此函數中的this為bind()的輸入參數。此外,bind()後的函數其this無法再被修改)。

再看以下這個例子:
class Greeting{
    constructor(name){
      this.name = name;
    }
    hi(){
      console.log('Hi, ',this.name);
    }
    hello(){
      setTimeout(function(){
        console.log('Hello!, ',this.name)
      }, 1000); // 經過一秒
    }
  }
  let gr = new Greeting('Jenny');
  gr.hi();   // Hi,  Jenny
  gr.hello(); // Hello!,  undefined
在hi()中的this.name沒有問題,就是這個instance中的this.name。但是在hello()內this.name,因為其宣告之所在是在hello()內的函數內,所以其this.name為undefined。解決方法如下:
    將原來的this先儲存起來(命名為self),然後再使用self.name。
    class Greeting{
        constructor(name){
          this.name = name;
        }
        hi(){
          console.log('Hi, ',this.name);
        }
        hello(){
          const self = this;
          setTimeout(function(){
            console.log('Hello!, ',self.name)
          }, 1000); // 經過一秒
        }
      }
      let gr = new Greeting('Jenny');
      gr.hi();   // Hi,  Jenny
      gr.hello(); // Hello!,  Jenny
    第二個方式是使用bind(),將原來的this綁入形成新的函數。
    class Greeting{
        constructor(name){
          this.name = name;
        }
        hi(){
          console.log('Hi, ',this.name);
        }
        hello(){
          setTimeout(function(){
            console.log('Hello!, ', this.name)
          }.bind(this), 1000); // 經過一秒
        }
      }
      let gr = new Greeting('Jenny');
      gr.hi();   // Hi,  Jenny
      gr.hello(); // Hello!,  Jenny
    第三個方式是使用箭頭函數來寫feedback函數即可。因為箭頭函數沒有this,所以其內的this相當於宣告處的this。
    class Greeting{
        constructor(name){
          this.name = name;
        }
        hi(){
          console.log('Hi, ',this.name);
        }
        hello(){
          setTimeout(()=>{
            console.log('Hello!, ', this.name)
          }, 1000); // 經過一秒
        }
      }
      let gr = new Greeting('Jenny');
      gr.hi();   // Hi,  Jenny
      gr.hello(); // Hello!,  Jenny

getter & setter and private variable


在建立物件時,我們常需要使用getter&setter方式來存取變數值,主要的原因是可能變數是private,不欲讓外部改變其值,另一個原因則是要控制值的範圍(例如有個變數為age,很大部分的情況下便不接受負值)。要注意的是private的設定並不是所有瀏覽器都支援。例如:
class Node{
	#demand // FireFox & IE 不支援private variable
    constructor(id, xx, y){
        this.id = id;
        this.xValue = xx;
        this.y = y;
        this.#demand = 100;
    }
	get x(){
		return this.xValue;
	}
	set x(newX){
		if (newX >= 0 && newX <= 100){
			this.xValue = newX;
		}else{
			throw new Error("x has to be within 0 and 100");
		}
	}
	get demand(){
		return this.#demand;
	}
	set demand(newDemand){
		throw new Error("demand value is read only");
	}
    info(){
        return this.id + " " +this.x + " " + this.y + " " + this.demand;
    }
}

let a = new Node(1, 2, 3);
console.log(a.x);
a.x = 18;
console.log(a.x);
console.log(a.demand);
// a.demand = 999; // Uncaught Error: demand value is read only
a.x = 200; // Uncaught Error: x has to be within 0 and 100

private method


與private variable類似,僅需要在方法前面加#字號即可變成private method。(一樣要注意並非所有瀏覽器都支援。FireFox & IE 不支援。)
class Arc{
	constructor(na, nb){
		this.na = na;
		this.nb = nb;
		this.len = this.#arcLength();
	}
	#arcLength(){ /* private method >> 前面加#字號 */
		return Math.sqrt(Math.pow((this.na.x-this.nb.x),2) + Math.pow((this.na.y-this.nb.y),2));
	}
}

a = new Node(1,2,3);
b = new Node(2,9,5);
arc = new Arc(a, b);
console.log(arc.len);

繼承(Inheritance)


使用此方式可將constructor與methods分開來寫,容易分類,跟其他語言例如Java的寫法較為類似。此外,尚可跟Java一樣使用extends關鍵字做繼承。 >>
class SNode extends ANode{
    constructor(id, x, y, size){
        super(id, x, y);
        this.size = size;
    }
    setSize(size){
        this.size = size;
    }
    info(){
        return super.info() + " " + this.size;
    }
}
var nodeThree = new SNode("Three", 33, 36, 5);
nodeThree.setSize(6);
console.log(nodeThree.info());
使用super關鍵字來呼叫父類別。

Prototype


原型(Prototype)是所有物件繼承鏈的最頂端,之前提到不能在外部增加constructor的屬性,但是可以修改其原型(Prototype)來達到修改的目的。例如:
TheNode.prototype.isVisited = false;
TheNode.prototype.printInfo = function(){
    console.log(this.info() + "\t" + this.isVisited);
}
na.printInfo();
nb.printInfo();
我們在剛才的constructor的prototype加上一個新的變數跟一個新的方法。
More about prototype:
雖然我們可以使用class來定義物件,不過原則上在javascript與其他語言的物件不同,class這個Syntactic sugar只是讓我們容易coding,根本上還是在修改prototype。在JS中,所有函數都有一個內建的prototype,指向一個prototype物件。
function Person(name){
	this.name = name;
}
console.log(Person);
console.log(Person.prototype);
console.log(Person.prototype.constructor === Person); // true
console.log(Person.__proto__);// 指向函數的prototype
console.log(Person.prototype.__proto__); // 指向物件的prototype
console.log(Function.prototype.__proto__); // 指向物件的prototype
console.log(Object.prototype.__proto__); // null
因為prototype物件中的constructor指向原來物件,所以感覺有點像無窮迴圈,互相指來指去。__proto__是物件的內部屬性,代表繼承而來的源頭。當產生instance時,其關係如下例:
function Person(name){
	this.name = name;
}
let tom = new Person('Tom');
console.log(tom.prototype); // undefined, tom是instance沒有prototype
console.log(tom.__proto__); // 指向Person.prototype
console.log(tom.__proto__===Person.prototype); // true
因此我們可以使用函數的prototype修改其相關屬性。
function Person(name){
	this.name = name;
}
let tom = new Person('Tom');
Person.prototype.isMale = true;
console.log(tom.isMale); // true
Person.prototype.toString = function(){ // 不能使用箭頭函數
	return `${this.name}, ${this.isMale};`
}
console.log(tom.toString()); // Tom, true;
console.log(tom);
好像都是用函數建構子的方式測試,改用class建立物件再測試一次。
class Cls{
	constructor(x){
		this.x = x;
	}
}
Cls.prototype.y = 10;
let c = new Cls(1);
Cls.prototype.toString = function(){
	return this.x + " " + this.y;
}
console.log(c.toString());
如前所述,使用class建立物件,可以使用entends關鍵字來實現繼承。繼承原則上就是根據之前的物件再增補上需要的變數或方法,所以在JS我們可以使用prototype來擴充原來物件。例如:
function Super(x){
	this.x = x;
}

function Sub(x, y){
	Super.call(this, x);
	this.y = y;
}

Sub.prototype = Object.create(Super.prototype);//Object.create():指定其原型物件與屬性,創建一個新物件。
Sub.prototype.constructor = Sub;
let ins = new Sub(1, 2);
Sub.prototype.toString = function(){
	return this.x + " " + this.y;
}
console.log(ins.toString()); // 1 2
console.log(ins instanceof Super); // true
console.log(ins instanceof Sub); // true

Static


在class內,若是方法由static修飾,表示不需要instantiating(new之後)即可呼叫,若instantiating後反而不能呼叫。常見的例子如Math,練習一下:
class MyMethods{
    static hi(name){
        console.log("Hello, " + name);
    }
    static circleArea(radius){
        return Math.PI*radius*radius;
    }
}

MyMethods.hi("Jenny");
console.log(MyMethods.circleArea(10));
mm = new MyMethods();
mm.hi("Alice"); // << invalid

Private Static method


static方法也可以是private,原則上就是僅給class內的方法使用。
class Area{
	static #circle(r){ // static private method
		return Math.PI*r*r;
	}//circle
	static oneCircle(r){
		return this.#circle(r);//使用Area.#circle(r)可能較好,因為若有牽涉到繼承,this可能指向不同的class
	}
	static circles(...rs){
		let area = 0;
		for (let r of rs){
			area += this.#circle(r);
		}
		return area;
	}//circles
}
console.log(Area.oneCircle(10)); // 314.1592653589793
console.log(Area.circles(10, 20)); // 1570.7963267948967

Built-in Constructors & BOM


JavaScript包含數個內建的constructors供我們直接使用,計有:
  1. Object(): {}
  2. String(): ""
  3. Number(): 0
  4. Boolean(): true
  5. Array(): []
  6. RegExp(): /()/
  7. Function(): function(){}
  8. Date():
以上的內建constructor都可以使用類似var x = new Object();這樣的語法來產生,但是為了簡單化,所以只要給象徵的數值即可產生,例如給數字即知道是Number。

BOM

Browser Object Model:關於瀏覽器的物件模式,可讓Javascript與之連結。
Window Object: 所有瀏覽器都適用。所有的Javascript全域objects, functions, 與variables會自動變成window object的一員。
Window Screen: 關於使用者螢幕的資訊。
Window location: 取得目前頁址(URL)並指向(redirect)新頁。
Window History: 歷史頁面。
window.navigator物件包含使用者瀏覽器資訊。
Popup Alert: 包含Alert box, Confirm box, 與Prompt box。
Timing Events: 關於控制時間間隔以執行程式,主要有兩個方法setTimeout(function, milliseconds)與setInterval(function, milliseconds),兩個方法都是HTML DOM Window物件的方法。
Cookies: 可讓我們儲存少量資料(about 4KB)在網頁。

Math&Date

介紹兩個常用的物件,Math & Date。

Math


math物件提供數學相關參數與方法供我們使用,Math內的方法都是static,所以不需要建立,直接使用Math.XXX來呼叫。Date也是內建物件,使用new關鍵字建立,可以讓我們操作時間的顯示,在之後介紹。

Math properties


而其擁有的屬性如下:>>
  1. Math.E: Euler's number -> 2.718
  2. Math.LN2: Natural logarithm of 2 -> 0.693
  3. Math.LOG2E: Base 2 logarithm of E -> 1.442
  4. Math.LOG10E: Base 10 logarithm of E -> 0.434
  5. Math.PI: pi -> 3.14159
  6. Math.SQRT1_2: Square root of 1/2 -> 0.707
  7. Math.SQRT2: Square root of 2 -> 1.414

>>
console.log("E: " + Math.E + "\nLN2: " + Math.LN2 + "\nLN10: " + Math.LN10 + "\nLOG2E: " + Math.LOG2E + 
"\nLOG10E: " + Math.LOG10E + "\nPI: " + Math.PI+ "\nSQRT1_2: " + Math.SQRT1_2 + "\nSQRT2: " + Math.SQRT2);

Math methods


Math的相關方法計有如下:
  1. Math.abs(x): absolute value
  2. Math.acos(x): arccosine (in radians)
  3. Math.asin(x): arcsine (in radians)
  4. Math.atan(x): arctangent (in radians)
  5. Math.atan2(x,y): arctanget of the quotient of its arguments
  6. Math.ceil(x): smallest integer greater than of equal to x
  7. Math.cos(x): cosine
  8. Math.exp(x): ex
  9. Math.floor(x): largest integer less than or equal to x
  10. Math.log(x): logex(ln(x))
  11. Math.max(x,y,...): largest number
  12. Math.min(x,y,...): smallest number
  13. Math.pow(x,y): xy
  14. Math.random(): random number between 0 and 1
  15. Math.round(x): a number rounded to the nearest integer
  16. Math.sin(x): sine
  17. Math.sqrt(x): square root of x
  18. Math.tan(x): tangent

Date


Date的宣告方式可以有以下幾種:

Date methods


  1. Date(): today's date and time. >>
    console.log(Date());
    
  2. getDate(): returns day of the month. >>
    var d = new Date("December 31, 1999, 23:59:59");
    console.log(d.getDate());
    
  3. getDay(): returns day of the week. >>
    console.log(d.getDay());
    
  4. getFullYear(): returns the year. >>
    console.log(d.getFullYear());
    
  5. getHours(): returns the hour. >>
    console.log(d.getHours());
    
  6. getMilliseconds(): returns milliseconds. >>
    console.log(d.getMilliseconds());
    
  7. getMinutes(): returns the minutes. >>
    console.log(d.getMinutes());
    
  8. getMonth(): returns the month. >>
    console.log(d.getMonth());
    
  9. getSeconds(): returns the seconds. >>
    console.log(d.getSeconds());
    
  10. getTime(): returns the numeric value of time (milliseconds since January 1, 1970, 00:00:00 UTC). >>
    console.log(d.getTime());
    console.log(Date.parse(d));
    console.log(d.valueOf());
    
    or use Date.parse() method or valueOf() method.
  11. getTimezoneOffset(): returns the time-zone offset in minutes. >>
    console.log(d.getTimezoneOffset());
    
  12. getUTCDate(): returns the date of the month according to universal time. >>
    console.log(d.getUTCDate());
    
  13. getUTCDay(): returns the day of the week according to universal time. >>
    console.log(d.getUTCDay());
    
  14. getUTCFullYear(): returns the year according to universal time. >>
    console.log(d.getUTCFullYear());
    
  15. getUTCHours: returns the hours according to universal time. >>
    console.log(d.getUTCHours());
    
  16. getUTCMilliseconds(): returns the milliseconds according to universal time. >>
    console.log(d.getUTCMilliseconds());
    
  17. getUTCMinutes(): returns the minutes according to universal time. >>
    console.log(d.getUTCMinutes());
    
  18. getUTCMonth(): returns the month according to universal time. >>
    console.log(d.getUTCMonth());
    
  19. getUTCSeconds(): returns the seconds according to universal time. >>
    console.log(d.getUTCSeconds());
    
  20. setDate(): sets the day of month. >>
    d.setDate(1);
    console.log(d);
    
  21. setFullYear(): sets the year. >>
    d.setFullYear(2018);
    console.log(d);
    
  22. setHours(): sets the hour. >>
    d.setHours(12);
    console.log(d);
    
  23. setMilliseconds(): sets the Milliseconds. >>
    d.setMilliseconds(12);
    console.log(d);
    
  24. setMinutes(): sets the Minutes. >>
    d.setMinutes(12);
    console.log(d);
    
  25. setMonth(): sets the Month. >>
    d.setMonth(12);
    console.log(d);
    
  26. setSeconds(): sets the Seconds. >>
    d.setSeconds(12);
    console.log(d);
    
  27. setTime(): sets the Date object to the time represented by a milliseconds >>
    d.setTime(9466559990000);
    console.log(d);
    
  28. setUTCDate(): sets the day of the month according to universal time. >>
    d.setUTCDate(10);
    console.log(d);
    
  29. setUTCFullYear(): sets the year according to universal time. >>
    d.setUTCFullYear(2250);
    console.log(d);
    
  30. setUTCHours(): sets the hour according to universal time. >>
    d.setUTCHours(10);
    console.log(d);
    
  31. setUTCMilliseconds(): sets the milliseconds according to universal time. >>
    d.setUTCMilliseconds(86400000);
    console.log(d);
    
  32. setUTCMinutes(): sets the minutes according to universal time. >>
    d.setUTCMinutes(30);
    console.log(d);
    
  33. setUTCMonth(): sets the month according to universal time. >>
    d.setUTCMonth(6);
    console.log(d);
    
  34. setUTCSeconds(): sets the seconds according to universal time. >>
    d.setUTCSeconds(75);
    console.log(d);
    
  35. toDateString(): returns date. >>
    console.log(d.toDateString());
    
  36. toLocaleDateString(): returns date using current locale's conventions. >>
    console.log(d.toLocaleDateString());
    
  37. toLocaleString(): converts a date to a string, using current locale's conventions. >>
    console.log(d.toLocaleString());
    
  38. toLocaleTimeString(): returns the time of the Date, using current locale's conventions. >>
    console.log(d.toLocaleTimeString());
    
  39. toString(): returns a String of a Date object. >>
    console.log(d.toString());
    
  40. toTimeString(): returns a String of time of a Date object. >>
    console.log(d.toTimeString());
    
  41. toUTCString(): returns a String of time of a Date object according to universal time. >>
    console.log(d.toUTCString());
    

Time Comparison


比較兩時間的先後。 >>
var now = new Date(Date.now());
var oneday = new Date("06/15/2018");
if(now > oneday){
    console.log(now.toString() + " is after " + oneday.toString());
}else{
    console.log(now.toString() + " is before " + oneday.toString());
}

陣列(Array)

陣列是一個物件(object)包含數個元件,可能是變數或物件,通常我們在陣列中存放相同型態的資料,雖然這裡並不限制我們放置不同型態的資料。 陣列會有index,就好像門牌號碼,我們可以根據這個編號得到或修改其中的資料。如前一章所述,當我們要宣告一個陣列時,可以使用中括號[],如下例:
var a = [1,2,3];
console.log(a[0]);
其實這個陣列等同於我們宣告了三個變數。我們可以經由其編號得到其中的內容,使用中括號及其index,結果顯示1,可以知道編號由0開始。可以想見若是要改變其中的值,只要使用
a[1] = 200;
console.log(a[1]);
可以看到a[1]的值被改變了。其實我們只是宣告了a[0],a[1],a[2]三個變數不是嗎?
如前一章所述,因為陣列其實是一個物件,所以也可以使用以下方式來宣告。
var fruits = new Array('apple', 'banana', 'carrot');
console.log(fruits[2]);
若是嘗試印fruits[3]會發生甚麼事?若是將fruits[2]改為fruits又會印出甚麼?
其實array的index也可以不需要是整數,例如下例:
var node = [];
node["x"] = 100;
node["y"] = 200;
node["demand"] = 50;
console.log(node.length + "\t" + node["x"] + "\t" + node["y"] + "\t" + node["demand"]);
雖然能使用不過可以發現結果顯示node.length變成0,若是用數字的index則不會發生這個情形,所以與其這樣使用還不如做成一個物件。
在宣告一個陣列的時候,若是沒有給初始值,可以直接宣告如下:
var arr = [];
要增加元素時,可以直接使用push()函數,例如:
for (let i = 0; i < 5; i++) {
    arr.push(i);
}
console.log(arr);
也可以直接接著最後一個index加入元素,例如:
arr[arr.length] = "one";
arr[arr.length] = "two";
arr[8] = "two"; // [ 0, 1, 2, 3, 4, "one", "two", <1 empty slot>, "two" ]
console.log(arr);
要注意的是若是直接給index數字,若不是連續整數,則會出現空洞,如上例應該要接著給7但是使用了8,所以出現了一個empty slot。

Traversal


Array最常用的屬性大概就是長度(length),我們可以使用陣列名稱.length來得到其長度,也就是元素個數。可以使用此屬性來traverse整個array。例如:
for (let i = 0; i < fruits.length; i++){
    console.log(fruits[i] + "\t");
}
也可以使用如之前traverse物件的方式,使用in關鍵字,例如:
for (i in fruits){
    console.log(fruits[i] +"\t");
}
這跟我們的期待有點落差,本以為i便是array內物件,所以印i即可,結果i卻是index,所以要印fruits[i]。其實應該這樣:
for (i of fruits){
    console.log(i + "\t");
}
使用of關鍵字即可。 也可以使用以下方式:
fruits.forEach(
    function(e){
        console.log(e);
    }
);
forEach函數就是對於array內每一個元素(e)的意思,也可以改寫為使用=>,如下:
fruits.forEach(
    (e)=>{console.log(e);}
);
這幾個方式可以讓我們巡迴整個array。

陣列方法(methods)


JavaScript提供數個methods供我們操縱array,計有:
  1. concat(): 合併兩個array。 >>
    var cars = ["Benz", "BMW", "Ferrari", "Porsche", "Lamborghini"];
    var owners = ["Alex", "Boney", "John", "Tom", "Sean"];
    var carowner = cars.concat(owners);
    console.log(carowner);
    
  2. every(): 檢查array中每一個元素,傳回boolean。 >>
    function less20(e){
        return e < 20;
    }
    var n = [3,6,11,2,18];
    console.log(n.every(less20));
    
    也可以將函數簡寫為如下:
    console.log(
        n.every(
            (e)=>{return e<20;}
        )
    );
    
  3. filter(): 傳回新的array包含舊array中符合某標準的所有元素。 >>
    function less10(e){
        return e < 10;
    }
    console.log(n.filter(less10));
    
    或是
    console.log(
        n.filter(
            (e)=>{return e < 10;}
        )
    );
    

    Hint:
    使用filter讓程式碼看起來清爽一點。
    // 使用for loop
    const cars = ["Benz", "BMW", "Ferrari", "Porsche", "Lamborghini"];
    let ford = [];
    for (let c of cars){
    	if (c < "Ford"){
    		ford.push(c);
    	}
    }
    console.log(ford);
    // 使用filter
    let fford = cars.filter((ele)=>{
    	return ele<"Ford";
    });
    console.log(fford);
    

  4. forEach(): 針對array中每一個元素。之前介紹過,再加個例子。 >>
    n.forEach(
        (e, i)=>{console.log(i + " - " + e)}
        //(e,i)=>{n[i] = e*e;} // try this
    )
    
    第二個參數是index。
  5. indexOf():傳回第一個相符元素的index,若不包含傳回-1。 >>
    console.log(n.indexOf(6));
    
  6. join(): 將所有元素加成一個字串。 >>
    console.log(n.join());
    
  7. lastIndexOf(): 跟indexOf(),只是由後往前搜尋,若是有重複的元素指傳回第一個找到的位址。
  8. map(): 傳回一個新array,其中元素為舊元素傳入某一函數後之傳回。 >>
    console.log(n.map(
        (e) => {return e*e;}
    ));
    

    flatMap()
    flatMap()相當於呼叫map()之後再呼叫flat()使其攤平。
    // Example 1
        let arr = [1,2,3];
        arr.flatMap((x)=>{
            console.log(x*x);
        }); // flatMap((currentValue) => { /* ... */ } )
        arr.flatMap((x, i)=>{
            console.log(arr[i], x*x);
        }); // flatMap((currentValue, index) => { /* ... */ } )
        
        // Example 2
        let song = ["one little", "two little", "three little indians."];
        console.log(song.map(x=>x.split(" ")));
        console.log(song.flatMap(x=>x.split(" "))); // 將結果攤平
        
        // Example 3
        let nums = [];
        for(let i=0; i<10; i++){
            nums.push(Math.floor(Math.random()*987654321)%100-50);
        }
        console.log(nums);
        let output = nums.flatMap(x=> x<0? []:x%2==0?x:[x-1, 1]);
    
        // let output = nums.flatMap(x=>{
        //     if (x<0){
        //         return [];
        //     }else{
        //         if (x%2==0){
        //             return [x];
        //         }else{
        //             return [x-1, 1];
        //         }
        //     }
        // });
        console.log(output);

  9. pop(): 移除array的最後一個元素並傳回該元素。 >>
    console.log(n.pop() + "\n" + n);
    
  10. push(): 在array後面加上一個或多個元素並傳回新array的length。 >>
    console.log("length = " + n.push(18, 22) + "\n" + n);
    
  11. reduce():將一個函數每次應用至兩個元素使其成為一個元素,依次應用至array所有元素,最後回傳一個值。看起來多拗口,看以下例子。 >>
    function add(e1, e2){
        return e1+e2;
    }
    console.log(n.reduce(add));
    
    看例子就好理解得多,將array中前兩個數字加起來,再將和與下一個數字相加,這樣一直到array中所有元素都被加上去,最後得到一個數字也就是所有元素之和。
    試試看這個code。
    console.log(
        n.reduce(
            (e1,e2)=>{return e1*e2;}
        )
    );
    
  12. reduceRight(): 跟reduce()一樣,只是方向相反,由右向左。 >>
    console.log(
        n.reduceRight(
            (e1,e2)=>{return e1*e2;}
        )
    );
    
  13. reverse(): 將array中的元素順序倒轉。 >>
    console.log(n.reverse());
    
    這個結果跟上一個例子似乎相同,實際上上一個例子只是倒過來印出,這個例子是改變了array的內容,也就是此時n[0]變成了原來的最後一個元素。
  14. shift(): 跟pop()相反,此為移除第一個元素並傳回該元素。 >>
    console.log(n.shift() + "\n" + n);
    
  15. slice(): 將array的一部分切片形成一個新的array並傳回。 >>
    console.log(n.slice(1,3) + "\n" + n);
    
    傳回[n[1], n[2]](第二個數字位址不包含),而原array不變。
  16. some(): 如果array內至少一個元素符合特定函數標準,傳回true,否則傳回false。 >>
    console.log(
        n.some(
            (e) => {return e > 10;}
        )
    );
    
    跟every()類似,只是every()需要所有元素都符合才傳回true。
  17. sort(): 排序array。 >>
    n.sort();
    console.log(n);
    
    顯示出的結果是[11, 18, 2, 3, 6],好像不太對,事實上是排序好了,只是不是照數字大小,而是字串大小。 若是要排序數字,需要加上一個函數。 >>
    function compare(e1, e2){
        return e1 > e2;
    }
    n.sort(compare);
    console.log(n);
    
    若是想要由大到小排序,可以修改函數,或是記得之前介紹的reverse()函數? 若是要排序的是物件?只要把比較函數修改一下即可。 >>
    var sedans = [];//create array
    sedans.push(new sedan("Porche", 5, "Expensive"));
    sedans.push(new sedan("BMW", 5, "Expensive"));
    sedans.push(new sedan("Lamborghini", 5, "Expensive"));
    sedans.push(new sedan("Benz", 5, "Expensive"));
    sedans.push(new sedan("Farrari", 5, "Expensive"));
    var compareSedan = function(s1, s2){ // compare function
        if (s1.brand > s2.brand){
            return 1;
        }else if(s1.brand < s2.brand){
            return -1;
        }else{
            return 0;
        }
    }
    console.log(sedans.sort(compareSedan)); // sort
    
    首先建立物件的constructor,然後建立array,使用之前介紹的push()方法將物件加入,再建立一個比較函數,然後就可以排序了。 若是要找array中的最大或最小,可以直接排序然後找第一個(或最後一個或使用reverse())即可。不過這樣比較沒效率,只是要找一個值,也可以使用如下:
    console.log(Math.max.apply(null, n));
    
    將數學函數Math.max(或Math.min)應用到array上。不過這只適用於array內的元素不太多的情況,所以可以使用如下方式。 >>
    function bigger(e1, e2) {
        return Math.max(e1, e2);
    }
    console.log(n.reduce(bigger));
    
    利用之前教過的reduce()函數,一路檢查array到底然後保留最大的。 Shuffle: 將array內的元素打亂。例如撲克牌洗牌,可以使用sorting方法。 >>
    console.log(
        n.sort(
            (e1, e2) => {return Math.random() - 0.5;}
        )
    );
    
    每一次重載網頁可以看到陣列的排列順序都不同,Math.random()函數會傳回0-1之前的隨機數,將在後面的章節介紹。
  18. splice(): 加入或刪除array內的元素。 >>
    console.log(n);
    n.splice(2,2,20,30,40,50,60);
    console.log(n);
    
    參數的意義是自index=2開始(第一個參數)刪除兩個元素(第二個參數),並在同位置加上後面所有元素(其餘的所有參數)。
  19. toString(): 傳回元素內容字串。 >>
    console.log(n.toString());
    
  20. unshift(): 跟shift()相反,在array前端加上一個(或多個)元素並傳回新array的length。 >>
    console.log(n.unshift(100,200) + "\n" + n.toString());
    
使用箭頭函數來一次使用多個方法:
const ar = [1,2,3,4,5];
const result = ar.map(n=>n*3)
                 .filter(n=>n%2===0)
                 .reduce((a, b)=>a+b, 0);
console.log(result);

More about Array
Array.isArray():用來判斷物件是否為陣列。
let a1 = [];
let a2 = "123";
let a3 = {a:1, a:2, a:3};
let a4 = 789;
console.log(Array.isArray(a1)); // true
console.log(Array.isArray(a2)); // false
console.log(Array.isArray(a3)); // false
console.log(Array.isArray(a4)); // false
Array.from():將類陣列物件(e.g. string)或是可迭代物件(e.g. Map(儲存key-value pairs資料的物件), Set(儲存unique值的物件))轉換成Array。
// string to array
const s1 = "abc";
console.log(Array.from(s1)); // Array(3) [ "a", "b", "c" ]
console.log(Array.from(s1, (ele)=>{return ele.repeat(3);})); // Array(3) [ "aaa", "bbb", "ccc" ]

// object to array
let obj = {
	'0': 0,
	'4': 44, 
	'1': 11,
	'3': 33, 
	'2': 22, 
	length:5
};
console.log(Array.from(obj)); // Array(5) [ 0, 11, 22, 33, 44 ]

// map to array
let map = new Map();
map.set(1, 'do');
map.set(2, 're');
map.set(3, 'mi');
console.log(map.size); // 3
console.log(map.get(1)); // do
console.log(Array.from(map)); 
/* 	Array(3) [ (2) […], (2) […], (2) […] ]

0: Array [ 1, "do" ]

1: Array [ 2, "re" ]

2: Array [ 3, "mi" ] */

// set to array
let set  = new Set([1,2,3,4,5]);
console.log(set.has(1));
console.log(Array.from(set));//Array(5) [ 1, 2, 3, 4, 5 ]
Array.of():可快速將數字字串轉換成陣列。
let a = Array.of(1,2,3,'do','re','mi'); 
console.log(a); // Array(6) [ 1, 2, 3, "do", "re", "mi" ]
Array.includes():判斷陣列是否包含特定的元素,傳回boolean。
let arr = [1,2,3,'a','b','c'];
console.log(arr.includes(2)); // true
console.log(arr.includes(1, 2)); // false, 從位置2(arr[2])開始尋找
console.log(arr.includes('c')); // true
console.log(arr.includes('x')); // false
Array.flat():將巢狀array攤平。
let arr1 = [1,2,[3,4],[5,6]];
console.log(arr1.flat()); // 攤平一層(預設值)

let arr2 = [1,2,[3,4,[5,6]]];
console.log(arr2.flat(2)); // 攤平兩層

let arr3 = [1,2,[3,4,[5,6,[7,8]]]];
console.log(arr3.flat(3)); // 攤平三層

let arr4 = [1,2,[3,4,[5,6,[7,8,[9,10,[11,12]]]]]];
console.log(arr4.flat(Infinity)); // 使用Infinity關鍵字來無限攤平(無論幾層)

function's arguments


之前我們提到的函數,其輸入參數是一個物件,其中包含一個array,名稱為arguments。 >>
function area(w,h) {
    console.log(arguments);
    return w*h;
}
area(10,20);
呼叫方法後可以看到印出arguments這個陣列,其中包含了輸入的參數。我們可以利用這個陣列來直接對輸入參數做計算。 >>
function sum(){
    var s = 0;
    for(let i = 0; i < arguments.length; i++){
        s = s + arguments[i];
    }
    return s;
}
console.log(sum(1,2,3,4,5,6,7,8,9,10));
練習寫一個函數找出輸入參數的最大值。

Map & Set

前述的物件({})與陣列([])都是資料的container,只是其索引(index)不同,array是預定的數列,物件是自訂的key(類似python中的dict或是java中的map)。問題是物件中的key-value pair中的key是字串,也就是說key的型態無法被保留。例如:
const obj1 = {
    'true': 'String true',
    true: 'Boolean true',
}; 
console.log(obj1['true']); // Boolean true
console.log(obj1[true]); // Boolean true
從結果可以看出'true'跟true都是一樣的,其值都是Boolean true(因為是後定義)。為了克服這個問題,JavaScript提供Map()資料結構供使用。

Map()


顯然Map()是類似物件且其key是可以有型態的分別,例如:
let map = new Map();
map.set(true, 'a Boolen');
map.set('true', 'a String');
console.log(map.get(true)); // a Boolen
console.log(map.get('true')); // a String
很明顯Map使用set(key, value)方法來設定key-value pair,並使用get(key)來取得對應該key的值。呼叫set(key, value)會傳回Map物件本身,所以可以寫這樣:
const obj = {};
const map = new Map(); 
map.set('string', 'is a string').set(true, 'is a boolean').set(1, 'is a number').set(obj, 'is an object');
console.log(map);
console.log(map.has(obj));
map.delete(obj);
console.log(map);
因為呼叫set(key, value)方法會傳回Map自身,所以可以連寫下去。此外,可以使用has(key)來判斷是否存在該key以及delete(key)來刪除該資料對。之前提過可以將Map這種類陣列使用Array.from()轉換成陣列,而Map()建構子可以直接將array轉換成Map(array內容須為資料對,否則會出現預設值undefined)。例如:
let arr = [[1, "one"],[2, "two"],[3, "three"]];
let map = new Map(arr);
console.log(map.size);
console.log(map);
map.set(4, 'four').set(5, 'five').set(6, 'six');
console.log(map.size);
console.log(map);

traverse


使用for loop來traverse。
let arr = [[1, "one"],[2, "two"],[3, "three"], [4, 'four'], [5, 'five'], [6, 'six']];
let map = new Map(arr);

console.log("..........get value.........");
for (let v of map.values()){
    console.log(v);
}

console.log("..........get key.........");
for (let k of map.keys()){
    console.log(k, map.get(k));
}

console.log("..........get [key, value].........");
for (let [key, value] of map.entries()){
    console.log(key, value);
}

console.log("..........use forEach.........");
map.forEach((value,key) => console.log(key,value));
要注意使用forEach的時候,輸入參數是(value, key)。

轉換


Map轉換成陣列(array)、字串(String)或JSON
let arr = [[1, "one"],[2, "two"],[3, "three"], [4, 'four'], [5, 'five'], [6, 'six']];
let map = new Map(arr);

let toStr = [...map].join('\n'); // map to string
console.log(toStr);

let toArr = Array.from(map); // map to array
console.log([...toArr].join('\n')); // array to string (or simply use String(toArr))

let js = JSON.stringify([...map]);
console.log(js); // string [[1,"one"],[2,"two"],[3,"three"],[4,"four"],[5,"five"],[6,"six"]]
let toJSON = JSON.parse(js); //
console.log(toJSON[0]); // Array [ 1, "one" ]

let jsonToMap = new Map(toJSON); // json to map
console.log(jsonToMap);
array(資料對)、map、與object三者轉換,使用Object.fromEntries(entries)Object.entries(obj)
// array & map
let entries = [[1, "one"],[2, "two"],[3, "three"], [4, 'four'], [5, 'five'], [6, 'six']];
let map = new Map(entries);

// array to object
let obj = Object.fromEntries(entries);
console.log("----------array to object----------\n", obj);

// map to object
let mapobj = Object.fromEntries(map);
console.log("----------map to obj----------\n", mapobj);

// object to array
let toArr = Object.entries(obj);
console.log("----------object to array----------\n", toArr);

合併maps。
let arr1 = [[1, "one"],[2, "two"],[3, "three"]];
let arr2 = [[4, 'four'], [5, 'five'], [6, 'six']];
let map1 = new Map(arr1);
let map2 = new Map(arr2);
let map = new Map([...map1, ...map2]);
console.log(map);
map.clear(); // clear
console.log(map.size); // 0

Set()


Set跟Map類似,不過不是資料對(或是說key value同值),特色是不能有重複的原件,因此Set並沒有索引(index)。
const arr = ['do', 're', 'mi'];
const set = new Set(arr);
set.add(1); // add element
set.add('do');
console.log(set); // Set(4) [ "do", "re", "mi", 1 ]
console.log(set.has('do')); // true
set.delete('re'); // delete element
console.log(set.size); // 3

const colors = ['red', 'green', 'blue'];
const union = new Set([...set, ...colors]); // combine two set

for (let i of union){ // traverse by for ... of ...
    console.log(i);
}
// traverse by forEach()
set.forEach((k,v) => console.log(k,v)); // set.forEach((v) => console.log(v));

const toArr = [...union]; // set to arr, or -> const toArr = Array.from(union);
console.log(toArr);
真的要根據索引值取得個別的值,只好先轉換成array。

字串(String)

String也是內建的物件,型態是string。宣告時可以使用單引號或雙引號,或是使用new String("...")物件方式宣告(此時型態為object)。>>
var s1 = "abc";
var s2 = new String("abc");
var s3 = new String("abc");
console.log(s1==s2);//true > same content
console.log(s1===s2);//false > different type
console.log(s2==s3);//false > different objects
console.log(s2===s3);//false > different objects
第一個結果是因為兩個字串內容相同,之後幾個結果是因為型態不同或物件不同。(在JavaScript內比較兩個物件永遠都是false)

字串屬性


其實字串就是字元的陣列(array),所以一樣有length這個屬性。 >>
console.log(s1.length);

for(let i=0; i < s1.length; i++) {
    console.log(s1[i]);
}

for(i in s1){
    console.log(s1[i]);
}

for(i of s1){
    console.log(i);
}
跟array的做法相同,若要使用forEach需要先分拆字串,在後面的方法介紹。不過字串雖然是個array,但是方法不大相同,無法通用,例如我們可以使用 s1[1]來得到字元,但是卻無法使用s1[1]='d';來改變字串內容。

字串方法(methods)


字串也有許多內建方法供我們使用,如下:
  1. charAt(): 在某個index位置的字元。
    console.log(s1.charAt(1));
    
  2. charCodeAt():在某個index位置字元的Unicode value。 >>
    console.log(s1.charCodeAt(1));
    console.log("\u0062");
    
    第一行傳回b的unicode value而第二行是使用該value來印出b,但為何要使用62?那是因為要換成16進位。
    換成16進位也可以讓電腦幫忙。 >>
    var b = 98;
    console.log(b.toString(16));
    
    若要知道數字的2進位或8進位只要把其中的數字改為2或8即可。
    也可以不換成16進位,使用String.fromCharCode()方法即可。 請注意fromCharCode()是String類別方法,不是物件方法,所以要在之前加上String。 >>
    console.log(String.fromCharCode(98, 99, 100, 0x2014, 0x03a4, 999));
    
    unicode範圍介於0到65535(0xFFFF),這個方法輸入10進位或16進位皆可,你可以試著自己輸入不同數字。
  3. concat(): 連結兩個字串並傳回新字串。
    var s = "xyz";
    s = s.concat(s1);
    console.log(s);
    
    跟array相同,不過字串也可以使用+號來串聯。
  4. endsWith():檢查字串是否結尾於某子字串,傳回boolean。 >>
    console.log("String s ends with 'bc' : " + s.endsWith("bc"));
    
  5. includes: 檢查字串是否包含某子字串,傳回boolean。 >>
    console.log(s.includes("ab"));
    
  6. indexOf(): 尋找字串某一個子字串,傳回第一個出現的index,若不包含則傳回-1。 >>
    console.log(s.indexOf("za"));
    
  7. lastIndexOf(): 與indexOf()類似,只是搜尋方向自後向前。 >>
    console.log(s = s.concat("xyzabc"));
    console.log(s.concat("xyzabc").lastIndexOf("za"));
    
  8. localeCompare(): 比較兩個字串排序的先後,傳回1表示原字串在比較字串(傳入參數)之後,-1在之前,0則表示相同。 >>
    console.log(s.localeCompare("xyzabcxyzabb") + 
    "\t" + s.localeCompare("xyzabcxyzabc") + 
    "\t" +s.localeCompare("xyzabcxyzabd")
    );
    
  9. match(): 傳回符合regular expression(RE)的子字串。 >>
    var str = "Check js8.html, js12.html, and probably js110.html ..."
    var re = /js\d+\.html/g;
    console.log(str.match(re));
    
    regular expression可以幫助我們尋找符合某種描述的字串。
    matchAll()
    matchAll()傳回符合條件所有元素之iterator。
  10. repeat(): 將字串重複給定次數。 >>
    console.log(s.repeat(3));
    
  11. replace(): 用新字串替代字串中符合某re規則的子字串。 >>
    console.log(s.replace("abc", "ijk"));
    console.log(s.replace(/abc/g, "ijk"));
    
    沒有re規則只替換了第一個子字串。
  12. search(): 搜尋字串中符合某re規則的子字串,傳回index,-1表示不包含該re規則的子字串。 >>
    console.log(str.search(re));
    console.log(str.search("js110.html"));
    
  13. slice(): 傳回某一範圍內之子字串。 >>
    console.log(s.slice(3,10));
    
    若是僅給一個數字表示直至字串的最後。
  14. split(): 將字串根據某字元分拆為數個單元並傳回包含所有單元之array。 >>
    console.log(s.split("a"));
    console.log(s.split(""));
    
  15. startsWith(): 檢查字串是否開始於某子字串,傳回boolean。 >>
    console.log(s.startsWith("xy"));
    
  16. substr(): 傳回某位置之後某長度的子字串。>>
    console.log(s.substr(3, 5));
    console.log(s.substr(-3, 5));
    
  17. substring(): 傳回某一範圍內之子字串。效果同slice()。 >>
    console.log(s.substring(3,10));
    
  18. toLocaleLowerCase(): 將字串中字元轉換成小寫。 >>
    console.log("ABCDEFG".toLocaleLowerCase());
    
  19. toLowerCase(): 將字串中字元轉換成小寫。與toLocaleLowerCase()同。 >>
    console.log("ABCDEFG".toLowerCase());
    
  20. toUpperCase(): 將字串中字母轉換為大寫。 >>
    console.log("This is a book.".toUpperCase());
    
  21. trim(): 去除字串頭尾的空白。 >>
    console.log("   well...\t".trim() + "   well...\t".trim().length);
    

Events

之前設計的每一個函數,都是直接給指令執行,當操作網頁時,我們希望函數在特定的情形下才會被執行,例如在文字輸入列輸入了資料或是按了一個按鈕,Event的含意便是讓我們控制函數執行的時機。
以下為部分Events: >>

Exercise


先設計一個按鈕,事件是onclick: >>
<form>
    <input type="button" onclick="sayHello()" value="Hello">
</form>	
再加上以下的函數。
function sayHello(){
    console.log("Hello, World.");
}
接下來每按下一次按鈕就會印出一次Hello World。
一個element可以有超過一個事件,所以現在再加上一個函數。
function mouseover(){
    console.log("Mouse pointer just move over.");
}	
然後在原來的button之onclick之後加上
onmouseover="mouseover()"
兩者中間用空白分隔,現在將滑鼠移至按鈕上方,可以看到結果。


再試一個例子(尚無功能,僅做練習)。 >>
<div id="toDrag" class="dragtarget" draggable="true" 
ondragstart="dragStart()" ondragend="dragEnd()" 
ondragover="dragging()" ondragover="allowDrop(event)">
Drag this.
</div>

<div id="toDrop" class="dragtarget" ondragover="allowDrop(event)" ondrop="dropping()">
</div>

The css code:
div.dragtarget{
    font-size: 100%;
    width:100px; 
    height: 100px; 
    margin: 15px; 
    padding: 10px; 
    background:green; 
    border: 1px solid red;
    float:left;
}

The functions:
function dragging(){
    console.log("Element is being dragging.");
}

function dropping(){
    console.log("Element is dropped.");
}

function dragStart(){
    console.log("Start dragging.");
}

function dragEnd(){
    console.log("End dragging.");
}

function allowDrop(event){
    event.preventDefault();
}

allowDrop(event)這個函數主要是允許將element放在另一個element之內,預設值是無法將物品拖移到另一個物品內。
Drag this.







在設計函數時,可以加上元素作為輸入索引,例如:
<button id="tar" onclick="fun(this)" name="clickme">click</button>
<script>
	function fun(ele){
		if(ele.hasAttribute('name')){
			console.log(ele.getAttribute('name'));
		}else{
			console.log("No such attribute.");
		}
	}	
</script>
傳入之this表示物件自身。
也可以這樣寫。
<button id="tar" name="clickme">click</button>
<script>
	let e = document.getElementById('tar');
	e.onclick = function (){
		if(this.hasAttribute('name')){
			console.log(this.getAttribute('name'));
		}else{
			console.log("No such attribute");
		}
	}
</script>

DOM

DOM是Document Object Model的縮寫,每次載入網頁時,瀏覽器便會建立DOM,DOM將網頁內的物件以樹狀排列。例如head跟body是html的children,title又是head的child,而html為整個樹的root。這個結構可以協助我們取得網頁內的物件,進而針對物件進行新增修改刪除等操作。
HTML DOM document物件是網頁中所有物件的擁有者,所以document便是表示整個網頁,要取得網頁中的物件,永遠都要使用document關鍵字開頭。document擁有以下方法來讓我們取得、改變與增刪物件。
取得物件element後,可以使用屬性或方法來取得或改變屬性的值。
屬性: 方法:

Finding elements


根據上述,舉例說明如何取得網頁物件。 >>
先設計以下html。
<h5 id="id11_1">Say hello inside h5</h5>
<form>
    <input type="button" onclick="func11_1()" value="click">
</form>

加上函數。
function func11_1(){
    document.getElementById("id11_1").innerHTML = "Inside text has been changed at" + Date();
    document.getElementById("id11_1").style.color = "blueviolet";
    document.getElementById("id11_1").style.background = "#1fff1f";
    document.getElementById("id11_1").style.textDecoration = "underline";	
}
按以下按鈕看結果。
Say hello inside h5
再試試看使用getElementsByTagName。 >>
<p>第一個p</p>
<p>第二個p</p>
<p>第三個p</p>
<form>
    <input type="button" onclick="func11_2()" value="按這裡">
</form>

加上函數。
function func11_2(){
    var x = document.getElementsByTagName("p");
    x[0].innerHTML = Date();
    x[1].style.color = "blue";
    x[2].setAttribute('class','p11');	
}

第一個p

第二個p

第三個p


顯然使用byTagName會得到所有相同Tag的物件(所以是Elements),而且這些物件會放在類似array的結構內(稱為collection)。 此外,第三個p顯然在css有個名為p11的class來改變其背景顏色。

接下來再試試看使用getElementsByClassName。 >>
<h6 class="js11_3">h6</h6>
<p class="js11_3">p</p>
<section class="js11_3">section</section>
<form>
    <input type="button" onclick="func11_3()" value="Press this button">
</form>	

加上函數。
function func11_3(){
    var x = document.getElementsByClassName("js11_3");
    x[0].innerHTML = Date();
    x[1].style.color = "blue";
    x[2].setAttribute('class','p11');	
}
h6

p

section

顯然跟getElementsByTagName類似,只是這次抓取所有相同class name的物件。
另一個類似的方式為使用querySelectorAll()方法,此方法可以抓取相同CSS selector的元件。 >>
<h6 class="js11_3">h6</h6>
<p class="js11_3">p</p>
<section class="js11_3">section</section>
<form>
    <input type="button" onclick="func11_3()" value="Press this button">
</form>

加上函數。
function func11_4(){
    var x = document.querySelectorAll(".js11_4");
    x[0].setAttribute('class','p11');
    x[1].innerHTML = Date();
    x[2].style.color = "blue";		
}
h6

p

section

記得argument是CSS selector,所以要用.js11_4。
上述兩方式效果類似,一個傳回collection,一個傳回nodelist,差別是collection是elements的集合,而nodelist是document nodes的集合。 文件中所有東西都是node,而element是一種特殊的node。只有nodelist物件可以包含屬性node與文字node。 我們可以根據名稱,id,或index number取得collection內的元件,但是只能使用index number來取得nodelist內的元件。

Exercise: Calculator

Navigating Between Nodes


我們已經了解整個在HTML document中任何東西都是一個node,例如整個document是一個document node, 每一個HTML element都是一個element node,文字是一個文字node,每一個屬性是屬性node,連註解也是一個註解node。 而所有這些形成一個樹狀排列。通常html是root,head, body是他的child,head跟body彼此是siblings。因此我們 可以利用之前列出的許多方法找到某一個node的對應關係物件,例如其child或是parent。 >>
設計一個按鈕並利用之前用來展示bubbling&capturing的物件。
<Button onclick="func12_9()">click</Button>

加上函數。
function func12_9(){
    var t1 = document.getElementById("div12_7_b").firstElementChild.innerHTML;
    var t2 = document.getElementById("p12_7_b").firstChild.nodeValue;
    var t3 = document.getElementById("div12_7_b").children[0].innerHTML;
    var t4 = document.getElementById("p12_7_b").childNodes[0].nodeValue;
    console.log(t1 + "\t" + t2 + "\t" + t3 + "\t" + t4);
}

按下按鈕後可以發現兩者皆得到相同的文字,對於p來說,其內的文字是其child node,但是text並不是一個element, 所以使用nodeValue取得其內的值。而p是div的first element child,不是第一個node child,所以使用firstElementChild取得p, 此時使用innerHTML來得到element內的值。此外,顯然children取得所有的child elements,而childNodes取得所有的child nodes。 要注意的是對於element來說,其nodeValue是undefined。

Adding & Deleting


可以使用前述的方法來增減文件。>>
<div id="div12_10"></div>
<Button onclick="func12_10a()">Button A</Button>
<Button onclick="func12_10b()">Button B</Button>
<Button onclick="func12_10c()">Button C</Button>
<Button onclick="func12_10d()">Button D</Button>

加上函數。
function func12_10a(){
    var aNode = document.createAttribute("class");
    aNode.value = "d12_7";
    document.getElementById("div12_10").setAttributeNode(aNode);
    var textNode = document.createTextNode("Inside div");
    document.getElementById("div12_10").appendChild(textNode);
}

此時按下Button A可看見變化。 再加上Button B的函數。
function func12_10b(){
    var onep = document.createElement("p");
    var textnode = document.createTextNode("Inside P");
    onep.appendChild(textnode);
    onep.setAttribute("class","p12_7");
    document.getElementById("div12_10").appendChild(onep);
}

使用setAttribute()可得到與setAttributeNode()一樣的效果。 再加上Button C的函數。
function func12_10c(){
    var twop = document.createElement("p");
    var textnode = document.createTextNode("Inside P two");
    twop.appendChild(textnode);
    twop.setAttribute("class", "p12_7");
    var one = document.getElementById("div12_10").firstElementChild;
    one.insertAdjacentElement('afterend',twop);
}

可以看到加上第一個p的sibling,接著再加上Button D的函數。
function func12_10d(){
    var d = document.getElementById("div12_10");
    d.removeChild(d.lastElementChild);
}

可以看到移除了最後一個p。

Event Linstener

可以使用addEventListener方法直接將event跟函數連結在物件上,讓物件隨時傾聽是否有event,效果跟前述的events作用相同(名稱為去除on)。 >>
<button id="bt11_5" style="color:olive;">click</button>
<script>
    document.getElementById("bt11_5").addEventListener("click", func11_5);
</script>

加上函數。
function func11_5(){
    document.getElementById("bt11_5").innerHTML = "clicked";		
    document.getElementById("bt11_5").style.color = "blue";			
}

跟之前使用onclick類似,有一點要注意的是addEventListener的script要寫在元件(button)之後(如上),不然會因傳回空(null)而出錯。

跟之前一樣的,同一個element可以同時有超過一個event。 >> 事實上同一element也可以加入超過一個相同event的傾聽者,例如兩個click
<button id="bt11_6" style="color:olive;">click</button>
<script>
    function func11_61(){
        document.getElementById("bt11_6").innerHTML = "找到按鈕";		
        document.getElementById("bt11_6").style.color = "blue";			
    }
    function func11_62(){
        document.getElementById("bt11_6").innerHTML = "離開按鈕";		
        document.getElementById("bt11_6").style.color = "black";			
    }
    function func11_63(){
        document.getElementById("bt11_6").innerHTML = "按下按鈕";		
        document.getElementById("bt11_6").style.color = "tomato";			
    }    
    document.getElementById("bt11_6").addEventListener("mouseover", func11_61);
    document.getElementById("bt11_6").addEventListener("mouseout", func11_62);
    document.getElementById("bt11_6").addEventListener("click", func11_63);
</script>

Event Bubbling & Event Capturing


當一個element位於另一個element之內成巢狀排列(Nested)時,若是兩者的event同時被觸發,哪一個element的event要先執行? >>
<div id="div12_7_b" class="d12_7">
    <p id="p12_7_b" class="p12_7">Bubbling</p>
</div>
<div id="div12_7_c" class="d12_7">
    <p id="p12_7_c" class="p12_7">Capturing</h5>
</div>
<script>
    document.getElementById("div12_7_b").addEventListener("click", func12_7_bd, false);
    document.getElementById("p12_7_b").addEventListener("click", func12_7_bp, false);
    document.getElementById("div12_7_c").addEventListener("click", func12_7_cd, true);
    document.getElementById("p12_7_c").addEventListener("click", func12_7_cp, true);
</script>

加上函數。
function func12_7_bd(){
    console.log("Click on div.");			
}
function func12_7_bp(){
    console.log("Click on p.");			
}
function func12_7_cd(){
    console.log("Click on div.");			
}
function func12_7_cp(){
    console.log("Click on p.");			
}
在addEventListener參數加上false表示是Bubbling,true表示Capturing。在之下的圖形中按下bubbling,會發現內層的element之event先執行,Capturing則外層先執行。 可知Bubbling表示由內而外,Capturing表示由外而內。

Bubbling

Capturing

More about propagation:

事件代理

假設我們現在有一個ul如下:
<ul id="ula">
	<li>Apple</li>
	<li>Banana</li>
	<li>Apricot</li>
	<li>Pear</li>
	<li>Plum</li>
	<li>Peach</li>
	<li>Mango</li>
	<li>Melon</li>
</ul>
現在我們希望滑鼠移到的li位置,背景顏色變成藍色。JS這樣寫:
<script>	
	let lis = document.getElementById("ula").children;
	for (let item of lis){
		item.addEventListener("mouseover", (e)=>{
			e.target.style.background = "blue";
		})
		item.addEventListener("mouseout", (e)=>{
			e.target.style.background = "white";
		})
	}
</script>
原則上就是在每一個li上加掛一個eventListener。也就是說如果有100個li就要掛上100個eventListener。不過因為有event propagation,所以我們可以只在ul上加上eventListener,讓這個event傳遞下去,稱為事件代理,也就是把eventlistener加在父節點上。如下:
<script>
	document.getElementById("ula").addEventListener('mouseover', (e)=>{
		if (e.target.tagName === "LI") // 先判斷是否為li
			e.target.style.background = "blue";
	})
	document.getElementById("ula").addEventListener('mouseout', (e)=>{
		e.target.style.background = "white";
	})
</script>	

Stop propagation

有的時候我們並不想要這個propagation的功能,只希望自己顧自己就好了,這個時候可以使用stopPropagation()這個方法。例如:
<div id="container" style="width:74px;height:45px;background-color:pink;">
	<button id = "inner" style="position: absolute;left:20px;top:20px">inner</button>
</div>
<br>
<input type="checkbox" id="check"> <span style="font-size:75%;">勾選以停止propagation</span>
這是一個大腸包小腸的HTML,div內有一個button。此處並設計一個勾選框,讓我們選擇是否要執行stopPropagation()。JS code如下:
<script>
	let inn = document.getElementById("inner");
	let con = document.getElementById("container")
	inn.addEventListener("click", function(e){
		inn.style.border = "2px solid red";
		if (document.getElementById("check").checked) {
			e.stopPropagation();
		}
		
	})
	con.addEventListener("click", function(e){
		con.style.border = "2px dashed blue";
	})
</script>
若是沒勾選方框,點擊按鈕會讓按鈕跟外框div的border都出現,也就是兩個函數都被呼叫了。現在如果我們勾選了方框,再點擊按鈕,發現只有按鈕出現方框,也就是說掛在div上的函數沒有被執行。

preventDefault

preventDefault()函數的意義就是中止預設的功能。例如讓連結無法使用。借用上例,在button外圍加上anchor,也就是點擊button會跳出新視窗到另一網頁。
<a href="http://www.nkust.edu.tw" target="_blank">
	<button id = "inner" style="position: absolute;left:20px;top:20px">inner</button>
</a>
如果我們要中止點下button跳出新網頁,可以加上preventDefault()方法,如下:
inn.addEventListener("click", function(e){
	inn.style.border = "2px solid red";
	if (document.getElementById("check").checked) {
		e.stopPropagation();
	}
	e.preventDefault();
})
如此anchor就變得沒功能了。甚至我們要中止按右鍵出現快捷選單,可以加上以下程式碼:
document.addEventListener("contextmenu", (e)=>{//中止右鍵功能
	e.preventDefault();
})
不過若不是設計甚麼特別遊戲需要取消右鍵功能,僅是設計網站,應不需要使用到此功能。

removeEventListener()


Event handler也可以被移除,使用removeEventListener()方法。 >>
<h1 id="h12_8">按這裡移除listener</h1>
<p id="p12_8" class="p12_7">顯示滑鼠位置</p>
<script>
    document.getElementById("h12_8").addEventListener("mousemove", func12_8);
    document.getElementById("h12_8").addEventListener("click", func12_8r);
</script>

加上函數。
function func12_8(){
    document.getElementById("p12_8").innerHTML = Math.random();
}
function func12_8r(){
    document.getElementById("h12_8").removeEventListener("mousemove", func12_8);
}

按這裡移除listener

顯示滑鼠位置

Event Object


當我們跟設計的網頁互動時,原則上就是一個event,例如我們按下了滑鼠,或是鍵盤的某個鍵等。如前所述,我們可以在網頁的物件上使用addEventListner來傾聽我們跟元件互動的event。看一下addEventListner的函數定義:
element.addEventListener(event, function, useCapture)
其中第一個參數是event,必須要有,定義要傾聽的event,可以是點擊滑鼠某鍵,也可以是按下鍵盤某鍵。第二個參數是觸發時要執行的函數,也是必須要有的。最後一個是控制是Bubbling或是Capturing,因為名稱為useCapture且有內定值為false,所以可以不給值,因為預設為bubbling,若是想要使用capturing則輸入true。
當我們觸發event後(e.g. click),JS會回傳一個物件(event object),包含相關的資料,且此物件通常作為觸發函數的輸入值。例如:
<button id="tar">click</button>
<div id="demo">
	A Demo Div.
</div>
<script>
	let btn = document.getElementById("tar");
	btn.addEventListener('click', (e)=>{
		console.log(e); 
		// click { target: button#tar, buttons: 0, 
		// clientX: 34, clientY: 14, layerX: 34, layerY: 14 }
	})
</script>
當使用滑鼠點擊按鈕後,在我的fire fox顯示出註解的內容,確認e為event click的物件。而event object本身有許多性質(event object properties)可供我們查詢使用,此處僅介紹幾個常用的。
<script>
	let btn = document.getElementById("tar");
	btn.addEventListener('click', (e)=>{
		console.log(e.type); // click >> event 的名稱
		console.log(e.target); // <button id="tar"> >> 被event作用的元件
	})
	
	btn.addEventListener('mouseover', (e)=>{ // 試著把btn換成document
		e.target.style.background = "green";
	})

	btn.addEventListener('mouseout', (e)=>{ // 試著把btn換成document
		e.target.style.background = "white";
	})

	document.addEventListener('keydown', (e)=>{ // 使用document來將linster加到整個網頁
		console.log(e.keyCode + " " + e.key); // keyCode是代碼,key是字元
		let k = e.key;
		let di = document.getElementById("demo");
		switch (k){
			case 'b':
				di.style.color = 'blue';
				break;
			case 'r':
				di.style.color = 'red';
				break;
		}
	})
</script>

做個練習:
<section id="sec">
	<div id="demo1">
		A Demo Div 1.
	</div>
	<div id="demo2">
		A Demo Div 2.
	</div>
	<div id="demo3">
		A Demo Div 3.
	</div>
</section>
<style>		
	#sec{
		display: flex;
		justify-content: center;
	} 
	#sec>div{
		width: 100px;
		height: 100px;
		margin: 0px 10px;
		border: 1px solid red;
	}
</style>
<script>
	let s = document.getElementById("sec");
	for (let c of s.children){
		c.addEventListener("mouseover", function(e){
			// 若用箭頭函數,this指的便不是這個物件
			for (let d of s.children){
				if (d === this){
					this.style.backgroundColor = "blue";
					this.style.opacity = 1;
				}else{
					d.style.backgroundColor = "grey";
					d.style.opacity = 0.5;
				}
			}
		})

		c.addEventListener("mouseout", function(e){
			for (let d of s.children){
				// 可以寫成CSS再加入CSSLists
				d.style.backgroundColor = "white";
				d.style.opacity = 1;
			}
		})
	}
</script>
A Demo Div 1.
A Demo Div 2.
A Demo Div 3.

此外,我們還可以得到滑鼠位置之座標。
function coords(event){
	let di = document.getElementById("demo");
	di.style.width = '200px';
	di.style.height= '2000px';
	di.style.border = '1px solid blue';
	let xpos;
	let ypos;
	// screenX/Y >> 根據螢幕範圍的座標(原則上沒甚麼用)
	xpos = event.screenX; 
	ypos = event.screenY; 

	let px;
	let py;
	// pageX/Y >> 根據頁面範圍的座標(計入卷軸掉的部分)
	px = event.pageX;
	py = event.pageY;

	let cx;
	let cy;
	// clientX/Y >> 根據頁面可見範圍的座標(不計卷軸掉的部分)
	cx = event.clientX;
	cy = event.clientY;

	if (event.type === 'mousemove'){
		di.innerHTML = 
`x=${xpos}, y=${ypos}<br>
px=${px}, py=${py}<br>
cx=${cx}, cy=${cy}<br>`;
	}
	if (event.type === 'click'){
		console.log(
`x=${xpos}, y=${ypos}
px=${px}, py=${py}
cx=${cx}, cy=${cy}`);
	}
}
document.getElementById("demo").onmousemove  = coords;
document.getElementById("demo").onclick = coords;
</script>

Toggle
整理一下關於toggle的寫法,使用以下的ul為例。
<ul id="wlist">
    <li value="0">Ford</li>
    <li value="0">Benz</li>
    <li value="0">Misibushi</li>
    <li value="0">Toyota</li>
    <li value="0">BMW</li>
</ul>
欲在滑鼠點擊後變換背景顏色,可以使用如下方法。
  • 使用closure:
    'use strict'
    
    let ww = document.getElementById("wlist");
    for(let j=0; j<ww.children.length; j++){
        // closure
        function toClick(){
            let isClick = true;
            return function(){
                if (isClick){
                    ww.children[j].style.background = 'green';
                    isClick = false;
                }else{
                    ww.children[j].style.background = 'white';
                    isClick = true;
                }
            }
        }
        ww.children[j].onclick = toClick();
    }
  • 使用let:
    'use strict'
    
    let ww = document.getElementById("wlist");
    for(let j=0; j<ww.children.length; j++){
        let clickFun;
        {
            let isClick = true;
            let toClick = function(){
                if(isClick){
                    ww.children[j].style.background = 'orange';
                    isClick = false;
                }else{
                    ww.children[j].style.background = 'white';
                    isClick = true;
                }
            }
            clickFun = {
                toClick: toClick,
            }
        }
        ww.children[j].onclick = clickFun.toClick;
        
    }
  • 使用element內的attribute:
    'use strict'
    
    let ww = document.getElementById("wlist");
    for(let j=0; j<ww.children.length; j++){
        ww.children[j].onclick = function(){ 
            if (ww.children[j].getAttribute('value')==0){
                ww.children[j].style.background = 'olive';
                ww.children[j].setAttribute("value", "1");
            }else{
                ww.children[j].style.background = 'white';
                ww.children[j].setAttribute("value", "0");
            }
        }
    }
  • 使用classList.toggle: 首先先建立一個CSS如下:
    .toGrey{
        background-color: lightgrey;
    }
    然後Javascript code如下:
    'use strict'
    
    let ww = document.getElementById('wlist');
    
    for (let ele of ww.children){
        ele.onclick = function(){
            ele.classList.toggle('toGrey');
        }
    
    }
    使用此方式在點擊時轉換元件的css class。
欲在滑鼠移入與移出變換背景顏色,可以使用如下方法。
  • 'use strict'
    
    // 需要onmouseover + onmouseleave才能操作
    let ww = document.getElementById('wlist');
    console.log(ww.childNodes);
    
    for (let ele of ww.children){
    //for (let ele of ww.childNodes){
        ele.onmouseover = function(){
            ele.style.background = 'lightgrey';
        }
        ele.onmouseleave = function(){
            ele.style.background = 'white';
        }
    }
    注意使用children與childNodes的差別,children取得元件,childNodes會得到所有包含文字的節點。以下使用不同的for loop寫法再做一次:
    let ww = document.getElementById('wlist');
    for (let i=0; i<ww.children.length; i++){
        ww.children[i].onmouseover = function(){
            ww.children[i].style.background = 'lightgrey';
        }
        ww.children[i].onmouseleave = function(){
            ww.children[i].style.background = 'none';
        }
    }
  • 使用addEventListener:
    'use strict'
    
    let ww = document.getElementById('wlist');
    
    for (let ele of ww.children){
        ele.addEventListener('mouseover', (e)=>{
            ele.style.background = 'lightgrey';
        });
            
        ele.addEventListener('mouseout', (e)=>{
            ele.style.background = 'white';
        });
    }
  • 當然也可以使用前述的event propagation:
    'use strict'
    
    let ww = document.getElementById('wlist');
    
    ww.addEventListener('mouseover', (e)=>{
        if (e.target.tagName === "LI") // 先判斷是否為li
            e.target.style.background = "lightgrey";
    })
    ww.addEventListener('mouseout', (e)=>{
        e.target.style.background = "white";
    })

setSong.html | guestbook.html | RPG | Move Photo | Dodge Box | Moving Bar | Moving Bar 2

AJAX

How it works?


Callback Function


deal XML


JSONP


use JQuery


fetch


開始之前,先了解以下幾種做法:
Promise
JavaScript是屬於同步語言(Synchronous,也就是說一次做一件事,依序完成),當出現需要非同步(asynchronous)的事件時(e.g. ),會先將其放置於Event queue中,等其他事件執行完成後再開始執行Event queue中事件。例如:
console.log("上館子");
(function(){
    console.log("點菜");
    // 等上菜
    setTimeout(function(){
        console.log("上菜");
    }, 0);
})();
console.log('喝飲料');
看執行結果,發現出現上館子->點菜->喝飲料->上菜的執行順序,也就是說,即使上菜的等待時間為0,還是排在最後執行(因為在Event queue中)。再試一個例子:
function randomcolor(){
    return `rgb(${Math.floor(Math.random()*100000)%255}, 
                ${Math.floor(Math.random()*100000)%255}, 
                ${Math.floor(Math.random()*100000)%255})`;
}

console.log("預備");
(function (){
    console.log("點擊換色");
    document.addEventListener('click', ()=>{
        document.body.style=`background: ${randomcolor()};`;
    })
})();
console.log('GO');
可見當使用click、setTimeout()、setInterval()或是AJAX request等,都是非同步。假定有多個非同步的mission,且各自的執行時間不定,會產生甚麼結果?
(()=>{
    setTimeout(()=>{
        console.log("mission 1");
    }, Math.floor(Math.random()*500));
})();

(()=>{
    setTimeout(()=>{
        console.log("mission 2");
    }, Math.floor(Math.random()*500));
})();

(()=>{
    setTimeout(()=>{
        console.log("mission 3");
    }, Math.floor(Math.random()*500));
})();
如果上例中各個mission的執行時間是固定的,結果的順序是根據執行時間完成的。但是很多的因素可能干擾完成的時間(例如ajax連線取得資料),如上例,完成的時間便不確定,導致完成的順序也不確定。若是我們必須讓這些mission依照特定順序完成(例如需等待前一任務的輸出才能進行),此時可以使用callback function來處理。
function mission1(callbackFunction){
    setTimeout(()=>{
        console.log("mission 1");
        callbackFunction();
    }, Math.floor(Math.random()*500));
};

function mission2(callbackFunction){
    setTimeout(()=>{
        console.log("mission 2");
        callbackFunction();
    }, Math.floor(Math.random()*500));
};

function mission3(){
    setTimeout(()=>{
        console.log("mission 3");
    }, Math.floor(Math.random()*500));
};

mission1(()=>{
    mission2(()=>{
        mission3();
    });
});
這個方法的困擾就是會產生過多的巢狀結構,可讀性較差,此時Promise便登場了(ES6)。
  • Promise用來處理非同步事件,事件包含三種狀態pending(運行中), resolved(成功執行), rejected(操作失敗),其語法如下:
    new Promise((resolve, reject) => {
        if (success)
            resolve();
        else
            reject();
    })
  • 也就是說產生一個Promise物件,其參數為一包含resolve跟reject函數之函數。舉例說明,例如模擬一個接力賽跑,每個跑者跑完一圈的時間不定,可能跑完也可能跑不完放棄,且需一棒接著一棒。一個跑者是一個asynchronous事件,設計如下:
    runner = function (name, success){
        return new Promise((resolve, reject)=>{
            if (success){
                usedTime = Math.floor(Math.random()*2000);
                setTimeout(function(){
                    resolve(`${name} 使用${usedTime/1000}秒成功跑完`);
                }, usedTime);
            }else{
                reject(new Error(`${name} 挑戰失敗`));
            }
        })
    }
    runner("Runner A", true).then((data)=>{
        console.log(data);
    }).catch((err)=>{
        console.log(err);
    })
    呼叫runner()函數,會得到一個Promise物件(包含asynchronous事件)。
  • 要使用此物件,主要就是呼叫resolve或reject的狀況,使用then()以及catch()分別取得成功或失敗的結果,如上例。then()會回傳一個新的Promise物件,如此可以讓多個Promise一直串接下去。例如若是有兩個跑者接力,可以寫成如下:
    runner = function (name){
        success = Math.random() > 0.4? true:false; // 成功機率80%
        return new Promise((resolve, reject)=>{
            if (success){
                usedTime = Math.floor(Math.random()*2000);
                setTimeout(function(){
                    resolve(`${name} 使用${usedTime/1000}秒成功跑完`);
                }, usedTime);
            }else{
                reject(new Error(`${name} 挑戰失敗`));
            }
        })
    }
    runner("Runner A").then((data)=>{
        console.log(data);
        return runner("Runner B");
    }).then((data)=>{
        console.log(data);
    }).catch((err)=>{
        console.log(err);
    })
    若是runner A失敗的話,不會執行runner B。 多個跑者的狀況:
    runner("Runner A").then((data)=>{
        console.log(data);
        return runner("Runner B");
    }).then((data)=>{
        console.log(data);
        return runner("Runner C");
    }).then((data)=>{
        console.log(data);
        return runner("Runner D");
    }).then((data)=>{
        console.log(data);
        return runner("Runner E");
    }).then((data)=>{
        console.log(data);
    }).catch((err)=>{
        console.log(err);
    })
    若是前一個跑者失敗,則不會執行之後的任務。
  • 倘若不論之前是否成功,每一個mission都要執行,亦即每個跑者都得下場,則可以這樣寫。
    runner = function (name){
        success = Math.random() > 0.4? true:false; // 成功機率80%
        return new Promise((resolve, reject)=>{
            if (success){
                usedTime = Math.floor(Math.random()*2000);
                setTimeout(function(){
                    resolve(`${name} 使用${usedTime/1000}秒成功跑完`);
                }, usedTime);
            }else{
                reject(new Error(`${name} 挑戰失敗`));
            }
        })
    }
    
    runner("A").then(
        (data)=> {
            console.log(data);
            return runner("B");
        },
        (err) => {
            console.log(err);
            return runner("B");
        }
    ).then(
        (data)=> {
            console.log(data);
            return runner("C");
        },
        (err) => {
            console.log(err);
            return runner("C");
        }
    ).then(
        (data)=> {
            console.log(data);
        },
        (err) => {console.log(err)}
    )

  • Promise.race():若是有多個mission,只需要最先完成的那個,依上例也就是變成賽跑,只取第一名,這時使用Promise.race()。例如:
    runner = function (name){
        success = Math.random() > 0.1? true:false; // 成功機率80%
        return new Promise((resolve, reject)=>{
            if (success){
                usedTime = Math.floor(Math.random()*2000);
                setTimeout(function(){
                    resolve(`${name} 使用${usedTime/1000}秒成功跑完`);
                }, usedTime);
            }else{
                reject(new Error(`${name} 挑戰失敗`));
            }
        })
    }
    
    Promise.race([runner("A"), runner("B"), runner("C")]).then((data)=>{
        console.log(data);
    }).catch((err)=>{
        console.log(err);
    })
  • Promise.all():若是多個mission都要完成才回傳,依上例則為賽跑,等所有選手都跑回終點再公布成績。例如,將上例中的race改為all。
    Promise.all([runner("A"), runner("B"), runner("C")]).then((data)=>{
        console.log(data);
    }).catch((err)=>{
        console.log(err);
    })
    此時會傳回一個array,不過要注意的是若是其中有一個失敗則算全部失敗。

  • Promise.allSettled():若是有多個mission,無論成功與否都會執行每一個。
    Promise.allSettled([runner("A"), runner("B"), runner("C")]).then((data)=>{
        console.log(data);
    }).catch((err)=>{
        console.log(err);
    })
Promise => AJAX
使用Promise來實現AJAX,如下:
<head>
    <title>AJAX</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>

<body>

    <h3>Change Text</h3>

    <button type="button" id="get">Get JSON</button>
    <div id = "div1">	
    </div>
    
    <script>
        function loadDoc(url){
            return new Promise((resolve, reject)=>{
                var xhttp = new XMLHttpRequest();
                xhttp.open("GET", url, true);
                xhttp.onload = function(){
                    if (this.status == 200){ 
                        resolve(xhttp.response); 
                        // or xhttp.responseText->then()的data直接得文字
                    }else{
                        reject(new Error(xhttp));
                    }
                };    
                xhttp.send();
            });
        }

        document.getElementById("get").addEventListener('click', (event)=>{
            loadDoc('http://www2.nkust.edu.tw/~shanhuen/JavaScript/ajax_1.json')
            .then((data)=>{
                console.log(data);
                document.getElementById("div1").innerHTML = data.toString();
            })
            .catch((err)=>{
                document.getElementById("div1").innerHTML = err;
            })
            
        });

    </script>
</body>

Async&Await
在函數定義之前加上async關鍵字,該函數變成傳回一個Promise物件,如下:
console.log(1);
async function f(){
    return "test";
}
f().then((value)=>{
    console.log(value);
}).catch((err)=>{
    console.log(err);
});
//f().then(console.log);
console.log(2);
async函數比之Promise的優勢是它可以結合await關鍵字。await之後須接一個Promise物件(),它可以讓該Promise完成後再往下執行,也就是在前節Promise處提到的依序進行。例如:
function onePromise() {
  return new Promise((resolve, reject)=>{
      setTimeout(()=>{
        resolve("one promise.")
      }, 1000);
  })
};

async function f(){
    console.log("start");
    await onePromise().then((data)=>{
        console.log(data);
    });
    console.log("end");
}

f();
此時可以看到結果是依序進行的。把之前的例子稍作改寫,看多個Promise能否依序進行(接力賽)。
let runner = function (name){
        success = Math.random() > 0.4? true:false; // 成功機率80%
        //success = 1;
        return new Promise((resolve, reject)=>{
            if (success){
                let usedTime = Math.floor(Math.random()*2000);
                setTimeout(function(){
                    resolve(`${name} 使用${usedTime/1000}秒成功跑完`);
                }, usedTime);
            }else{
                reject(new Error(`${name} 挑戰失敗`));
            }
        })
    }

async function f(){
    
    console.log("Start");
    await runner("Runner A").then((data)=>{
        console.log(data);
    });
    await runner("Runner B").then((data)=>{
        console.log(data);
    });
    console.log("End");
};

f();
此時確實是依序進行了,不過若是其中一個失敗,之後的便不會執行了。接下來使用Promise.race()取得最先完成者。
let runner = function (name){
        success = Math.random() > 0.3? true:false; // 成功機率80%
        //success = 1;
        
        return new Promise((resolve, reject)=>{
            if (success){
                let usedTime = Math.floor(Math.random()*2000);
                setTimeout(function(){
                    resolve(`${name} 使用${usedTime/1000}秒成功跑完`);
                }, usedTime);
            }else{
                reject(new Error(`${name} 挑戰失敗`));
            }
        })
    }

async function f(){
    
    console.log("Start");

    await Promise.race([runner("Runner A"), runner("Runner B")]).then((data)=>{
        console.log(data);
    });
    
    console.log("End");
};

f();
當然也能使用Promise.all()來記錄所以成績。
async function f(){
    
    console.log("Start");

    await Promise.all([runner("Runner A"), runner("Runner B")]).then((data)=>{
        console.log(data);
    });
    
    console.log("End");
};
若是無論成功與否,每個跑者都參加比賽,則可以使用Promise.allSettled()。
async function f(){
    
    console.log("Start");

    await Promise.allSettled([runner("Runner A"), runner("Runner B")]).then((data)=>{
        console.log(data);
    });
    
    console.log("End");
};

加入Async&Await來實現AJAX。與前例類似,只是加入Async與Await,如下:
<head>
    <title>AJAX</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>

<body>

    <h3>Change Text</h3>

    <button type="button" id="get">Get JSON</button>
    <div id = "div1">	
    </div>
    
    <script>
        function loadDoc(url){
            return new Promise((resolve, reject)=>{
                var xhttp = new XMLHttpRequest();
                xhttp.open("GET", url, true);
                xhttp.onload = function(){
                    if (this.status == 200){ 
                        resolve(xhttp.response); 
                        // or xhttp.responseText->then()的data直接得文字
                    }else{
                        reject(new Error(xhttp));
                    }
                };    
                xhttp.send();
            });
        }

        document.getElementById("get").addEventListener('click', async (event)=>{
            await loadDoc('http://www2.nkust.edu.tw/~shanhuen/JavaScript/ajax_1.json')
            .then((data)=>{
                console.log(data);
                document.getElementById("div1").innerHTML = data.toString();
            })
            .catch((err)=>{
                document.getElementById("div1").innerHTML = err;
            })
            
        });

    </script>
</body>

XML

XML


XSLT


JSON

Data Storage

Canvas

Canvas是一個畫布,可以讓我們在其上繪圖。 首先建立一個HTML檔案:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>JS Game Practices</title>

  </head>
  <body>
	
	<script src="Canvas.js"></script>
  </body>
</html>
之後JavaScript程式碼寫在Canvas.js即可。接著我們可以很容易地設定一個畫布如下:
<script>
	let canvas = document.createElement("canvas"); <!-- 建立畫布 -->
	canvas.setAttribute("width", "2048"); <!-- 設定寬度 -->
	canvas.setAttribute("height", "1024"); <!-- 設定高度 -->
	canvas.style.border = "1px dashed red"; <!-- 設定邊框 -->
	document.body.appendChild(canvas); <!-- 加到body之下 -->
	let ctx = canvas.getContext("2d"); <!-- 繪製內容,自此開始繪製 -->
</script>
上述程式碼等同以下的html程式碼。
<canvas id="canvas" width="2048" height="1024" style="border:1px dashed #000000;"></canvas>
有了畫布之後便可以開始繪圖。

繪製線段(Line)


繪製矩形(Rectangle)


繪製圓與弧(Circle & Arc)


橢圓


效果


圖片


Sprites

上述的繪圖方式屬於比較低階(low-level)的API(Application Programming Interface),需要針對每一個圖形的細節做控制。當我們想要製作多個圖形時(尤其是同類的圖形),較好的方式是建立sprite來讓其變成高階(high-level)的語言形式。所謂sprite,原則上就是建立物件來描繪某圖形,之後直接產生該物件的instance便可以繪製。一般來說,sprite可以包含兩個部分,

Sprite


以下以矩形為例,首先建立一個包含矩形各相關參數的class。
class Rectangle{
    constructor(width=32, height=32, fillStyle="gray", strokeStyle="none", lineWidth=0, x=0, y=0, rotation=0, alpha=1, visible=true, scaleX=1, scaleY=1){
        this.width = width;
        this.height = height;
        this.fillStyle = fillStyle;
        this.strokeStyle = strokeStyle;
        this.lineWidth = lineWidth;
        this.x = x;
        this.y = y;
        this.rotation = rotation;
        this.alpha = alpha;
        this.scaleX = scaleX;
        this.scaleY = scaleY;
        this.visible = visible; // 控制是否繪製該圖形
    }

    render(ctx) {
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的矩形,之後再使用translate()來移動
        ctx.rect(-this.width / 2, -this.height / 2, this.width, this.height);
        if (this.strokeStyle !== "none") 
            ctx.stroke();
        ctx.fill();
    };

}//Rectangle
先試試看物件的建構是否成功,加入以下的程式碼:
let canvas = document.createElement("canvas");
canvas.setAttribute("width", '512');
canvas.setAttribute("height", '512');
canvas.style.outline = '1px dashed red';
document.body.appendChild(canvas);
let ctx = canvas.getContext("2d");
let blueBox = new Rectangle(128, 128, "blue", "none", 0, 32, 32);
let redBox = new Rectangle(64, 64, "red", "oive", 0, 100, 100);
blueBox.render(ctx);
redBox.render(ctx);
可以看到在原點(是特意這樣設計,以方便之後的旋轉或扭曲等效果)有兩個方形,其他待完成的事項留到後續再做。為何不直接繪製或是全部留待之後一併完成,而要加上似是半成品的render()函數,是因為每個圖形的繪製方式或參數並不相同,所以在每個物件都設計一個render()函數(內容包含該特定圖形的繪製步驟),然後再在後面設計另一個render()函數(此函數內容應是對於繪製每種圖形都需要之相同步驟)來完成提交。

接下來設計另一個物件,其中包含產生canvas以及繪製所有圖形的render()函數。
class Canvas{
    constructor(width = 360, height = 360, outline = "1px dashed black", backgroundColor = "white"){
        // 建立初始化canvas
        this.canvas = this.initCanvas(width, height, outline, backgroundColor);
        this.ctx = this.canvas.getContext("2d"); // ctx
        this.children = []; // 用以儲存所有的圖形物件
    }

    // 初始化canvas
    initCanvas(width, height, outline, backgroundColor){
        let thecanvas = document.createElement("canvas");
        thecanvas.width = width;
        thecanvas.height = height;
        thecanvas.style.outline = outline;
        thecanvas.style.backgroundColor = backgroundColor;
        document.body.appendChild(thecanvas);
        return thecanvas;
    }

    // 提交所有在children內的圖形物件
    render(){
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.children.forEach(sprite => {
            this.displaySprite(sprite);
        });
    }
    
    // 顯示sprite
    displaySprite(sprite) {
        // 確認visible是否為true
        if (sprite.visible) {
            // 先儲存目前狀態
            this.ctx.save();
            // 將目前圖形(中心為0,0)左上角座標平移至sprite的x,y
            this.ctx.translate(
                sprite.x + sprite.width / 2,
                sprite.y + sprite.height /2
            );
            // 設定sprite的各種效果
            this.ctx.rotate(sprite.rotation);
            this.ctx.globalAlpha = sprite.alpha;
            this.ctx.scale(sprite.scaleX, sprite.scaleY);
            // 呼叫sprite自帶的render()來完成設定
            sprite.render(this.ctx);
            // 回復canvas至之前狀態
            this.ctx.restore();
        }
    }

    addChild(sprite){
        // 將新的sprite加入到 array children
        this.children.push(sprite);
    }
} // Canvas
接下來加上以下的主程式,便可以看繪製出來的結果。
function draw(){
    let canvas = new Canvas();

    let blueBox = new Rectangle(64, 64, "blue", "none", 0, 32, 32);
    blueBox.rotation = 0.3;

    let redBox = new Rectangle(64, 64, "red", "black", 4, 160, 100);
    redBox.scaleY = 2;

    let greenBox = new Rectangle(64, 64, "green", "orange", 4, 50, 150);
    greenBox.rotation = 0.25;
    greenBox.scaleX = 2;

    let goldBox = new Rectangle(10, 10, "gold", "olive", 4, 10, 10);

    canvas.addChild(blueBox, redBox, greenBox, goldBox);
    canvas.render();
}

draw();
也試試看這個函數。
function draw(){
    let canvas = new Canvas(370, 370);
    let reckon = Math.floor(canvas.canvas.width/60);
    let x=10, y=10;
    let randomColor=()=>`rgb(${Math.floor(Math.random()*256)},
                            ${Math.floor(Math.random()*256)},
                            ${Math.floor(Math.random()*256)})`;
    for (let i=0; i<reckon; i++){
        for (let j=0; j<reckon; j++){
            let box = new Rectangle(45, 45, randomColor(), randomColor(), 3, x, y);
            canvas.addChild(box);
            x += 60;
        }
        x = 10;
        y += 60;
    }
    canvas.render();
}

Exercise

Dice Sprite
再練習一次之前的骰子問題,這次使用sprite的方式來設計。
class Sibala{
    constructor(width=100, height=100, fillStyle="white", strokeStyle="black", lineWidth=3, x=0, y=0, visible=true){
        this.width = width;
        this.height = height;
        this.fillStyle = fillStyle;
        this.strokeStyle = strokeStyle;
        this.lineWidth = lineWidth;
        this.x = x;
        this.y = y;
        this.visible = visible;
		this.point = this.roll(); // 骰子點數
		this.radius = 0; // dot的半徑
		this.dotxy = []; // dot 的中心點座標

    }

	roll(){
		this.point = Math.floor(Math.random()*6)+1;
		// console.log("point:", this.point);
		// 根據點數來定義對應的參數
		switch(this.point){
			case 1:
				this.fillStyle = "red";
				this.radius = 18;
				this.dotxy = [[0,0]]; // dot 的中心點座標
				break;
			case 2:
				this.fillStyle = "black";
				this.radius = 6;
				// 以骰子中心在(0,0)來定義dot的位置
				this.dotxy = [[-this.width / 2 + 5*this.radius, -this.height / 2 + 5*this.radius], 
							  [this.width / 2 - 5*this.radius, this.height / 2 - 5*this.radius]];
				break;
			case 3:
				this.fillStyle = "black";
				this.radius = 6;
				// 以骰子中心在(0,0)來定義dot的位置
				this.dotxy = [[-this.width / 2 + 4*this.radius, -this.height / 2 + 4*this.radius], 
							  [this.width / 2 - 4*this.radius, this.height / 2 - 4*this.radius],
							  [0,0]];
				break;
			case 4:
				this.fillStyle = "red";
				this.radius = 6;
				// 以骰子中心在(0,0)來定義dot的位置
				this.dotxy = [[this.width / 4, this.height / 4], 
							  [this.width / 4, -this.height / 4],
							  [-this.width / 4, this.height / 4],
							  [-this.width / 4, -this.height / 4]];
				break;
			case 5:
				this.fillStyle = "black";
				this.radius = 6;
				// 以骰子中心在(0,0)來定義dot的位置
				this.dotxy = [[this.width / 4, this.height / 4], 
							  [this.width / 4, -this.height / 4],
							  [-this.width / 4, this.height / 4],
							  [-this.width / 4, -this.height / 4],
							  [0,0]];
				break;
			case 6:
				this.fillStyle = "black";
				this.radius = 6;
				// 以骰子中心在(0,0)來定義dot的位置
				this.dotxy = [[-this.width / 2 + 5*this.radius, -this.height / 2 + 3*this.radius], 
							  [-this.width / 2 + 5*this.radius, 0],
							  [-this.width / 2 + 5*this.radius, this.height / 2 - 3*this.radius],
							  [this.width / 2 - 5*this.radius, -this.height / 2 + 3*this.radius],
							  [this.width / 2 - 5*this.radius, 0],
							  [this.width / 2 - 5*this.radius, this.height / 2 - 3*this.radius]];
				break;
		} // switch()
	} // roll()


    render(ctx) {
		// 繪製骰子外框
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = "white";
        ctx.beginPath();
        // 繪製中心為(0,0)的矩形,之後再使用translate()來移動
        ctx.rect(-this.width / 2, -this.height / 2, this.width, this.height);
        if (ctx.strokeStyle !== "none") 
            ctx.stroke();
        ctx.fill();

		// 繪製點數
		this.roll();
        ctx.fillStyle = this.fillStyle;
		// 逐顆繪製dot
		for (let dot of this.dotxy){
			ctx.beginPath();
			ctx.arc(dot[0], dot[1], this.radius, 0, Math.PI*2, true);
			ctx.closePath();
			ctx.fill();
		}
    };

}//Sibala

// 此處原則上與之前例子相同
class Canvas{
    constructor(width = 256, height = 256, outline = "1px dashed black", backgroundColor = "white"){
        // 建立初始化canvas
        this.canvas = this.initCanvas(width, height, outline, backgroundColor);
        this.ctx = this.canvas.getContext("2d"); // ctx
        this.children = []; // 用以儲存所有的圖形物件
		this.pointSum = 0;
    }

    // 初始化canvas
    initCanvas(width, height, outline, backgroundColor){
        let thecanvas = document.createElement("canvas");
        thecanvas.width = width;
        thecanvas.height = height;
        thecanvas.style.outline = outline;
        thecanvas.style.backgroundColor = backgroundColor;
        document.body.appendChild(thecanvas);
        return thecanvas;
    }

    // 提交所有在children內的圖形物件
    render(){
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
		this.pointSum = 0;
        this.children.forEach(sprite => {
            this.displaySprite(sprite);
			this.pointSum += sprite.point;
        });
		console.log("total point:", this.pointSum);
    }
    
    // 顯示sprite
    displaySprite(sprite) {
        // 確認visible是否為true
        if (sprite.visible) {
            // 先儲存目前狀態
            this.ctx.save();
            // 將目前圖形(中心為0,0)左上角座標平移至sprite的x,y
            this.ctx.translate(
                sprite.x + sprite.width / 2,
                sprite.y + sprite.height /2
            );
            // 設定sprite的各種效果
            this.ctx.rotate(sprite.rotation);
            this.ctx.globalAlpha = sprite.alpha;
            this.ctx.scale(sprite.scaleX, sprite.scaleY);
            // 呼叫sprite自帶的render()來完成設定
            sprite.render(this.ctx);
            // 回復canvas至之前狀態
            this.ctx.restore();
        }
    }

    addChild(...sprite){
        // 將新的sprite加入到 array children
        for (let s of sprite)
            this.children.push(s);
    }
}//Canvas

function draw(){
	// 建立canvas
    let canvas = new Canvas(500, 500); 
	// 建立骰子物件
    let sib1 = new Sibala(width=100, height=100, fillStyle="white", strokeStyle="black", lineWidth=10, x=20, y=20, visible=true);
	let sib2 = new Sibala(width=100, height=100, fillStyle="white", strokeStyle="black", lineWidth=10, x=140, y=20, visible=true);
	let sib3 = new Sibala(width=100, height=100, fillStyle="white", strokeStyle="black", lineWidth=10, x=260, y=20, visible=true);
	let sib4 = new Sibala(width=100, height=100, fillStyle="white", strokeStyle="black", lineWidth=10, x=20, y=140, visible=true);
	let sib5 = new Sibala(width=100, height=100, fillStyle="white", strokeStyle="black", lineWidth=10, x=140, y=140, visible=true);
	let sib6 = new Sibala(width=100, height=100, fillStyle="white", strokeStyle="black", lineWidth=10, x=260, y=140, visible=true);
	// 物件加入到children
	canvas.addChild(sib1, sib2, sib3, sib4, sib5, sib6);
	// 繪製所有的圖形
    canvas.render();

	// 加上分隔線
	const hr = document.createElement("hr");
	document.body.appendChild(hr);

	// 加上按鈕
	const roll = document.createElement("button");
	roll.innerText = "Roll";
	roll.addEventListener("click", ()=>canvas.render()); // 使用=>函數來呼叫canvas.render()
	document.body.appendChild(roll);
}

draw();
現在想要有幾顆骰子就有幾顆,之後便可以設計骰子相關遊戲,例如Big & Small、骰子梭哈等。

Nested Sprite


所謂nested就是sprite中有sprite,原則上重點是下層的sprite之座標是對應上層的相對座標,當上層的座標改變(移動或旋轉等),下層物件應跟著對應變動。為了達成此目的,須至少於class Rectangle中新增以下變數及方法: 一樣是以矩形為例的class,如下:
class Rectangle{
    constructor(width=32, height=32, fillStyle="gray", strokeStyle="none", lineWidth=0, x=0, y=0, rotation=0, alpha=1, visible=true, scaleX=1, scaleY=1){
        this.width = width;
        this.height = height;
        this.fillStyle = fillStyle;
        this.strokeStyle = strokeStyle;
        this.lineWidth = lineWidth;
        this.x = x;
        this.y = y;
        this.rotation = rotation;
        this.alpha = alpha;
        this.scaleX = scaleX;
        this.scaleY = scaleY;
        this.visible = visible; // 控制是否繪製該圖形
        // new parameters
        this.children = [];
        this.parent = undefined;
        this.layer = 0;
        // 此處並不需要使用,寫下來in case有用的時候
        this.gx = this.getGx(); // this sprite's global x
        this.gy = this.getGy(); // this sprite's global y

    }

    getGx(){
        // 若this有parent
        if(this.parent){
            return this.x + this.parent.gx;
        }else{
            return this.x;
        }
    }
    getGy(){
        // 若this有parent
        if(this.parent){
            return this.y + this.parent.gy;
        }else{
            return this.y;
        }
    }

    addChild(sprite){
        // 如果要加入的sprite有parent,先將其自其parent移除
        if (sprite.parent){
            sprite.parent.removeChild(sprite);
        }
        sprite.parent = this; // 將其parent指向此物件(this)
        this.children.push(sprite); // 將sprite加入陣列children
    }

    removeChild(sprite){
        // 若此sprite的parent是此物件(this)
        if(sprite.parent==this){
            // 刪除sprite
            this.children.splice(this.children.indexOf(sprite), 1);
        }else{
            throw new Error(sprite, "is not a child of", this);
        }
    }

    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的矩形,之後再使用translate()來移動
        ctx.rect(-this.width / 2, -this.height / 2, this.width, this.height);
        if (this.strokeStyle !== "none") 
            ctx.stroke();
        ctx.fill();
    };

}//Rectangle
接著是建立canvas,與之前的主要不同是首先在render時須先將children排序(for layer)。此外是displaySprite(sprite)變成recursive method,當某個sprite要被render時,先檢查看是否其尚有children,若是則先處理其children之render。程式碼如下:
class Canvas{
    constructor(width = 360, height = 360, outline = "1px dashed black", backgroundColor = "white"){
        // 建立初始化canvas
        this.canvas = this.initCanvas(width, height, outline, backgroundColor);
        this.ctx = this.canvas.getContext("2d"); // ctx
        this.children = []; // 用以儲存所有的圖形物件
    }

    // 初始化canvas
    initCanvas(width, height, outline, backgroundColor){
        let thecanvas = document.createElement("canvas");
        thecanvas.width = width;
        thecanvas.height = height;
        thecanvas.style.outline = outline;
        thecanvas.style.backgroundColor = backgroundColor;
        document.body.appendChild(thecanvas);
        return thecanvas;
    }

    // 提交所有在children內的圖形物件
    render(){
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 先根據layer將children內的物件排序
        this.children.sort((a, b)=> a.layer-b.layer);
        this.children.forEach(sprite => {
            this.displaySprite(sprite);
        });
    }
    
    // 顯示sprite
    displaySprite(sprite) {
        // 確認visible是否為true
        if (sprite.visible) {
            // 先儲存目前狀態
            this.ctx.save();
            // 將目前圖形(中心為0,0)左上角座標平移至sprite的x,y
            this.ctx.translate(
                sprite.x + sprite.width / 2,
                sprite.y + sprite.height /2
            );
            // 設定sprite的各種效果
            this.ctx.rotate(sprite.rotation);
            if (sprite.parent)
                this.ctx.globalAlpha = sprite.alpha * sprite.parent.alpha;
            this.ctx.scale(sprite.scaleX, sprite.scaleY);
            // 呼叫sprite自帶的render()來完成設定
            sprite.render(this.ctx);
            
            // render the children of this sprite recursively
            if (sprite.children.length>0){
                 // 根據layer將children內的物件排序
                sprite.children.sort((a,b)=>a.layer-b.layer);

                this.ctx.translate(-sprite.width / 2, -sprite.height / 2);
                sprite.children.forEach(child=>{
                    this.displaySprite(child);
                })
            }

            // 回復canvas至之前狀態
            this.ctx.restore();
        }
    }

    addChild(...sprite){
        // 將新的sprite加入到 array children
        for (let s of sprite)
            this.children.push(s);
    }
}//Canvas
最後將主程式步驟寫成一個函數即可,範例如下:
function draw(){
    let canvas = new Canvas(370, 370);
    let blueBox = new Rectangle(256, 256, "blue", "black", 0, 32, 32);
    let redBox = new Rectangle(64, 64, "red", "black", 4, 10, 10);
    redBox.layer = 2;
    let greenBox = new Rectangle(64, 64, "green", "olive", 4, 10, 10);
    let orangeBox = new Rectangle(64, 64, "orange", "#a90dff", 4, 10, 10);
    let greyBox = new Rectangle(128, 128, "grey", "red", 4, 30, 30);
    greyBox.layer = 3;
    orangeBox.alpha = 0.5;
    let tomatoBox = new Rectangle(64, 64, "tomato", "#a90dff", 4, 60, 60);
    tomatoBox.layer = 2;

    greenBox.addChild(orangeBox);
    greenBox.scaleY = 1.5;
    greenBox.addChild(greyBox);
    //greenBox.alpha = 0.5;
    blueBox.addChild(greenBox);
    blueBox.addChild(redBox);
    blueBox.rotation = 0.25;
    canvas.addChild(blueBox);
    canvas.addChild(tomatoBox);

    canvas.render();
} // draw()

draw();
從結果可以看出sub sprite是跟著其root sprite改變。試著改變rotation、scale、alpha、layer等值觀察其對應反應。

Sprite物件


因為圖形不只是矩形,所以我們可以設計通用的sprite物件,然後各個不同圖形再利用繼承來建立class,然後根據不同的圖形特色設計render()方法,這樣可以讓程式更簡潔。

首先建立class sprite。此處主要定義需要的參數,有些參數視情況看有需要再加,例如陰影(shadow)。
class Sprite{
    constructor(){
        this.x = 0;
        this.y = 0;
        this.width = 0;
        this.height = 0;

        // this.fillStyle = fillStyle;
        // this.strokeStyle = strokeStyle;
        // this.lineWidth = lineWidth;
        
        this.rotation = 0;
        this.alpha = 1;
        this.scaleX = 1;
        this.scaleY = 1;
        this.visible = true; // 控制是否繪製該圖形

        // new parameters
        this.children = [];
        this.parent = undefined;
        this._layer = 0;

        // 設定旋轉軸,0.5表示是sprite的中心點
        this.pivotX = 0.5;
        this.pivotY = 0.5;

        // shadow。說明範例,需要時再設計即可。
        this.shadow = false; //定義預設值為false,需要shadow時再設定為true即可。
        this.shadowColor = "rgba(150, 150, 150, 0.5)";
        this.shadowOffsetX = 3;
        this.shadowOffsetY = 3;
        this.shadowBlur = 3;

    }
    // 設定global x, y的getter & setter
    get gx(){
        // 若this有parent
        if(this.parent){
            return this.x + this.parent.gx;
        }else{
            return this.x;
        }
    }
    get gy(){
        // 若this有parent
        if(this.parent){
            return this.y + this.parent.gy;
        }else{
            return this.y;
        }
    }

    // 設定layer的getter & setter
    get layer(){
        return this._layer;
    }
    set layer(lay){
        this._layer = lay;
        //設定後直接排序
        if(this.parent){
            this.parent.children.sort((a,b)=>a.layer-b.layer);
        }
    }

    // 增加與移除
    addChild(...sprites){
        for (let sprite of sprites){
            
            // 如果要加入的sprite有parent,先將其自其parent移除
            if (sprite.parent){
                sprite.parent.removeChild(sprite);
            }
            sprite.parent = this; // 將其parent指向此物件(this)
            this.children.push(sprite); // 將sprite加入陣列children
        }
    }

    removeChild(sprite){
        // 若此sprite的parent是此物件(this)
        if(sprite.parent==this){
            // 刪除sprite
            this.children.splice(this.children.indexOf(sprite), 1);
        }else{
            throw new Error(sprite, "is not a child of", this);
        }
    }

    // 可能用到的便利函數
    get centerX(){
        return this.x + this.width/2;
    }

    get centerY(){
        return this.y + this.height/2;
    }

} // Sprite
接著便可以繼承自Sprite來建立各式的圖形。例如之前提過的矩形。
class Rectangle extends Sprite{
    constructor(width = 32, height = 32, fillStyle = "gray", strokeStyle = "none", lineWidth = 0, x = 0, y = 0){
        super();
        Object.assign(this, {width, height, fillStyle, strokeStyle, lineWidth, x, y});
    }
    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的矩形,之後再使用translate()來移動
        ctx.rect(-this.width / 2, -this.height / 2, this.width, this.height);
        if (this.strokeStyle !== "none") 
            ctx.stroke();
        ctx.fill();
    };
} // Rectangle
圓形:
class Circle extends Sprite{
    constructor(x=0, y=0, radius=0, ){
        super();
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.fillStyle = "olive";
        this.lineWidth = 3;
    }
    
    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的圓形,之後再使用translate()來移動
        ctx.arc(this.radius + (-this.radius*2*this.pivotX), // 0
                this.radius + (-this.radius*2*this.pivotY), // 0
                this.radius, 0, 2*Math.PI, false);
        if (this.strokeStyle !== "none") 
            ctx.stroke();
        ctx.fill();
    };
} // Circle
橢圓形:
class Ellipse extends Sprite{
    constructor(x=0, y=0, radiusX=0, radiusY=0){
        super();
        this.x = x;
        this.y = y;
        this.radiusX = radiusX;
        this.radiusY = radiusY;
        //this.rotation = rotation;
    }
    
    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的橢圓形,之後再使用translate()來移動
        ctx.ellipse(this.radiusX + (-this.radiusX*2*this.pivotX), 
                    this.radiusY + (-this.radiusY*2*this.pivotY), 
                    this.radiusX, this.radiusY, 0, 0, Math.PI*2, false);
        if (this.strokeStyle !== "none")    
            ctx.stroke();
        ctx.fill();
    };
} // Ellipse
線段:
class Line extends Sprite{
    constructor(x1=0, y1=0, x2=0, y2=0, lineWidth=0, strokeStyle='none'){
        super();
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.lineWidth = lineWidth;
        this.strokeStyle = strokeStyle;
        this.lineJoin = "round";
    }
    
    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.lineJoin = this.lineJoin;
        ctx.beginPath();
        // 繪製從(x1, y1)到(x2, y2)的線段
        ctx.moveTo(this.x1, this.y1);
        ctx.lineTo(this.x2, this.y2);
        if (this.strokeStyle !== "none")    
            ctx.stroke();
    };
} // Line
文字:
class Text extends Sprite{
    constructor(x=0, y=0, content='', font='12px sans-serif', fillStyle='none'){
        super();
        this.x = x;
        this.y = y;
        this.content = content;
        this.font = font;
        this.fillStyle = fillStyle;
        this.textBaseline = "top";
        this.strokeText = "none";
    }
    
    render(ctx){
        ctx.font = this.font;
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        if (this.width == 0)
            this.width = ctx.measureText(this.content).width;
        if (this.height == 0)
            this.height = ctx.measureText("M").width;

        ctx.textBaseline = this.textBaseline;
        ctx.translate(
            -this.width * this.pivotX,
            -this.height * this.pivotY
        );
        ctx.fillText(this.content, 0, 0);
        if (this.strokeText !== "none")    
            ctx.strokeText();
    };
} // Text
骰子:是一個矩形跟圓形的組合,因為是矩形內含圓形,以矩形為容器,所以將class繼承自Rectangel。
class Dice extends Rectangle{
    constructor(x=0, y=0, width=10, height=10){
        super();
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;    
        this.lineWidth = 3;
        this.strokeStyle = "black";
        this.fillStyle = "white";
        this.point = (Math.floor(Math.random()*987654321))%6+1;
        //console.log(this.point);
        if(this.point==1){
            let n1 = new Circle(x=this.width/2, y=this.height/2, circleRadius=6);
            n1.fillStyle = "red";
            n1.strokeStyle = "none";

            this.children.push(n1);
        }else if(this.point==2){
            let n1 = new Circle(x=this.width/4, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width*3/4, y=this.height*3/4, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";

            this.children.push(n1, n2);
        }else if(this.point==3){
            let n1 = new Circle(x=this.width/4, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width*3/4, y=this.height*3/4, circleRadius=3);
            let n3 = new Circle(x=this.width/2, y=this.height/2, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";
            n3.fillStyle = "black";
            n3.strokeStyle = "none";

            this.children.push(n1, n2, n3);
        }else if(this.point==4){
            let n1 = new Circle(x=this.width*7/24, y=this.height*17/24, circleRadius=3);
            let n2 = new Circle(x=this.width*17/24, y=this.height*17/24, circleRadius=3);
            let n3 = new Circle(x=this.width*7/24, y=this.height*7/24, circleRadius=3);
            let n4 = new Circle(x=this.width*17/24, y=this.height*7/24, circleRadius=3);
            n1.fillStyle = "red";
            n1.strokeStyle = "none";
            n2.fillStyle = "red";
            n2.strokeStyle = "none";
            n3.fillStyle = "red";
            n3.strokeStyle = "none";
            n4.fillStyle = "red";
            n4.strokeStyle = "none";

            this.children.push(n1, n2, n3, n4);
        }else if(this.point==5){
            let n1 = new Circle(x=this.width/4, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width*3/4, y=this.height*3/4, circleRadius=3);
            let n3 = new Circle(x=this.width/4, y=this.height*3/4, circleRadius=3);
            let n4 = new Circle(x=this.width*3/4, y=this.height/4, circleRadius=3);
            let n5 = new Circle(x=this.width/2, y=this.height/2, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";
            n3.fillStyle = "black";
            n3.strokeStyle = "none";
            n4.fillStyle = "black";
            n4.strokeStyle = "none";
            n5.fillStyle = "black";
            n5.strokeStyle = "none";

            this.children.push(n1, n2, n3, n4, n5);
        }else if(this.point==6){
            let n1 = new Circle(x=this.width/3, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width/3, y=this.height/2, circleRadius=3);
            let n3 = new Circle(x=this.width/3, y=this.height*3/4, circleRadius=3);
            let n4 = new Circle(x=this.width*2/3, y=this.height/4, circleRadius=3);
            let n5 = new Circle(x=this.width*2/3, y=this.height/2, circleRadius=3);
            let n6 = new Circle(x=this.width*2/3, y=this.height*3/4, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";
            n3.fillStyle = "black";
            n3.strokeStyle = "none";
            n4.fillStyle = "black";
            n4.strokeStyle = "none";
            n5.fillStyle = "black";
            n5.strokeStyle = "none";
            n6.fillStyle = "black";
            n6.strokeStyle = "none";

            this.children.push(n1, n2, n3, n4, n5, n6);
        }
    }
} // Dice
Canvas: 跟之前類似,這個class完成canvas建立以及包含全域render()方法。
class Canvas{
    constructor(width = 360, height = 360, outline = "1px dashed black", backgroundColor = "white"){
        // 建立初始化canvas
        this.canvas = this.initCanvas(width, height, outline, backgroundColor);
        this.ctx = this.canvas.getContext("2d"); // ctx
        this.children = []; // 用以儲存所有的圖形物件
    }

    // 初始化canvas
    initCanvas(width, height, outline, backgroundColor){
        let thecanvas = document.createElement("canvas");
        thecanvas.width = width;
        thecanvas.height = height;
        thecanvas.style.outline = outline;
        thecanvas.style.backgroundColor = backgroundColor;
        document.body.appendChild(thecanvas);
        return thecanvas;
    }

    // 提交所有在children內的圖形物件
    render(){
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 先根據layer將children內的物件排序

        this.children.forEach(sprite => {
            this.displaySprite(sprite);
        });
    }
    
    // 顯示sprite
    displaySprite(sprite) {
        // 確認visible是否為true且位於canvas範圍內
        if (sprite.visible 
            && sprite.gx < this.canvas.width + sprite.width
            && sprite.gx + sprite.width >= -sprite.width
            && sprite.gy < this.canvas.height + sprite.height
            && sprite.gy + sprite.height >= -sprite.height) {

            // 先儲存目前狀態
            this.ctx.save();

            // 將目前圖形(中心為0,0)左上角座標平移至sprite的x,y
            this.ctx.translate(
                sprite.x + (sprite.width * sprite.pivotX),
                sprite.y + (sprite.height * sprite.pivotY)
            );
            
            // 設定sprite的各種效果
            this.ctx.rotate(sprite.rotation);
            this.ctx.scale(sprite.scaleX, sprite.scaleY);
            if(sprite.parent)
                this.ctx.globalAlpha = sprite.alpha * sprite.parent.alpha;

            // 顯示shadow
            if(sprite.shadow){
                this.ctx.shadowColor = sprite.shadowColor;
                this.ctx.shadowOffsetX = sprite.shadowOffsetX;
                this.ctx.shadowOffsetY = sprite.shadowOffsetY;
                this.ctx.shadowBlur = sprite.shadowBlur;
            }

            // 呼叫sprite自帶的render()來完成設定
            if(sprite.render)
                sprite.render(this.ctx);
            
            // render the children of this sprite recursively
            if (sprite.children && sprite.children.length > 0){
                 // 根據layer將children內的物件排序
                this.ctx.translate(-sprite.width * sprite.pivotX, -sprite.height * sprite.pivotY);
                sprite.children.forEach(child=>{
                    this.displaySprite(child);
                })
            }

            // 回復canvas至之前狀態
            this.ctx.restore();
        }
    }

    addChild(...sprite){
        // 將新的sprite加入到 array children
        for (let s of sprite)
            this.children.push(s);
        
    }
}//Canvas
主程式(main): 在繪製時可以建立一個矩形做為畫布(root),所有其他圖形都加入到root的children,如此可以根據layer排序來決定圖層上下關係。
function main(){
    let canvas = new Canvas(1280, 1280);
    let root = new Rectangle(canvas.width, canvas.height, "white", "white", 0, 0, 0); // 做為畫布

    let blueBox = new Rectangle(256, 256, "blue", "black", 0, 0, 40);
    let redBox = new Rectangle(64, 64, "red", "black", 4, 10, 10);
    
    let greenBox = new Rectangle(64, 64, "green", "olive", 4, 50, 10);
    let orangeBox = new Rectangle(64, 64, "orange", "#a90dff", 4, 30, 50);
    let greyBox = new Rectangle(128, 128, "grey", "red", 4, 200, 30);
    
    orangeBox.alpha = 0.5;
    let tomatoBox = new Rectangle(64, 64, "tomato", "#a90dff", 4, 60, 60);
    tomatoBox.shadow = true; // 顯示陰影
    tomatoBox.rotaton = 0.2;
    
    greenBox.addChild(orangeBox, greyBox);
    greenBox.scaleY = 1.5;;
    greenBox.alpha = 0.5;
    blueBox.addChild(greenBox, redBox);
    //blueBox.rotation = 0.25;

    let cir1 = new Circle(x=30, y=30, circleRadius=20);
    cir1.strokeStyle = "red";
    
    let ell1 = new Ellipse(x=150, y=80, radiusX=50, radiusY=80);
    ell1.strokeStyle = "green";
    ell1.fillStyle = "gold";
    ell1.lineWidth = 5;
    ell1.rotation = Math.PI/2;

    let d1 = new Dice(x=550, y=50, width=50, height=50);
    let d2 = new Dice(x=610, y=50, width=50, height=50);
    let d3 = new Dice(x=670, y=50, width=50, height=50);
    let d4 = new Dice(x=550, y=110, width=50, height=50);
    let d5 = new Dice(x=610, y=110, width=50, height=50);
    let d6 = new Dice(x=670, y=110, width=50, height=50);

    let line1 = new Line(x1=500, y1=10, x2=500, y2=110, lineWidth=3, strokeStyle='blue');
    let line2 = new Line(x1=500, y1=110, x2=400, y2=70, lineWidth=3, strokeStyle='green');
    let line3 = new Line(x1=400, y1=70, x2=500, y2=10, lineWidth=3, strokeStyle='red');

    let text1 = new Text(x=450, y=200, content='Hi, there.', font='12px sans-serif', fillStyle='blue');
    
    // 使用root做為畫布(可以編輯root下層的layer)
    root.addChild(blueBox, tomatoBox, cir1, ell1);
    root.addChild(d1, d2, d3, d4, d5, d6);
    root.addChild(line1, line2, line3);
    root.addChild(text1);
    canvas.addChild(root);

    // 使用canvas做為畫布
    // canvas.addChild(blueBox, tomatoBox, cir1, ell1);
    // canvas.addChild(d1, d2, d3, d4, d5, d6);
    // canvas.addChild(line1, line2, line3);

    // 需先addChild()然後再設定layer。重疊部分的物件都得設定layer
    greenBox.layer = 2;
    redBox.layer = 1;
    greyBox.layer = 3;
    cir1.layer = 1;
    tomatoBox.layer = 5;
    blueBox.layer = 2;
    ell1.layer = 5;
    
    canvas.render();
} // main()

main();
若直接讓canvas做為畫布也可以,此處無設計畫布層的layer關係,可以將class Canvas中的render()修改如下(就是讓其children先行排序)即可:
render(){
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    // 先根據layer將children內的物件排序
    this.children.sort((a,b)=>a.layer-b.layer);
    this.children.forEach(sprite => {
        this.displaySprite(sprite);
    });
}
此時可以直接讓canvas做為畫布。 再加上繪製圖片的物件,假設傳入的都是Image,算是一個簡化版,都使用裁剪(blitting)的方式,若是整張圖片則裁剪全部範圍即可。
class Picture extends Sprite{
    constructor(x=0, y=0, source='none', sourceX=0, sourceY=0, sourceWidth=0, sourceHeight=0){
        // 假定傳入的source都是Image
        super();
        this.x = x; // 圖片顯示位置x
        this.y = y; // 圖片顯示位置y
        this.source = source;
        this.width = this.source.width; // 圖片顯示寬度
        this.height = this.source.height; // 圖片顯示高度

        this.sourceX = sourceX; // 來源圖片x
        this.sourceY = sourceY; // 來源圖片y
        
        this.sourceWidth = sourceWidth; // 來源圖片寬
        this.sourceHeight = sourceHeight; // 來源圖片高
        //console.log(`${this.sourceWidth}, ${this.sourceHeight}, ${this.sourceX}, ${this.sourceY}`);
    }//constructor

    render(ctx){
        this.source.addEventListener("load", 
            ()=>{
                ctx.drawImage(
                    this.source, 
                    this.sourceX, this.sourceY,
                    this.sourceWidth, this.sourceHeight,
                    this.x, this.y,
                    this.width, this.height);
            }
        , false);
    };
} // Picture
修改原來的主程式如下:
function main(){
    let canvas = new Canvas(1280, 1280);

    let blueBox = new Rectangle(256, 256, "blue", "black", 0, 0, 40);
    let redBox = new Rectangle(64, 64, "red", "black", 4, 10, 10);
    
    let greenBox = new Rectangle(64, 64, "green", "olive", 4, 50, 10);
    let orangeBox = new Rectangle(64, 64, "orange", "#a90dff", 4, 30, 50);
    let greyBox = new Rectangle(128, 128, "grey", "red", 4, 200, 30);
    
    orangeBox.alpha = 0.5;
    let tomatoBox = new Rectangle(64, 64, "tomato", "#a90dff", 4, 60, 60);
    tomatoBox.shadow = true; // 顯示陰影
    tomatoBox.rotaton = 0.2;
    
    greenBox.addChild(orangeBox, greyBox);
    greenBox.scaleY = 1.5;;
    greenBox.alpha = 0.5;
    blueBox.addChild(greenBox, redBox);
    //blueBox.rotation = 0.25;

    let cir1 = new Circle(x=30, y=30, circleRadius=20);
    cir1.strokeStyle = "red";
    
    let ell1 = new Ellipse(x=150, y=80, radiusX=50, radiusY=80);
    ell1.strokeStyle = "green";
    ell1.fillStyle = "gold";
    ell1.lineWidth = 5;
    ell1.rotation = Math.PI/2;

    let d1 = new Dice(x=550, y=50, width=50, height=50);
    let d2 = new Dice(x=610, y=50, width=50, height=50);
    let d3 = new Dice(x=670, y=50, width=50, height=50);
    let d4 = new Dice(x=550, y=110, width=50, height=50);
    let d5 = new Dice(x=610, y=110, width=50, height=50);
    let d6 = new Dice(x=670, y=110, width=50, height=50);

    let line1 = new Line(x1=500, y1=10, x2=500, y2=110, lineWidth=3, strokeStyle='blue');
    let line2 = new Line(x1=500, y1=110, x2=400, y2=70, lineWidth=3, strokeStyle='green');
    let line3 = new Line(x1=400, y1=70, x2=500, y2=10, lineWidth=3, strokeStyle='red');

    let text1 = new Text(x=450, y=200, content='Hi, there.', font='12px sans-serif', fillStyle='blue');
    // 整張圖片
    let pic = new Image();
    pic.src = "landscape1.jpg";
    let img1 = new Picture(x=500, y=200, source=pic, sourceX=0, sourceY=0, sourceWidth=pic.width, sourceHeight=pic.height);
    // 裁剪部分圖片
    let pic2 = new Image();
    pic2.src = "animeCharacters3.jpg";
    let img2 = new Picture(x=0, y=450, source=pic2, sourceX=100, sourceY=0, sourceWidth=145, sourceHeight=300);
    img2.width = 145;
    img2.height = 300;

    // 使用canvas做為畫布
    canvas.addChild(blueBox, tomatoBox, cir1, ell1);
    canvas.addChild(d1, d2, d3, d4, d5, d6);
    canvas.addChild(line1, line2, line3, text1, img1, img2);

    // 需先addChild()然後再設定layer。重疊部分的物件都得設定layer
    greenBox.layer = 2;
    redBox.layer = 1;
    greyBox.layer = 3;
    cir1.layer = 1;
    tomatoBox.layer = 5;
    blueBox.layer = 2;
    ell1.layer = 5;
    img1.layer = 1;
    img2.layer = 1;
    
    canvas.render();
} // main()

main();

Animation

繪製的圖形可以靠著不斷的繪製來形成移動的效果。

Movement


為了控制移動的物體的速度跟方向,首先在之前的class Sprite內加上vx與vy兩個變數(沒加也無妨的樣子,但是在之後要設定其值,然後會自動在class內補上),做為x向與y向的速度,預設值可以為0。

欲讓物體移動,需呼叫requestAnimationFrame(loopFunction);方法,此方法為animation的引擎。可以在通常情況下每16毫秒(milliseconds)呼叫loopfunction一次(約莫是每秒60次)。每一次稱為一個frame。
function main(){
    let canvas = new Canvas(800, 300);
    let ball = new Circle(x=10, y=10, circleRadius=10);
    ball.strokeStyle = "none";
    ball.fillStyle = "red";
    ball.vx = 3;
    ball.vy = 2;
    loop();
    function loop(){
        requestAnimationFrame(loop);
        ball.x += ball.vx;
        ball.y += ball.vy;
        // canvas跟瀏覽器之間有10px的間隙
        let leftBoundary = 10;
        let rightBoundary = canvas.canvas.width+10; // 取得寬高有點麻煩,也可以在class Canvas內建立width, height變數
        let topBoundary = 10;
        let bottomBoundary = canvas.canvas.height+10;
        if(ball.x<leftBoundary || ball.x+circleRadius*2>rightBoundary)
            ball.vx = -ball.vx;
        if(ball.y<topBoundary || ball.y+circleRadius*2>bottomBoundary)
            ball.vy = -ball.vy;
        
        
        canvas.addChild(ball);
        canvas.render();
    }
    
} // main()

main();
可以加上加速跟摩擦力來改變速度。當球碰觸到邊的時候,改變其加速度及摩擦力。
function main(){
    let canvas = new Canvas(800, 300);
    let ball = new Circle(x=10, y=10, circleRadius=10);
    ball.strokeStyle = "none";
    ball.fillStyle = "red";
    ball.vx = 3;
    ball.vy = 2;
    ball.accX = 0.2; // acceleration
    ball.accY = 0.2;
    ball.friX = 1; // friction
    ball.friY = 1;

    loop();
    function loop(){
        requestAnimationFrame(loop);
        // 加上加速度跟摩擦力
        ball.vx += ball.accX;
        ball.vy += ball.accY;
        ball.vx *= ball.friX;
        ball.vy *= ball.friY;

        ball.x += ball.vx;
        ball.y += ball.vy;
        
        // canvas跟瀏覽器之間有10px的間隙
        let leftBoundary = 10;
        let rightBoundary = canvas.canvas.width+10; // 取得寬高有點麻煩,也可以在class Canvas內建立width, height變數
        let topBoundary = 10;
        let bottomBoundary = canvas.canvas.height+10;
        if(ball.x<leftBoundary || ball.x+circleRadius*2>rightBoundary){
            // 改變加速度跟摩擦力
            ball.friX -= 0.01;
            ball.friY -= 0.01;
            ball.accX = 0;
            ball.accY = 0;
            ball.vx = -ball.vx;
        }
        if(ball.y<topBoundary || ball.y+circleRadius*2>bottomBoundary){
            // 改變加速度跟摩擦力
            ball.friX -= 0.01;
            ball.friY -= 0.01;
            ball.accX = 0;
            ball.accY = 0;
            ball.vy = -ball.vy;
        }
        
        canvas.addChild(ball);
        canvas.render();
    }
    
} // main()
模擬重力。
function main(){
    let canvas = new Canvas(800, 300);
    let ball = new Circle(x=10, y=10, circleRadius=10);
    ball.strokeStyle = "none";
    ball.fillStyle = "red";
    ball.vx = 10;
    ball.vy = 8;
    ball.accX = 0; // acceleration
    ball.accY = 0.3;
    ball.friX = 1; // friction
    ball.friY = 1;
    ball.mass = 1.3; // 質量,控制球的減速度,質量越大減少越多

    loop();
    function loop(){
        requestAnimationFrame(loop);
        // 加上加速度跟摩擦力
        ball.vx += ball.accX;
        ball.vy += ball.accY;
        ball.vx *= ball.friX;
        ball.vy *= ball.friY;

        ball.x += ball.vx;
        ball.y += ball.vy;
        
        // canvas跟瀏覽器之間有10px的間隙
        let leftBoundary = 10;
        let rightBoundary = canvas.canvas.width+10;
        let topBoundary = 10;
        let bottomBoundary = canvas.canvas.height+10;
        if(ball.x<leftBoundary){ // 碰觸左壁
            ball.x = leftBoundary;
            ball.vx = -ball.vx/ball.mass;
        }
            
        if(ball.x+circleRadius*2>rightBoundary){ // 碰觸右壁
            // 改變加速度跟摩擦力
            ball.x = rightBoundary - circleRadius*2;
            ball.vx = -ball.vx/ball.mass;
        }

        if(ball.y<topBoundary){ // 碰觸上壁
            ball.y = topBoundary;
            ball.vy = -ball.vy/ball.mass;
        }

        if(ball.y+circleRadius*2>bottomBoundary){ // 碰觸地面
            // 改變加速度跟摩擦力
            ball.y = bottomBoundary - circleRadius*2;
            ball.vy = -ball.vy/ball.mass;
            ball.friX = 0.96;
            //ball.friY = 0.99;
        }else{
            ball.friX = 1;
            ball.friY = 1;
        }
        
        canvas.addChild(ball);
        canvas.render();
    }
    
} // main()
控制fps(frams per second),可以讓速度不快過給定值,可以讓顯示在不同機器中顯得較穩定。順便將loop內容做整理,另外在一個函數內設計邏輯。
function main(){
    let canvas = new Canvas(1280, 600);
    let ball = new Circle(x=10, y=10, circleRadius=10);
    ball.strokeStyle = "none";
    ball.fillStyle = "red";
    ball.vx = 10;
    ball.vy = 8;
    ball.accX = 0; // acceleration
    ball.accY = 0.3;
    ball.friX = 1; // friction
    ball.friY = 1;
    ball.mass = 1.3; // 質量,控制球的減速度,質量越大減少越多
    

    function logic(){
        // 加上加速度跟摩擦力
        ball.vx += ball.accX;
        ball.vy += ball.accY;
        ball.vx *= ball.friX;
        ball.vy *= ball.friY;

        ball.x += ball.vx;
        ball.y += ball.vy;
        
        // canvas跟瀏覽器之間有10px的間隙
        let leftBoundary = 10;
        let rightBoundary = canvas.canvas.width+10;
        let topBoundary = 10;
        let bottomBoundary = canvas.canvas.height+10;
        if(ball.x<leftBoundary){ // 碰觸左壁
            ball.x = leftBoundary;
            ball.vx = -ball.vx/ball.mass;
        }
            
        if(ball.x+circleRadius*2>rightBoundary){ // 碰觸右壁
            // 改變加速度跟摩擦力
            ball.x = rightBoundary - circleRadius*2;
            ball.vx = -ball.vx/ball.mass;
        }

        if(ball.y<topBoundary){ // 碰觸上壁
            ball.y = topBoundary;
            ball.vy = -ball.vy/ball.mass;
        }

        if(ball.y+circleRadius*2>bottomBoundary){ // 碰觸地面
            // 改變加速度跟摩擦力
            ball.y = bottomBoundary - circleRadius*2;
            ball.vy = -ball.vy/ball.mass;
            ball.friX = 0.96;
            //ball.friY = 0.99;
        }else{
            ball.friX = 1;
            ball.friY = 1;
        }
        
        canvas.addChild(ball);
    } // logic

    let fps = 500; // frames per second
    let start = 0;
    let duration = 1000/fps; // frame間的時間gap

    loop();
    function loop(timestamp){
        requestAnimationFrame(loop);
        if(timestamp>=start) {
            logic();
            canvas.render();
            start = timestamp + duration;
        }
    } // loop
    
} // main()

Interactive

此處介紹如何與sprite互動。

Example Code


以下先列出完整的code。
class Sprite{
    constructor(){
        this.x = 0;
        this.y = 0;
        this.width = 0;
        this.height = 0;

        this.fillStyle = 'black';
        this.strokeStyle = 'black';
        this.lineWidth = 0;
        
        this.rotation = 0;
        this.alpha = 1;
        this.scaleX = 1;
        this.scaleY = 1;
        this.visible = true; // 控制是否繪製該圖形

        // new parameters
        this.children = [];
        this.parent = undefined;
        this._layer = 0;

        // 設定旋轉軸,0.5表示是sprite的中心點
        this.pivotX = 0.5;
        this.pivotY = 0.5;

        // shadow。說明範例,需要時再設計即可。
        this.shadow = false; //定義預設值為false,需要shadow時再設定為true即可。
        this.shadowColor = "rgba(150, 150, 150, 0.5)";
        this.shadowOffsetX = 3;
        this.shadowOffsetY = 3;
        this.shadowBlur = 3;
        

    }
    // 設定global x, y的getter & setter
    get gx(){
        // 若this有parent
        if(this.parent){
            return this.x + this.parent.gx;
        }else{
            return this.x;
        }
    }
    get gy(){
        // 若this有parent
        if(this.parent){
            return this.y + this.parent.gy;
        }else{
            return this.y;
        }
    }

    // 設定layer的getter & setter
    get layer(){
        return this._layer;
    }

    set layer(lay){
        this._layer = lay;
        //設定後直接排序
        if(this.parent){
            this.parent.children.sort((a,b)=>a.layer-b.layer);
        }
    }

    // 增加與移除
    addChild(...sprites){
        for (let sprite of sprites){
            
            // 如果要加入的sprite有parent,先將其自其parent移除
            if (sprite.parent){
                sprite.parent.removeChild(sprite);
            }
            sprite.parent = this; // 將其parent指向此物件(this)
            this.children.push(sprite); // 將sprite加入陣列children
        }
    }

    removeChild(sprite){
        // 若此sprite的parent是此物件(this)
        if(sprite.parent==this){
            // 刪除sprite
            this.children.splice(this.children.indexOf(sprite), 1);
        }else{
            throw new Error(sprite, "is not a child of", this);
        }
    }

    // 可能用到的便利函數
    get centerX(){
        return this.x + this.width/2;
    }

    get centerY(){
        return this.y + this.height/2;
    }

} // Sprite

class Rectangle extends Sprite{
    constructor(width = 32, height = 32, fillStyle = "gray", strokeStyle = "none", lineWidth = 0, x = 0, y = 0){
        super();
        Object.assign(this, {width, height, fillStyle, strokeStyle, lineWidth, x, y});
    }
    
    isInside(px, py){
        if(px>=this.x && py>=this.y && px<=this.x+this.width && py<=this.y+this.height)
            return true;
        else
            return false;
    }//isInside

    toString(){
        return "rectangle";
    }

    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的矩形,之後再使用translate()來移動
        ctx.rect(-this.width / 2, -this.height / 2, this.width, this.height);
        if (this.strokeStyle !== "none") 
            ctx.stroke();
        ctx.fill();
    };
} // Rectangle

class Circle extends Sprite{
    constructor(x=0, y=0, radius=0){
        super();
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.fillStyle = "olive";
        this.lineWidth = 3;
    }
    
    isInside(px, py){
        let dist = ((px-this.x)**2+(py-this.y)**2)**0.5;
        if(dist < this.radius)
            return true;
        else
            return false;
    }//isInside

    toString(){
        return "circle";
    }

    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的圓形,之後再使用translate()來移動
        ctx.arc(this.radius + (-this.radius*2*this.pivotX), // 0
                this.radius + (-this.radius*2*this.pivotY), // 0
                this.radius, 0, 2*Math.PI, false);
        if (this.strokeStyle !== "none") 
            ctx.stroke();
        ctx.fill();
    };
} // Circle

class Ellipse extends Sprite{
    constructor(x=0, y=0, radiusX=0, radiusY=0){
        super();
        this.x = x;
        this.y = y;
        this.radiusX = radiusX;
        this.radiusY = radiusY;
        //this.rotation = rotation;
    }
    
    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        ctx.beginPath();
        // 繪製中心為(0,0)的橢圓形,之後再使用translate()來移動
        ctx.ellipse(this.radiusX + (-this.radiusX*2*this.pivotX), 
                    this.radiusY + (-this.radiusY*2*this.pivotY), 
                    this.radiusX, this.radiusY, 0, 0, Math.PI*2, false);
        if (this.strokeStyle !== "none")    
            ctx.stroke();
        ctx.fill();
    };
} // Ellipse

class Line extends Sprite{
    constructor(x1=0, y1=0, x2=0, y2=0, lineWidth=0, strokeStyle='none'){
        super();
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.lineWidth = lineWidth;
        this.strokeStyle = strokeStyle;
        this.lineJoin = "round";
    }
    
    render(ctx){
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.lineJoin = this.lineJoin;
        ctx.beginPath();
        // 繪製從(x1, y1)到(x2, y2)的線段
        ctx.moveTo(this.x1, this.y1);
        ctx.lineTo(this.x2, this.y2);
        if (this.strokeStyle !== "none")    
            ctx.stroke();
    };
} // Line

class Text extends Sprite{
    constructor(x=0, y=0, content='', font='12px sans-serif', fillStyle='none'){
        super();
        this.x = x;
        this.y = y;
        this.content = content;
        this.font = font;
        this.fillStyle = fillStyle;
        this.textBaseline = "top";
        this.strokeText = "none";
    }
    
    render(ctx){
        ctx.font = this.font;
        ctx.strokeStyle = this.strokeStyle;
        ctx.lineWidth = this.lineWidth;
        ctx.fillStyle = this.fillStyle;
        if (this.width == 0)
            this.width = ctx.measureText(this.content).width;
        if (this.height == 0)
            this.height = ctx.measureText("M").width;

        ctx.textBaseline = this.textBaseline;
        ctx.translate(
            -this.width * this.pivotX,
            -this.height * this.pivotY
        );
        ctx.fillText(this.content, 0, 0);
        if (this.strokeText !== "none")    
            ctx.strokeText();
    };
} // Text


class Picture extends Sprite{
    constructor(x=0, y=0, source='none', sourceX=0, sourceY=0, sourceWidth=0, sourceHeight=0){
        // 假定傳入的source都是Image
        super();
        this.x = x; // 圖片顯示位置x
        this.y = y; // 圖片顯示位置y
        this.source = source;
        this.width = this.source.width; // 圖片顯示寬度
        this.height = this.source.height; // 圖片顯示高度

        this.sourceX = sourceX; // 來源圖片x
        this.sourceY = sourceY; // 來源圖片y
        
        this.sourceWidth = sourceWidth; // 來源圖片寬
        this.sourceHeight = sourceHeight; // 來源圖片高
        //console.log(`${this.sourceWidth}, ${this.sourceHeight}, ${this.sourceX}, ${this.sourceY}`);
    }//constructor

    render(ctx){
        this.source.addEventListener("load", 
            ()=>{
                ctx.drawImage(
                    this.source, 
                    this.sourceX, this.sourceY,
                    this.sourceWidth, this.sourceHeight,
                    this.x, this.y,
                    this.width, this.height);
            }
        , false);
    };
} // Picture

class Dice extends Rectangle{
    constructor(x=0, y=0, width=10, height=10){
        super();
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;    
        this.lineWidth = 3;
        this.strokeStyle = "black";
        this.fillStyle = "white";
        this.point = (Math.floor(Math.random()*987654321))%6+1;
        //console.log(this.point);
        if(this.point==1){
            let n1 = new Circle(x=this.width/2, y=this.height/2, circleRadius=6);
            n1.fillStyle = "red";
            n1.strokeStyle = "none";

            this.children.push(n1);
        }else if(this.point==2){
            let n1 = new Circle(x=this.width/4, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width*3/4, y=this.height*3/4, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";

            this.children.push(n1, n2);
        }else if(this.point==3){
            let n1 = new Circle(x=this.width/4, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width*3/4, y=this.height*3/4, circleRadius=3);
            let n3 = new Circle(x=this.width/2, y=this.height/2, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";
            n3.fillStyle = "black";
            n3.strokeStyle = "none";

            this.children.push(n1, n2, n3);
        }else if(this.point==4){
            let n1 = new Circle(x=this.width*7/24, y=this.height*17/24, circleRadius=3);
            let n2 = new Circle(x=this.width*17/24, y=this.height*17/24, circleRadius=3);
            let n3 = new Circle(x=this.width*7/24, y=this.height*7/24, circleRadius=3);
            let n4 = new Circle(x=this.width*17/24, y=this.height*7/24, circleRadius=3);
            n1.fillStyle = "red";
            n1.strokeStyle = "none";
            n2.fillStyle = "red";
            n2.strokeStyle = "none";
            n3.fillStyle = "red";
            n3.strokeStyle = "none";
            n4.fillStyle = "red";
            n4.strokeStyle = "none";

            this.children.push(n1, n2, n3, n4);
        }else if(this.point==5){
            let n1 = new Circle(x=this.width/4, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width*3/4, y=this.height*3/4, circleRadius=3);
            let n3 = new Circle(x=this.width/4, y=this.height*3/4, circleRadius=3);
            let n4 = new Circle(x=this.width*3/4, y=this.height/4, circleRadius=3);
            let n5 = new Circle(x=this.width/2, y=this.height/2, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";
            n3.fillStyle = "black";
            n3.strokeStyle = "none";
            n4.fillStyle = "black";
            n4.strokeStyle = "none";
            n5.fillStyle = "black";
            n5.strokeStyle = "none";

            this.children.push(n1, n2, n3, n4, n5);
        }else if(this.point==6){
            let n1 = new Circle(x=this.width/3, y=this.height/4, circleRadius=3);
            let n2 = new Circle(x=this.width/3, y=this.height/2, circleRadius=3);
            let n3 = new Circle(x=this.width/3, y=this.height*3/4, circleRadius=3);
            let n4 = new Circle(x=this.width*2/3, y=this.height/4, circleRadius=3);
            let n5 = new Circle(x=this.width*2/3, y=this.height/2, circleRadius=3);
            let n6 = new Circle(x=this.width*2/3, y=this.height*3/4, circleRadius=3);
            n1.fillStyle = "black";
            n1.strokeStyle = "none";
            n2.fillStyle = "black";
            n2.strokeStyle = "none";
            n3.fillStyle = "black";
            n3.strokeStyle = "none";
            n4.fillStyle = "black";
            n4.strokeStyle = "none";
            n5.fillStyle = "black";
            n5.strokeStyle = "none";
            n6.fillStyle = "black";
            n6.strokeStyle = "none";

            this.children.push(n1, n2, n3, n4, n5, n6);
        }
    }
} // Dice

class Canvas{
    constructor(width = 360, height = 360, outline = "1px dashed black", backgroundColor = "white"){
        // 建立初始化canvas
        this.canvas = this.initCanvas(width, height, outline, backgroundColor);
        this.ctx = this.canvas.getContext("2d"); // ctx
        this.children = []; // 用以儲存所有的圖形物件
        
    }

    // 初始化canvas
    initCanvas(width, height, outline, backgroundColor){
        let thecanvas = document.createElement("canvas");
        thecanvas.width = width;
        thecanvas.height = height;
        thecanvas.style.outline = outline;
        thecanvas.style.backgroundColor = backgroundColor;
        thecanvas.style.display = "block";
        // thecanvas.style.margin = "0";
        document.body.appendChild(thecanvas);
        return thecanvas;
    }

    // 提交所有在children內的圖形物件
    render(){
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 先根據layer將children內的物件排序
        this.children.sort((a,b)=>a.layer-b.layer);
        this.children.forEach(sprite => {
            this.displaySprite(sprite);
        });
    }
    
    // 顯示sprite
    displaySprite(sprite) {
        // 確認visible是否為true且位於canvas範圍內
        if (sprite.visible 
            && sprite.gx < this.canvas.width + sprite.width
            && sprite.gx + sprite.width >= -sprite.width
            && sprite.gy < this.canvas.height + sprite.height
            && sprite.gy + sprite.height >= -sprite.height) {

            // 先儲存目前狀態
            this.ctx.save();

            // 將目前圖形(中心為0,0)左上角座標平移至sprite的x,y
            this.ctx.translate(
                sprite.x + (sprite.width * sprite.pivotX),
                sprite.y + (sprite.height * sprite.pivotY)
            );
            
            // 設定sprite的各種效果
            this.ctx.rotate(sprite.rotation);
            this.ctx.scale(sprite.scaleX, sprite.scaleY);
            if(sprite.parent)
                this.ctx.globalAlpha = sprite.alpha * sprite.parent.alpha;

            // 顯示shadow
            if(sprite.shadow){
                this.ctx.shadowColor = sprite.shadowColor;
                this.ctx.shadowOffsetX = sprite.shadowOffsetX;
                this.ctx.shadowOffsetY = sprite.shadowOffsetY;
                this.ctx.shadowBlur = sprite.shadowBlur;
            }

            // 呼叫sprite自帶的render()來完成設定
            if(sprite.render)
                sprite.render(this.ctx);
            
            // render the children of this sprite recursively
            if (sprite.children && sprite.children.length > 0){
                 // 根據layer將children內的物件排序
                this.ctx.translate(-sprite.width * sprite.pivotX, -sprite.height * sprite.pivotY);
                sprite.children.forEach(child=>{
                    this.displaySprite(child);
                })
            }

            // 回復canvas至之前狀態
            this.ctx.restore();
        }
    }

    addChild(...sprite){
        // 將新的sprite加入到 array children
        for (let s of sprite)
            this.children.push(s);
        
    }
}//Canvas

class Pointer{
    constructor(element="none", scale=1){
        
        //this.element = document.getElementsByTagName(element)[0];
        this.element = element;
        //console.log(this.element);
        this.scale = scale;
        this._x = 0; // cursor x coordinate
        this._y = 0; // cursor y coordinate
        // press&enter methods to use
        this.press = undefined;
        this.enter = undefined;
        // add event listener to click and mousemove
        this.element.addEventListener("click", this.clickHandler.bind(this), false);
        this.element.addEventListener("mousemove", this.moveHandler.bind(this), false);
    }

    get x(){
        return this._x/this.scale;
    }
    get y(){
        return this._y/this.scale;
    }
    get centerX() {
        return this.x;
    }
    get centerY() {
        return this.y;
    }
    get position() {
        return {x: this.x, y: this.y};
    }

    clickHandler(event) {
        //Get the element that's firing the event
        let element = event.target;
        //Find the pointer’s x,y position (for mouse).
        //Subtract the element's top and left offset from the browser window
        this._x = (event.pageX - element.offsetLeft);
        this._y = (event.pageY - element.offsetTop);
        //console.log(this.centerX, this.centerY);
        //console.log(this.position);
        if (this.press) 
            this.press();
        //Prevent the event's default behavior
        event.preventDefault();
    }

    moveHandler(event) {
        //Get the element that's firing the event
        let element = event.target;
        //Find the pointer’s x,y position (for mouse).
        //Subtract the element's top and left offset from the browser window
        this._x = (event.pageX - element.offsetLeft);
        this._y = (event.pageY - element.offsetTop);

        if (this.enter) 
            this.enter();
        //Prevent the event's default behavior
        event.preventDefault();
    }
}//Pointer

function randomcolor(){
    return `rgb(${Math.floor(Math.random()*100000)%255}, 
                ${Math.floor(Math.random()*100000)%255}, 
                ${Math.floor(Math.random()*100000)%255})`;
}

function main(){
    let canvas = new Canvas(1280, 600);
    let ball_1 = new Circle(x=100, y=100, circleRadius=50);
    let ball_2 = new Circle(x=250, y=250, circleRadius=50);
    let box_1 = new Rectangle(100, 100, 'none', 'none', 3, 200, 50);
    let box_2 = new Rectangle(100, 100, 'none', 'none', 3, 50, 200);
    canvas.addChild(ball_1, ball_2, box_1, box_2);
    
    canvas.render();

    function hitSprite(...sprites){
        // when click on the sprite
        for(let s=0; s<sprites.length; s++){
            if(sprites[s].isInside(pointer.x, pointer.y)){
                sprites[s].fillStyle = `${randomcolor()}`;
                canvas.render();
            }
        }//for
    }//hitSprite()

    function enterSprite(...sprites){
        // when cursor enter/leave the sprite
        let inside=false;
        for(let s=0; s<sprites.length; s++){
            if(sprites[s].isInside(pointer.x, pointer.y)){
                inside=true;
            }
        }//for

        if(inside)
            canvas.canvas.style.cursor='pointer';
        else
            canvas.canvas.style.cursor='auto';

    }//enterSprite()

    let pointer = new Pointer(canvas.canvas);
    // define the functions for press & enter operations
    pointer.press = ()=>{hitSprite(ball_1, ball_2, box_1, box_2)};
    pointer.enter = ()=>{enterSprite(ball_1, ball_2, box_1, box_2)};

    //loop(); // >>>>>> Not really needed in this example
    // function loop() {
    //     requestAnimationFrame(loop);
    // }

} // main()
	
main();
上列程式會繪製幾個圖形(兩圓兩方),當滑鼠移動至圖形上,游標會改變,當點擊該圖形,該圖形會隨機變換顏色。欲達到此效果,首先先在Circle&Rectangle的class內增加一個函數來判斷某座標是否位於該sprite內。例如以下為Rectangle內之方法。
isInside(px, py){
    if(px>=this.x && py>=this.y && px<=this.x+this.width && py<=this.y+this.height)
        return true;
    else
        return false;
}//isInside
只要控制四個邊的範圍,而圓形則偵測與圓心的距離。至於其他比較複雜的圖形則需要比較複雜的方式來判斷。

接著建立Pointer物件來描繪游標位置。 在主程式中首先布置(canvas.render())所需要的sprites。而hitSprite()與enterSprite()則分別為press與enter所對應的函數內容,在此函數內描述圖形該對應之反應。然後使用箭頭函數分別將此兩函數指派給pointer.press與pointer.enter即可。

根據此架構可以自行增加圖形與對應的反應(針對滑鼠或鍵盤)。然後設計自己的邏輯架構與圖形互動(例如設計一個簡單的game)。

Exercise

踩地雷