JavaScript簡介
JavaScript是用在網頁設計的語言,是客戶端的語言,使用的IDE與網頁設計的相同即可,
Notepad++ 還不錯用。程式碼可以:
直接寫在html檔案內,只需要寫在<script></script>標籤內即可。
另一個地方是可以在瀏覽器內,因為是scripting語言,所以給予指令便可執行。
按F12 可看見分割視窗,選擇console即可。
寫成一個延伸檔名為.js的獨立的檔案,例如xxx.js,然後在head標籤內加上<script src="xxx.js"></script>即可
原則上寫成檔案為佳,方便管理並可重複使用,剛開始練習時可以使用另外兩種方式。
輸出
為了看到程式結果以及方便debug,我們需要輸出些文字來看內容或結果,一般可以用以下方式:
alert(): 跳出視窗顯示,例如alert("Hello!")。 See more about Popup box
console.log(): 在console顯示,例如console.log("Hello")。
若是包含變數,可以如此:
let num = 10 ;
console .log("He has studied" , num, "books." );
或是使用`${var name}`。
let num = 10 ;
console .log(`He has studied ${num} books.` );
document.write(): 在網頁顯示,例如document.write("Hello")。
let num = 10 ;
let color = "blue" ;
document .write(`<p style='color:${color} ;outline: 2px solid green;'>He has studied ${num} books.</p>` );
註解: 單行註解 => //; 區塊註解 => /**/
Statement:
敘述句是一個完整的程式指令,需於句後加上分號
; 來表示結束。=> console.log("Hello");
strict mode
使用"use strict";來表示使用strict mode,這可以強制我們寫比較安全的程式碼,部分不好的程式撰寫習慣會變成真正的錯誤。
在Strict Mode不允許以下寫法:
使用沒有宣告的變數 >> x = 3;
使用沒有宣告的物件 >> x = {a:10, b:20};
刪除變數或物件 >> var x = 2; delete x;
刪除函數 >> function f(a,b){}; delete f;
函數的輸入參數同名 >> function f(a,a){};
八進位數字 >> var x = 010; var x = "\010";
改變read only或get only屬性的值。
刪除不可刪除的屬性 >> delete Object.prototype;
使用with敘述句
使用eval()來建立在他被呼叫的範圍內的變數 >> eval("var x=2"); alert(x);
使用以下關鍵字作為參數名
arguments
eval
implements
interface
let
package
private
protected
public
static
yield
事實上其他所有關鍵字都不應該拿來當作變數名。
變數
JavaScript的變數型態包含
Number,Boolean,String ,可以使用typeof()函數來得到變數型態。
說明如下:
typeof (1 )
typeof (3.14 )
Boolean: 包含
true跟false >>
typeof (true )
typeof (false )
String: 使用
單引號或雙引號 >>
typeof ('1' )
typeof ("1" )
其中Number可分為int與float。
宣告
JavaScript不需要宣告變數型態,
var age = 18 ;
age
也可以使用
let 關鍵字。 >>
let school = "NKUST" ;
school
兩者的差異是let只作用於區塊{}內,區塊外無法使用let宣告的變數。而var可在整個函數內使用。
'use strict' ;
var a = '123' ;
if (true ){
let a = "doremi" ;
}
console .log(a);
const 用於定義常數,若是使用
const 宣告的變數,表示無法被改變。
const pi = 3.14159 ;
pi
pi = 18 ;
我們應多使用let與const,避免使用var。因為let為區域變數,僅可用於被定義的區塊,而var則否,若名稱相同則會混淆。
Number
在宣告時會根據輸入數值自動判斷是整數(int)或實數(float),此外尚可將
16進位(0x開頭)或8進位(0開頭)或指數(E)轉成十進位數字。>>
console .log(.12 );
console .log(0b111 );
console .log(0o12 );
console .log(0x12 );
console .log(2E2 );
若想將十進位數字傳換成其他進位,使用
toString(n) 。
let n = (100 ).toString(2 );
console .log("100的二進位 =" , n);
n = (100 ).toString(4 );
console .log("100的四進位 =" , n);
n = (100 ).toString(8 );
console .log("100的八進位 =" , n);
n = (100 ).toString(16 );
console .log("100的十六進位 =" , n);
若想將其他進位傳換成十進位,使用
parseInt(num, n) 。
let n = parseInt ("1100100" , 2 );
console .log("二進位1100100 =" , n);
n = parseInt ("1210" , 4 );
console .log("四進位1210 =" , n);
n = parseInt ("144" , 8 );
console .log("八進位144 =" , n);
n = parseInt ("64" , 16 );
console .log("十六進位64 =" , n);
NaN 表示Not a Number,在計算錯誤時出現。>>
console .log("a" -1 );
整數的範圍介於-2
53 ~ 2
53 之間(
Number.MAX_SAFE_INTEGER ),而實數數字太大,懶得打,
可用
MAX_VALUE與MIN_VALUE得到 。>>
Number .MAX_VALUE
Number .MIN_VALUE
加上負號則為負數的最小與最大。
如果數字超過上述之範圍,則用無限大(
Infinity與-Infinity )來表示。
>>
Number .MAX_VALUE + 1.0E308
-Number .MAX_VALUE - 1.0E308
可以使用
Number() 函數將其他型態轉為數值型態。 >>
var a = "12" ;
console .log(typeof (a));
console .log(typeof (Number (a)));
Boolean
只有true跟false兩個值。
var isYes = 1 ;
console .log(typeof (isYes));
console .log(typeof (Boolean (isYes)));
console .log(Boolean (isYes));
isYes = 0 ;
console .log(Boolean (isYes));
除了使用Boolean()函數來轉型態,尚可由上例看出0跟1似乎代表不同的值,原則上下列
情形會得到false:
數值為0或是NaN
空字串
null或undefined
所以即使是字串"false",轉型後還是true。 >>
var c = "false" ;
console .log(Boolean (c));
使用
Boolean 來轉型。 >>
var d = undefined ;
console .log(typeof (d));
console .log(Boolean (d));
String
使用單引號或雙引號表示,可以一起使用。 >>
console .log("The car is 'so' big." );
如此可以輸出引號。
除了上述方法,可以使用escape character ,例如:\b, \t, \n, \", \', \\等。 >>
console .log('The car is \t\'so\'\t big.' );
使用String()或toString() 函數轉換型態,或是使用+ 轉換。 >>
var s1 = String (true );
console .log(s1);
var s2 = toString(false );
console .log(typeof (s2));
typeof ("She weights" +45 + "kg." );
在定義變數時,儘可能給予初始值,如此可以在之後再看程式碼時了解定義的變數型態。例如:
let s = "" ;
let a = [];
let b = true ;
運算子(Operators)
運算子是計算符號,讓我們可以做運算,計有
算數運算子(Arithmetic Operators)
指派運算子(Assignment Operators)
比較運算子(Comparison Operators)
邏輯運算子(Logical Operators)
型態運算子(Type Operators)
位元運算子(Bitwise Operators)
算數運算子(Arithmetic Operators)
計有以下數種:
+: addition(可用於數字相加,數字+字串,字串+字串) >> console.log(10+10);
-: subtraction >> console.log(10 - 1);
*: multiplication >> console.log("5"*2);
/: division >> console.log(10/3);
%: modulus(remainder) >> console.log(10%3);
++: increment >>
var x = 10 ;
console .log(x++);
console .log(++x);
印出x++表示印x,++在後面,印出++x表示先計算加1再印出x。
--: decrement
與++用法相同,只不過是減1。
**: power >> console.log(3**5);
JavaScript的變數型態不明顯,所以可以計算例如"5"-2,雖然是字串-數字,但是因為字串內容是數字,依然可以計算。
指派運算子(Assignment Operators)
= : >> x = 5
+=: >> x += 5
-=: >> x -= 5
*=: >> x *= 5
/=: >> x /= 5
%=: >> x %= 5
+號可用於字串的相加,結果傳回字串。
比較運算子(Comparison Operators)
==: equal to(only value) >> "5" == 5 (true)
===: equal value and equal type
!=: not equal >> "5" != 5 (false)
!==: not equal value or not equal type
>: greater than
<: less than
>=: greater than or equal to
<=: less than or equal to
?: ternary operator >> 10 > 5 ? "Yes": "No"
在判斷兩者之值時,儘量多使用===替代==,以避免產生
"5" == 5 (true) 這樣的結果。
邏輯運算子(Logical Operators)
&&: logical and
||: logical or
! : logical not
型態運算子(Type Operators)
typeof: returns the type of a variable
instanceof: returns true if an object is an instance of an object type
位元運算子(Bitwise Operators)
&: and >> 0101 & 0100
|: or >> 5|2
~: not >> ~5
^: xor >> 1^5
<<: zero fill left shift >> 10 << 2 (*4)
>>: signed right shift >> 10 >> 1 (/2)
>>> zero fill right shift >> 10 >>> 1
JavaScript使用32bits位元計算。所以~5 = -6。
流程控制
流程控制可讓我們控制程式的進行,計有
if...else
switch
while loop
for loop
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 ) ) ;
每次呼叫函數將會得到傳回的數值。
Javascrip在執行時無法控制是否函數個數是正確的,所以我們必須自己控制。下例表示第二個輸入參數可以省略。
function fun (arg1, arg2 ) {
if (!arg1){
throw new Error ("arg1 is required." )
}else {
const a = arg2||'nothing' ;
return arg1 + " is " + a + "." ;
}
}
console .log(fun('This' , 'book' ));
console .log(fun('That' ));
現在可以直接給參數初始值即可。
function usdToNT (usd, rate=31 ) {
if (!usd){
throw new Error ("usd is required." )
}else {
return usd*rate;
}
}
console .log(usdToNT(100 , 32 ));
console .log(usdToNT(100 ));
將函數當作變數
這是一個有趣的功能,可以使用一個變數來表示函數,例如將上例修改如下:
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關鍵字,改用=>符號替代。
箭頭函數與正規函數還是有差別,看看以下例子:
若有輸入參數,可以不使用括號,若無輸入參數,需使用括號。此外,箭頭函數(arrow function)無法hoisting,也就是需在其宣告之後使用。arrow function不能用作建構子(constructor),如下例中可以new regular(),若是箭頭函數則不行。。
const re = new regular("Mary" );
console .log(re);
const yes = x => x*x;
const noo = () => {};
console .log(yes(10 ));
console .log(noo());
function regular (name ) {
this .name = name
}
arrow function無法傳回輸入參數。
const returnArgs = function ( ) {
return arguments ;
}
console .log(returnArgs(1 ,"do" ,true ));
const arrowArgs = () => arguments ;
不定長度參數
若是輸入參數長度不定,可以使用不定長度參數(使用...表示),例如:
function test (className, ...grades ) {
let sum = 0 ;
for (let i of grades){
sum += i;
}
console .log(className, "班,總分為" , sum);
}
test("101" , 48 ,78 ,89 );
若是所有參數都屬於不定長度參數的部分,也可以直接使用arguments。
function test ( ) {
let sum = 0 ;
const toArr = Array .prototype.slice.call(arguments )
console .log(toArr);
for (let i=0 ; i<toArr.length; i++){
sum += toArr[i];
}
console .log("總分為" , sum);
}
test(48 ,78 ,89 );
其中的const toArr = Array.prototype.slice.call(arguments)表示將arguments強制改為array。
當然也可以寫成這樣。
function test (...grades ) {
let sum = 0 ;
for (let i=0 ; i<grades.length; i++){
sum += grades[i];
}
console .log("總分為" , sum);
}
test(48 ,78 ,89 );
有不定長度參數應使用此方式(減少使用arguments),若與其他參數混雜則為第一例。
...array可以將array拆解為數列,如下:
const arr = [1 ,2 ,3 ];
console .log(...arr);
所以也可以這樣:
function perimeter (a, b, c ) {
return a + b + c;
}
const sides = [7 , 8 , 5 ];
console .log(perimeter(...sides));
原則上加上...為數列,沒有...則為陣列(array)。
function rest (leading, ...supports ) {
return [leading, supports.sort()];
}
function restAndSpread (leading, ...supports ) {
return [leading, ...supports.sort()];
}
console .log(rest("Will Smith" , "Bill Pullman" , "Jeff Goldblum" , "Margaret Colin" ));
console .log(restAndSpread("Will Smith" , "Bill Pullman" , "Jeff Goldblum" , "Margaret Colin" ));
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。
Click(Press F12 see output)
< 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 來宣告。
變數給值是使用冒號(:) ,而變數之間是用逗點(,) 分開。
我們可以試著使用,加上以下的程式碼:
console. log ( Node. x + " " + Node. y + " " + Node. demand + " " + Node. isVisited) ;
可以在console看到結果。
若是在物件之外要改變裡面的值,可以使用
點(.) ,
例如在印出之前加上
Node. x = 80 ;
可以看到結果的x變成了80。
另一個方式是使用中括號[變數名]來取得變數,例如再加上
Node[ " isVisited " ] = true ;
再看一次結果。
物件方法
物件內除了有變數,還可以加上方法,原則上就是加上函數,修改上例如下:
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( ) ) ;
說明:
方法也算是一種變數,所以與變數間一樣用逗點(,) 分開。
方法與方法之間也是使用逗點(,) 分開。
需使用this 關鍵字來取得物件內的參數。即使是方法。關於this的說明如下:
在方法中的this表示此方法的擁有者,在這裡便是Node物件。所以this.x指的便是Node.x,
若是將this改為Node也可運行,若都沒有則出現錯誤。
如果不是在方法內,單獨使用this表示的是全域物件,在瀏覽器中代表的是window,例如:
var t = this ;
console. log ( t) ;
此會顯示Window。
通常定義物件內的函數為方法(method),非物件內的函數為函數(function),事實上所有的函數都是方法,因為函數是屬於全域物件(window)的方法。
函數也可以用來建立物件,如下:
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) ) ;
建立三個物件,其中arc的方法endpoint將使用到node的變數。
使用call()或是apply()來取得物件內的變數將其用於方法內。
兩者的不同處是apply()接受參數為陣列型態。例如我們將上例的arc改寫如下並呼叫:
var arc= {
endpoint: function ( id, demand) {
return this . x + " " + this . y + " " + id + " " + demand;
}
}
console. log ( arc. endpoint. call ( nodeA, " A " , 100 ) ) ;
console. log ( arc. endpoint. apply ( nodeB, [ " B " , 200 ] ) ) ;
可以看到apply需使用陣列型態。
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));
k = k + i;
}
console .log(k);
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 );
test.call(undefined , 1 , 2 );
test.apply(undefined , [1 , 2 ]);
test.call("value of this" , 1 , 2 );
test.apply(100 , [1 , 2 ]);
test.bind("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();
gr.hello();
在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();
gr.hello();
第二個方式是使用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();
gr.hello();
第三個方式是使用箭頭函數來寫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();
gr.hello();
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供我們直接使用,計有:
Object(): {}
String(): ""
Number(): 0
Boolean(): true
Array(): []
RegExp(): /()/
Function(): function(){}
Date():
以上的內建constructor都可以使用類似
var x = new Object(); 這樣的語法來產生,但是為了簡單化,所以只要給象徵的數值即可產生,例如給數字即知道是Number。
BOM
Browser Object Model:關於瀏覽器的物件模式,可讓Javascript與之連結。
Window Object: 所有瀏覽器都適用。所有的Javascript全域objects, functions, 與variables會自動變成window object的一員。
。
全域變數是window object的property。全域函數是屬於window object的方法。即使是HTML DOM 的document物件都是window object的property。例如:
< script >
document. getElementById( ' body ' ) . innerText = " Hello " ;
</ script >
與
< script >
window. document. getElementById( ' body ' ) . innerText = " Hello " ;
</ script >
原則上是相同的。
window size:可用來決定瀏覽器window的大小。例如:
console. log ( window. innerHeight + " \t " + window. innerWidth) ;
因為IE的語法不同,可以用以下的方式來cover所有的瀏覽器:
var w = window. innerWidth
|| document. documentElement. clientWidth
|| document. body. clientWidth;
var h = window. innerHeight
|| document. documentElement. clientHeight
|| document. body. clientHeight;
console. log ( w + " \t " + h) ;
window.open: 開啟新視窗。例如:
function newwindow( event) {
w = 800 ;
h = 600 ;
leftgap = ( window. innerWidth- w) / 2 ;
topgap = ( window. innerHeight- h) / 2 ;
window. open( ' http://www.google.com ' , ' Google ' ,
config= ` width = ${ w } , height = ${ h } , left= ${ leftgap } , top= ${ topgap } ` ) ;
}
function createANode( href, aText) {
var anode = document. createElement( " a " ) ;
anode. setAttribute( " href " , href) ;
anode. innerText = aText;
anode. addEventListener( ' click ' , newwindow) ;
return anode;
}
var anode = createANode( " # " , " Open A New Window " ) ;
document. getElementById( ' body ' ) . appendChild( anode) ;
此處將建立一個a node包裝成一個函數,callback函數也獨立寫成一個函數。不過callback函數只接受一個event argument,如果要將w, h等做為函數的輸入值,可以將callback函數當成函數的傳回(currying),如下:
function newwindow( w, h) {
return ( event) = > { // 回傳callback函數
w = 800 ;
h = 600 ;
leftgap = ( window. innerWidth- w) / 2 ;
topgap = ( window. innerHeight- h) / 2 ;
window. open( ' http://www.google.com ' , ' Google ' ,
config= ` width = ${ w } , height = ${ h } , left= ${ leftgap } , top= ${ topgap } ` ) ;
}
}
function createANode( href, aText) {
var anode = document. createElement( " a " ) ;
anode. setAttribute( " href " , href) ;
anode. innerText = aText;
anode. addEventListener( ' click ' , newwindow( 800 , 600 ) ) ; //此處的callback可以傳入參數
return anode;
}
var anode = createANode( " # " , " Open A New Window " ) ;
document. getElementById( ' body ' ) . appendChild( anode) ;
window.close:關閉視窗。先建立一個名為popwindow.html的檔案,然後將上例的open修改如下:
window. open( ' popwindow.html ' , ' tiny ' ,
config= ` width = ${ w } , height = ${ h } , left= ${ leftgap } , top= ${ topgap } );
此時開啟的是popwindow.html,在其內加上以下javascript code:
< script>
function createCloseButton( ) {
var closebutton = document. createElement( " button " ) ;
closebutton. innerHTML = " Close " ;
closebutton. addEventListener( " click " , ( event) = > {
window. close( ) ;
} )
return closebutton;
}
var bnode = createCloseButton( ) ;
document. getElementById( ' body ' ) . appendChild( bnode) ;
< / script>
在popwindow內建立一個按鈕,連結方法為close,點選按鈕關閉視窗。
window.moveTo:開啟新視窗並移動至特定位置(left & top)。將上例改寫如下:
< script>
function createButton( inner) {
var button = document. createElement( " button " ) ;
button. innerHTML = inner;
return button;
}
var closebutton = createButton( " Close " ) ;
closebutton. addEventListener( " click " , ( event) = > {
window. close( ) ;
} )
var movetobutton = createButton( " MoveTo " ) ;
movetobutton. addEventListener( " click " , ( event) = > {
this . moveTo( 0 , 0 ) ;
} )
document. getElementById( ' body ' ) . appendChild( movetobutton) ;
document. getElementById( ' body ' ) . appendChild( closebutton) ;
< / script>
如果把moveTo(x, y)方法改為moveBy(x, y)表示移動至原位置+(x,y)位置。
this . moveBy( 10 , 10 ) ; //原位置x+10,y+10
resizeBy() & resizeTo: 改變視窗大小。例如將以下code加到上例:
var resizebutton = createButton( " resize " ) ;
resizebutton. addEventListener( " click " , ( event) = > {
this . resizeTo( 300 , 300 ) ;
} ) ;
document. getElementById( ' body ' ) . appendChild( resizebutton) ;
如果換成resizeBy(x,y),亦即在原視窗大小+(x,y)。
this . resizeBy( 100 , - 100 ) ;
Window Screen: 關於使用者螢幕的資訊。
console. log ( ` ${ screen . width } , ${ screen . height } , ${ screen . availWidth } , ${ screen . availHeight } ` ) ;
Screen colorDepth & pixelDepth: 螢幕的色素與像素。
console. log ( ` ${ screen . colorDepth } , ${ screen . pixelDepth } ` ) ;
Window location: 取得目前頁址(URL)並指向(redirect)新頁。
相關屬性:
console. log ( `
URL: ${ window . location . href } ,
Host name: ${ window . location . hostname } ,
Pathname: ${ window . location . pathname } ,
Protocol: ${ window . location . protocol } ,
Port: ${ window . location . port } ` ) ;
window.location.assign():redirect to other page。
var reButton = createButton( " Google " ) ;
reButton. addEventListener( ' click ' , ( event) = > {
window. location. assign( " http://www.google.com " ) ;
} )
document. getElementById( ' body ' ) . appendChild( reButton) ;
Window History: 歷史頁面。
window.history.back & forward:往前往後頁。
var back = createButton( " back " ) ;
var forward = createButton( " forward " ) ;
back. addEventListener( ' click ' , ( event) = > {
window. history. back( ) ;
} )
forward. addEventListener( ' click ' , ( event) = > {
window. history. forward( ) ;
} )
document. getElementById( ' body ' ) . appendChild( back) ;
document. getElementById( ' body ' ) . appendChild( forward) ;
window.navigator物件包含使用者瀏覽器資訊。
例如cookieEnalbed或是appName等。
console. log ( `
cookie enable? = ${ navigator . cookieEnabled } ,
app name? = ${ navigator . appName } ,
app code name? = ${ navigator . appCodeName } ,
platform? = ${ navigator . platform }
` ) ;
Popup Alert: 包含Alert box, Confirm box, 與Prompt box。
Alert box:
window. alert ( ` ${ window . innerWidth } ` ) ; // or simply alert() without window
Confirm box:
var adiv = document. createElement( " div " ) ;
var txt = " " ;
if ( confirm ( " OK? " ) ) {
txt = " You pressed OK. " ;
adiv. innerText = txt;
adiv. style = ' border: 1px solid blue ' ;
} else {
txt = " Not OK. " ;
adiv. innerText = txt;
adiv. style = ' border: 1px dash red; color:tomato; background:black ' ;
}
document. getElementById( ' body ' ) . append( adiv) ;
Prompt box:
var adiv = document. createElement( " div " ) ;
var name = prompt ( " What is your name? " , " Tom Cruise " ) ; // second argument is default value
if ( name == " null " || name == " " ) {
txt = " User cancelled the prompt. " ;
adiv. innerText = txt;
adiv. style = ' border: 1px solid blue ' ;
} else {
txt = ` Hi, ${ name } . ` ;
adiv. innerText = txt;
adiv. style = ' border: 1px dash red; color:tomato; background:black ' ;
}
document. getElementById( ' body ' ) . append( adiv) ;
Timing Events: 關於控制時間間隔以執行程式,主要有兩個方法setTimeout(function, milliseconds)與setInterval(function, milliseconds),兩個方法都是HTML DOM Window物件的方法。
setTimeout(function, milliseconds):經過milliseconds之後執行function。例如:
var start = document. createElement( " button " ) ;
var stop = document. createElement( " button " ) ;
start. innerText = " Start " ;
stop. innerText = " Stop " ;
var toStart;
function func( event) {
toStart = window. setTimeout ( ( ) = > { // window.可有可無
alert ( " Hi, there. " ) ;
} , 3000 ) ;
}
start. addEventListener( " click " , func) ;
stop. addEventListener( " click " , ( event) = > {
window. clearTimeout ( toStart) ;
console. log ( " setTimeout() operation has been stopped. " ) ;
} ) ;
document. getElementById( ' body ' ) . appendChild( start) ;
document. getElementById( ' body ' ) . appendChild( stop) ;
clearTimeout()用來終止setTimeout()內函數的執行。
clearTimeout()需在setTimeout()內函數執行前被執行才有效。
setInterval(function, milliseconds):設定在每milliseconds便執行一次function。
var clock = document. createElement( " div " ) ;
var si = setInterval ( ( ) = > {
clock. innerHTML = Date( ) ; // see also Date()
} , 1000 ) ;
document. getElementById( ' body ' ) . appendChild( clock) ;
var stop = document. createElement( " button " ) ;
stop. innerHTML = " stop " ;
stop. addEventListener( ' click ' , ( event) = > {
window. clearInterval ( si) ;
} )
document. getElementById( ' body ' ) . appendChild( stop) ;
clearInterval()用來終止setInterval()內函數的執行。
練習:設計一個碼表。
class Stopwatch{
constructor( ) {
this . theTime = 0 ;
this . si; // the variable returned from setInterval()
/* create elements */
this . startButton = this . createStartButton( ) ;
this . stopButton = this . createStopButton( ) ;
this . timeDiv = this . createTimeDiv( ) ;
/* append elements */
document. getElementById( ' body ' ) . appendChild( this . startButton) ;
document. getElementById( ' body ' ) . appendChild( this . stopButton) ;
document. getElementById( ' body ' ) . appendChild( this . timeDiv) ;
/* start & stop operations */
this . start( ) ;
this . stop( ) ;
}
/* create a start button */
createStartButton( ) {
var startButton = document. createElement( " button " ) ;
startButton. innerHTML = " start " ;
return startButton;
}
/* create a stop button */
createStopButton( ) {
var stopButton = document. createElement( " button " ) ;
stopButton. innerHTML = " stop " ;
return stopButton;
}
/* create a show div */
createTimeDiv( ) {
var adiv = document. createElement( " div " ) ;
adiv. innerHTML = " 00 : 00 : 00 " ;
return adiv;
}
/* calculate the time and return a string */
interval( theTime) {
let min = 0 ;
let sec = 0 ;
let minisec = 0 ;
min = Math. floor ( this . theTime/ 100 / 60 ) ;
sec = Math. floor ( ( this . theTime - min * 60 * 100 ) / 100 ) ;
minisec = this . theTime - min * 100 * 60 - sec* 100 ;
let showmin = min < 10 ? " 0 " + min : min ;
let showsec = sec < 10 ? " 0 " + sec: sec;
let showminisec = minisec < 10 ? " 0 " + minisec: minisec;
return ` ${ showmin } : ${ showsec } : ${ showminisec } ` ;
}
/* operation while start button is pressed */
start( ) {
this . startButton. addEventListener( ' click ' , ( ) = > {
this . si = window. setInterval ( ( ) = > {
this . theTime = this . theTime + 1 ;
this . timeDiv. innerHTML = this . interval( ) ;
} , 10 ) ;
} ) ;
}
/* operation while stop button is pressed */
stop( ) {
this . stopButton. addEventListener( ' click ' , ( ) = > {
window. clearInterval ( this . si) ;
} ) ;
}
}
var stopwatch = new Stopwatch( ) ;
Cookies: 可讓我們儲存少量資料(about 4KB)在網頁。
Basic: 內容是個字串,按F12可以直接操作:
document. cookie = " username = Tom Cruise " ;
簡單就可建立cookie內容,使用
console. log ( document. cookie) ;
來查看內容。因為cookie有存活時間,一般來說可以給定expire date,並給定路徑,如下:
document. cookie = " username = Tom Cruise; expires = Wed, 11 Mar 2020 12:00:00 UTC; path=/ " ;
console. log ( document. cookie) ;
新的cookie會加到字串內。如果要刪除的話,可以將過期日期修改到現在之前即可。
建立設定cookie的函數:
function setCookie( kname, kvalue, kday) {
let d = new Date( ) ; // current time
d. setTime ( d. getTime ( ) + ( kday* 24 * 60 * 60 * 1000 ) ) ; // set expired day(kdays)
document. cookie = ` ${ kname } = ${ kvalue } ; expires = ${ d . toUTCString ( ) } ; path=/ ` ;
}
建立取得cookie的函數:
function getCookie( kname) {
let startIndex = document. cookie. indexOf ( kname+ " = " ) ;
if ( startIndex == - 1 ) {
console. log ( " Unknown cookie name >> " + kname) ;
return " " ;
} else {
startIndex = startIndex + kname. length + 1 ;
let endIndex = document. cookie. indexOf ( " ; " , startIndex+ 1 ) ;
if ( endIndex === - 1 ) {
endIndex = document. cookie. length ;
}
return document. cookie. substring ( startIndex, endIndex) ;
}
}
建立使用cookie的函數:
function checkCookie( ) {
var adiv = document. createElement( " div " ) ;
let msg = " Hello, " ;
var user = getCookie( " username " ) ;
if ( user != " " ) {
adiv. innerHTML = ` Hello, ${ user } ` ;
} else {
user = prompt ( " Please enter your name: " , " Bill Clinton " ) ;
if ( user != " " && user != null ) {
setCookie( " username " , user, 1 ) ;
adiv. innerHTML = ` Hello, ${ getCookie ( " username " ) } `
}
}
document. getElementById( ' body ' ) . appendChild( adiv) ;
}
//setCookie("username", "Bill Clinton", -10); // to delete the cookie
checkCookie( ) ;
Math&Date
介紹兩個常用的物件,Math & Date。
Math
math物件提供數學相關參數與方法供我們使用,Math內的方法都是static,所以不需要建立,直接使用Math.XXX來呼叫。Date也是內建物件,使用new關鍵字建立,可以讓我們操作時間的顯示,在之後介紹。
Math properties
而其擁有的屬性如下:>>
Math.E: Euler's number -> 2.718
Math.LN2: Natural logarithm of 2 -> 0.693
Math.LOG2E: Base 2 logarithm of E -> 1.442
Math.LOG10E: Base 10 logarithm of E -> 0.434
Math.PI: pi -> 3.14159
Math.SQRT1_2: Square root of 1/2 -> 0.707
Math.SQRT2: Square root of 2 -> 1.414
>>
console. log ( " E: " + Math. E + " \n LN2: " + Math. LN2 + " \n LN10: " + Math. LN10 + " \n LOG2E: " + Math. LOG2E +
" \n LOG10E: " + Math. LOG10E + " \n PI: " + Math. PI+ " \n SQRT1_2: " + Math. SQRT1_2 + " \n SQRT2: " + Math. SQRT2) ;
Math methods
Math的相關方法計有如下:
Math.abs(x): absolute value
Math.acos(x): arccosine (in radians)
Math.asin(x): arcsine (in radians)
Math.atan(x): arctangent (in radians)
Math.atan2(x,y): arctanget of the quotient of its arguments
Math.ceil(x): smallest integer greater than of equal to x
Math.cos(x): cosine
Math.exp(x): ex
Math.floor(x): largest integer less than or equal to x
Math.log(x): loge x(ln(x))
Math.max(x,y,...): largest number
Math.min(x,y,...): smallest number
Math.pow(x,y): xy
Math.random(): random number between 0 and 1
Math.round(x): a number rounded to the nearest integer
Math.sin(x): sine
Math.sqrt(x): square root of x
Math.tan(x): tangent
Date
Date的宣告方式可以有以下幾種:
new Date()
new Date(milliseconds): 1/1/70 + milliseconds
new Date(datestring): "yyyy-mm-dd", "yyyy-mm", "yyyy", "yyyy-mm-ddThh:mm:ssZ"(Z:UTC time),
"yyyy", "yyyy-mm-ddThh:mm:ss-hh:mm" (compare to UTC), "mm/dd/yyyy", "mmm dd yyyy" (Jul 10 2000)
new Date(year, month, date[, hours, minutes, seconds, milliseconds])
Date methods
Date(): today's date and time. >>
console. log ( Date( ) ) ;
getDate(): returns day of the month. >>
var d = new Date( " December 31, 1999, 23:59:59 " ) ;
console. log ( d. getDate ( ) ) ;
getDay(): returns day of the week. >>
console. log ( d. getDay ( ) ) ;
getFullYear(): returns the year. >>
console. log ( d. getFullYear ( ) ) ;
getHours(): returns the hour. >>
console. log ( d. getHours ( ) ) ;
getMilliseconds(): returns milliseconds. >>
console. log ( d. getMilliseconds ( ) ) ;
getMinutes(): returns the minutes. >>
console. log ( d. getMinutes ( ) ) ;
getMonth(): returns the month. >>
console. log ( d. getMonth ( ) ) ;
getSeconds(): returns the seconds. >>
console. log ( d. getSeconds ( ) ) ;
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.
getTimezoneOffset(): returns the time-zone offset in minutes. >>
console. log ( d. getTimezoneOffset ( ) ) ;
getUTCDate(): returns the date of the month according to universal time. >>
console. log ( d. getUTCDate ( ) ) ;
getUTCDay(): returns the day of the week according to universal time. >>
console. log ( d. getUTCDay ( ) ) ;
getUTCFullYear(): returns the year according to universal time. >>
console. log ( d. getUTCFullYear ( ) ) ;
getUTCHours: returns the hours according to universal time. >>
console. log ( d. getUTCHours ( ) ) ;
getUTCMilliseconds(): returns the milliseconds according to universal time. >>
console. log ( d. getUTCMilliseconds ( ) ) ;
getUTCMinutes(): returns the minutes according to universal time. >>
console. log ( d. getUTCMinutes ( ) ) ;
getUTCMonth(): returns the month according to universal time. >>
console. log ( d. getUTCMonth ( ) ) ;
getUTCSeconds(): returns the seconds according to universal time. >>
console. log ( d. getUTCSeconds ( ) ) ;
setDate(): sets the day of month. >>
d. setDate ( 1 ) ;
console. log ( d) ;
setFullYear(): sets the year. >>
d. setFullYear ( 2018 ) ;
console. log ( d) ;
setHours(): sets the hour. >>
d. setHours ( 12 ) ;
console. log ( d) ;
setMilliseconds(): sets the Milliseconds. >>
d. setMilliseconds ( 12 ) ;
console. log ( d) ;
setMinutes(): sets the Minutes. >>
d. setMinutes ( 12 ) ;
console. log ( d) ;
setMonth(): sets the Month. >>
d. setMonth ( 12 ) ;
console. log ( d) ;
setSeconds(): sets the Seconds. >>
d. setSeconds ( 12 ) ;
console. log ( d) ;
setTime(): sets the Date object to the time represented by a milliseconds >>
d. setTime ( 9466559990000 ) ;
console. log ( d) ;
setUTCDate(): sets the day of the month according to universal time. >>
d. setUTCDate ( 10 ) ;
console. log ( d) ;
setUTCFullYear(): sets the year according to universal time. >>
d. setUTCFullYear ( 2250 ) ;
console. log ( d) ;
setUTCHours(): sets the hour according to universal time. >>
d. setUTCHours( 10 ) ;
console. log ( d) ;
setUTCMilliseconds(): sets the milliseconds according to universal time. >>
d. setUTCMilliseconds ( 86400000 ) ;
console. log ( d) ;
setUTCMinutes(): sets the minutes according to universal time. >>
d. setUTCMinutes ( 30 ) ;
console. log ( d) ;
setUTCMonth(): sets the month according to universal time. >>
d. setUTCMonth ( 6 ) ;
console. log ( d) ;
setUTCSeconds(): sets the seconds according to universal time. >>
d. setUTCSeconds ( 75 ) ;
console. log ( d) ;
toDateString(): returns date. >>
console. log ( d. toDateString( ) ) ;
toLocaleDateString(): returns date using current locale's conventions. >>
console. log ( d. toLocaleDateString( ) ) ;
toLocaleString(): converts a date to a string, using current locale's conventions. >>
console. log ( d. toLocaleString ( ) ) ;
toLocaleTimeString(): returns the time of the Date, using current locale's conventions. >>
console. log ( d. toLocaleTimeString( ) ) ;
toString(): returns a String of a Date object. >>
console. log ( d. toString ( ) ) ;
toTimeString(): returns a String of time of a Date object. >>
console. log ( d. toTimeString( ) ) ;
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,計有:
concat(): 合併兩個array。 >>
var cars = [ " Benz " , " BMW " , " Ferrari " , " Porsche " , " Lamborghini " ] ;
var owners = [ " Alex " , " Boney " , " John " , " Tom " , " Sean " ] ;
var carowner = cars. concat ( owners) ;
console. log ( carowner) ;
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 ; }
)
) ;
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);
forEach(): 針對array中每一個元素。之前介紹過,再加個例子。 >>
n. forEach (
( e, i) = > { console. log ( i + " - " + e) }
//(e,i)=>{n[i] = e*e;} // try this
)
第二個參數是index。
indexOf():傳回第一個相符元素的index,若不包含傳回-1。 >>
console. log ( n. indexOf ( 6 ) ) ;
join(): 將所有元素加成一個字串。 >>
console. log ( n. join ( ) ) ;
lastIndexOf(): 跟indexOf(),只是由後往前搜尋,若是有重複的元素指傳回第一個找到的位址。
map(): 傳回一個新array,其中元素為舊元素傳入某一函數後之傳回。 >>
console. log ( n. map (
( e) = > { return e* e; }
) ) ;
flatMap()
flatMap()相當於呼叫map()之後再呼叫flat()使其攤平。
let arr = [1 ,2 ,3 ];
arr.flatMap((x)=>{
console .log(x*x);
});
arr.flatMap((x, i)=>{
console .log(arr[i], x*x);
});
let song = ["one little" , "two little" , "three little indians." ];
console .log(song.map(x=>x.split(" " )));
console .log(song.flatMap(x=>x.split(" " )));
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 ]);
console .log(output);
pop(): 移除array的最後一個元素並傳回該元素。 >>
console. log ( n. pop( ) + " \n " + n) ;
push(): 在array後面加上一個或多個元素並傳回新array的length。 >>
console. log ( " length = " + n. push( 18 , 22 ) + " \n " + n) ;
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; }
)
) ;
reduceRight(): 跟reduce()一樣,只是方向相反,由右向左。 >>
console. log (
n. reduceRight (
( e1, e2) = > { return e1* e2; }
)
) ;
reverse(): 將array中的元素順序倒轉。 >>
console. log ( n. reverse( ) ) ;
這個結果跟上一個例子似乎相同,實際上上一個例子只是倒過來印出,這個例子是改變了array的內容,也就是此時n[0]變成了原來的最後一個元素。
shift(): 跟pop()相反,此為移除第一個元素並傳回該元素。 >>
console. log ( n. shift ( ) + " \n " + n) ;
slice(): 將array的一部分切片形成一個新的array並傳回。 >>
console. log ( n. slice ( 1 , 3 ) + " \n " + n) ;
傳回[n[1], n[2]](第二個數字位址不包含),而原array不變。
some(): 如果array內至少一個元素符合特定函數標準,傳回true,否則傳回false。 >>
console. log (
n. some (
( e) = > { return e > 10 ; }
)
) ;
跟every()類似,只是every()需要所有元素都符合才傳回true。
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之前的隨機數,將在後面的章節介紹。
splice(): 加入或刪除array內的元素。 >>
console. log ( n) ;
n. splice( 2 , 2 , 20 , 30 , 40 , 50 , 60 ) ;
console. log ( n) ;
參數的意義是自index=2開始(第一個參數)刪除兩個元素(第二個參數),並在同位置加上後面所有元素(其餘的所有參數)。
toString(): 傳回元素內容字串。 >>
console. log ( n. toString ( ) ) ;
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 ));
console .log(arr.includes(1 , 2 ));
console .log(arr.includes('c' ));
console .log(arr.includes('x' ));
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 ));
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' ]);
console .log(obj1[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 ));
console .log(map.get('true' ));
很明顯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' );
console .log(toStr);
let toArr = Array .from(map);
console .log([...toArr].join('\n' ));
let js = JSON .stringify([...map]);
console .log(js);
let toJSON = JSON .parse(js);
console .log(toJSON[0 ]);
let jsonToMap = new Map (toJSON);
console .log(jsonToMap);
array(資料對)、map、與object三者轉換,使用
Object.fromEntries(entries) 與
Object.entries(obj) 。
let entries = [[1 , "one" ],[2 , "two" ],[3 , "three" ], [4 , 'four' ], [5 , 'five' ], [6 , 'six' ]];
let map = new Map (entries);
let obj = Object .fromEntries(entries);
console .log("----------array to object----------\n" , obj);
let mapobj = Object .fromEntries(map);
console .log("----------map to obj----------\n" , mapobj);
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();
console .log(map.size);
Set()
Set跟Map類似,不過不是資料對(或是說key value同值),特色是不能有重複的原件,因此Set並沒有索引(index)。
const arr = ['do' , 're' , 'mi' ];
const set = new Set (arr);
set.add(1 );
set.add('do' );
console .log(set);
console .log(set.has('do' ));
set.delete('re' );
console .log(set.size);
const colors = ['red' , 'green' , 'blue' ];
const union = new Set ([...set, ...colors]);
for (let i of union){
console .log(i);
}
set.forEach((k,v) => console .log(k,v));
const toArr = [...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)
字串也有許多內建方法供我們使用,如下:
charAt(): 在某個index位置的字元。
console. log ( s1. charAt ( 1 ) ) ;
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進位皆可,你可以試著自己輸入不同數字。
concat(): 連結兩個字串並傳回新字串。
var s = " xyz " ;
s = s. concat ( s1) ;
console. log ( s) ;
跟array相同,不過字串也可以使用+ 號來串聯。
endsWith():檢查字串是否結尾於某子字串,傳回boolean。 >>
console. log ( " String s ends with 'bc' : " + s. endsWith ( " bc " ) ) ;
includes: 檢查字串是否包含某子字串,傳回boolean。 >>
console. log ( s. includes( " ab " ) ) ;
indexOf(): 尋找字串某一個子字串,傳回第一個出現的index,若不包含則傳回-1。 >>
console. log ( s. indexOf ( " za " ) ) ;
lastIndexOf(): 與indexOf()類似,只是搜尋方向自後向前。 >>
console. log ( s = s. concat ( " xyzabc " ) ) ;
console. log ( s. concat ( " xyzabc " ) . lastIndexOf ( " za " ) ) ;
localeCompare(): 比較兩個字串排序的先後,傳回1表示原字串在比較字串(傳入參數)之後,-1在之前,0則表示相同。 >>
console. log ( s. localeCompare( " xyzabcxyzabb " ) +
" \t " + s. localeCompare( " xyzabcxyzabc " ) +
" \t " + s. localeCompare( " xyzabcxyzabd " )
) ;
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。
repeat(): 將字串重複給定次數。 >>
console. log ( s. repeat ( 3 ) ) ;
replace(): 用新字串替代字串中符合某re規則的子字串。 >>
console. log ( s. replace ( " abc " , " ijk " ) ) ;
console. log ( s. replace ( / abc / g , " ijk " ) ) ;
沒有re規則只替換了第一個子字串。
search(): 搜尋字串中符合某re規則的子字串,傳回index,-1表示不包含該re規則的子字串。 >>
console. log ( str. search ( re) ) ;
console. log ( str. search ( " js110.html " ) ) ;
slice(): 傳回某一範圍內之子字串。 >>
console. log ( s. slice ( 3 , 10 ) ) ;
若是僅給一個數字表示直至字串的最後。
split(): 將字串根據某字元分拆為數個單元並傳回包含所有單元之array。 >>
console. log ( s. split ( " a " ) ) ;
console. log ( s. split ( " " ) ) ;
startsWith(): 檢查字串是否開始於某子字串,傳回boolean。 >>
console. log ( s. startsWith ( " xy " ) ) ;
substr(): 傳回某位置之後某長度的子字串。>>
console. log ( s. substr ( 3 , 5 ) ) ;
console. log ( s. substr ( - 3 , 5 ) ) ;
substring(): 傳回某一範圍內之子字串。效果同slice()。 >>
console. log ( s. substring ( 3 , 10 ) ) ;
toLocaleLowerCase(): 將字串中字元轉換成小寫。 >>
console. log ( " ABCDEFG " . toLocaleLowerCase( ) ) ;
toLowerCase(): 將字串中字元轉換成小寫。與toLocaleLowerCase()同。 >>
console. log ( " ABCDEFG " . toLowerCase ( ) ) ;
toUpperCase(): 將字串中字母轉換為大寫。 >>
console. log ( " This is a book. " . toUpperCase ( ) ) ;
trim(): 去除字串頭尾的空白。 >>
console. log ( " well... \t " . trim ( ) + " well... \t " . trim ( ) . length ) ;
Events
之前設計的每一個函數,都是直接給指令執行,當操作網頁時,我們希望函數在特定的情形下才會被執行,例如在文字輸入列輸入了資料或是按了一個按鈕,Event的含意便是讓我們控制函數執行的時機。
以下為部分Events: >>
Mouse :
onclick : mouse click
ondblclick : mouse double-click
onmousedown : mouse button is pressed
onmousemove : mose pointer moves
onmouseout : mouse pointer moves out an element
onmouseover : mouse pointe moves over an element
onmouseuup : mouse button is released
onmousewheel : mouse wheel is being rotated
Keyboard :
onkeydown : a key is pressed
onkeypress : a key is pressed and released
onkeyup : a key is released
Window & Document :
Offline : document goes offline
onafterprint : after the document is printed
onbeforeonload : before the document loads
onblur : when the window loses focus
onfocus : when the window gets focus
onload : when the document loads
onpagehide : when the window is hidden
onpageshow : when the window becomes visible
onresize : when the window is resized
Element :
onchange : when an element changes
ondrag : when an element is dragged
ondragend : at the end of a drag operation
ondragenter : when an element has been dragged to a valid drop target
ondragleave : when an element is being dragged out of a drop target
ondragover : when an element is being dragged over a drop target
ondragstart : at the start of a drag operation
ondrop : when dragged element is being dropped
onformchange : when a form changes
onforminput : when a form gets user input
oninput : when an element gets user input
onscroll : when an element's scrollbar is being scrolled
onselect : when an element is selected
onsubmit : when a form is submitted
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 : 100 px ;
height : 100 px ;
margin : 15 px ;
padding : 10 px ;
background : green;
border : 1 px 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擁有以下方法來讓我們取得、改變與增刪物件。
Finding:
document.getElementById(id): 根據id尋找。
document.getElementsByTagName(name): 根據標籤名尋找。
document.getElementsByClassName(name): 根據類別名尋找。
More about selectors:
原則上我們使用上述的三個方式應該可以完成所有的節點定位,除此之外還可以使用querySelector與querySelectorAll來做選擇。此二者可以使用CSS selectors的語法來選擇,兩者之差別是querySelector只會找到第一個符合的節點而querySelectorAll則會傳回所有符合的節點。
<section id = "sec" class ="container" value = 10 >
Inside the section
<p > 1</p >
</section >
<div class ="container" >
Inside div
</div >
<article id = "art" class ="container" >
Inside article
</article >
console. log ( document. querySelector( ' .container ' ) ) ; // <section id="sec" class="container" value="10">僅找到第一個
console. log ( document. querySelectorAll( ' .container ' ) ) ; // NodeList(3) [ section#sec.container, div.container, article.container ]找到全部
console. log ( document. querySelector( ' #art ' ) ) ; // <article id="art" class="container">
此外,我們尚可利用Node.childNodes、Node.children、Node.firstChild、Node.lastChild、Node.parentNode、Node.previousSibling、Node.nextSibling等方法來求得對應位置的節點。
部分舊版的指令在html5依然支援。
document.anchors: returns all <a>
document.baseURI
document.body: returns the <body> element
document.cookie
document.doctype
document.documentElement: returns the <html> element
document.documentMode
document.documentURI
document.domain
document.embeds: returns all <embed> elements
document.forms: returns all <form> elements
document.head: returns the <head> element
document.images: returns all <img> elements
document.implementation
document.inputEncoding
document.lastModified
document.links: returns all <area> and <a> elements that have a href attribute
document.readyState
document.referrer
document.scripts: returns all <script> elements
document.strictErrorChecking
document.title: returns the <title> element
document.URL
Some other document properties & Methods:
document.createElement(element): 建立元件。
document.createAttribute(): 建立屬性node。
document.createComment(): 建立comment node。
document.createEvent(): 建立event node。
document.createTextNode(): 建立text node。
document.lastModified: 傳回last modified的時間日期。
document.write(text): 寫上文字。
More about document properties & Methods:
這幾個方法可以讓我們建立新的節點,並做屬性設定。
<article id = "art" class ="container" >
Inside article
</article >
. blueText{
color : blue;
}
. orangeBackground{
background-color : orange;
}
let divNode = document. createElement( ' div ' ) ; // 建立一個div node
let textNode = document. createTextNode( ' String to write ' ) ; // 建立一個text node
let attNode = document. createAttribute( ' class ' ) ; // 建立一個attribute node
attNode. value = ' blueText ' ; // 設定attribute node的值
divNode. setAttributeNode( attNode) ; // 將attNode設為div node的屬性節點
divNode. appendChild( textNode) ; // 將textNode設定為divNode的child,亦即為div內的文字
let art = document. getElementById( ' art ' ) ; // 取得id=art的物件(article)
art. setAttribute( ' class ' , ' orangeBackground ' ) ; // 設定art的class屬性。注意,與setAttributeNode不同。
// 若是欲增加class屬性,可以使用classList.add('className')方式
art. appendChild( divNode) ; // 最後將整個divNode放置於art內(設定為其child)
取得物件element後,可以使用屬性或方法來取得或改變屬性的值。
屬性:
element.accessKey: 設定或傳回element的accesskey。
element.attributes: 傳回element的屬性。
More about attributes:
我們可以在JS中操控元素的屬性。
< a id= "glink" href= "http://www.google.com" > google</ a >
< script>
let goo = document. getElementById( " glink " ) ;
console. log ( goo. hasAttribute( ' href ' ) ) ; // hasAttribute >> 確認是否有某名之attribute
console. log ( goo. getAttribute( ' href ' ) ) ; // getAttribute >> 取得某attribute對應之內容
goo. setAttribute( ' href ' , ' http://www.nkust.edu.tw ' ) ; // 已存在屬性,更新其值
goo. setAttribute( ' target ' , ' _blank ' ) ; // 不存在之屬性,新增
console. log ( goo. getAttribute( ' href ' ) ) ;
goo. removeAttribute( ' target ' ) ; // 移除現有屬性
// -------------- 使用attributes屬性來達成 --------------
console. log ( goo. attributes) ; // NamedNodeMap [ id="glink", href=" http://www.nkust.edu.tw " ]
// NamedNodeMap是一個類似dict的成對資料(key-value)物件,可以根據其key取得其對應之value
goo. attributes[ ' href ' ] . value = ' http://www.youtube.com ' ; // 改變原來屬性的值
// 建立新屬性的方式 >> 對應setAttribute()方法
let tar = document. createAttribute( ' target ' ) ; // 建立一個屬性節點
tar. value = ' _blank ' ; // 設定該屬性節點之值
goo. attributes. setNamedItem( tar) ; // 使用setNamedItem()將屬性節點加入
// 使用for loop歷遍element的所有屬性
console. log ( " -------------- name(not key) vs value -------------- " ) ;
for ( let a = 0 ; a < goo. attributes. length ; ++ a) {
console. log ( goo. attributes[ a] . name + " " + goo. attributes[ a] . value) ;
}
console. log ( " -------------- 另一個for loop -------------- " ) ;
for ( let i of goo. attributes) {
console. log ( i) ;
}
< / script>
element.childElementCount: 傳回element的child數。
element.childNodes: 傳回一個element的child節點(包含文字跟註解)。
element.children: 傳回一個element的child節點(不包含文字跟註解)。
element.classList: 傳回一個element的class name(s)。
element.clientHeight: 傳回一個element的height,包含padding。
element.clientLeft: 傳回一個element左邊邊界的寬。
element.Top: 傳回一個element上方邊界的寬。
element.Width: 傳回一個element的width,包含padding。
element.contentEditable: 設定或傳回element是否是editable。
element.dir: 設定或傳回element的dir屬性之值。
element.firstChild: 傳回element的first child。
element.firstElementChild: 傳回element的first child element。
element.id: 設定或傳回一個element的id。
element.innerHTML: 設定或傳回一個element的html內容。
element.innerText: 設定或傳回一個node及其後代的文字內容。
More about innerHTML & innerText:
這兩個屬性都是可以取得跟修改,不過要注意的是修改時會將原內容蓋過,所以要注意使用。此外,尚可使用textContent,此屬性與innerText類似,不同處是textContent會傳回包含<script>與<style>的內容,且會傳回例如visibility: hidden; 或 display: none;等畫面沒出現的內容。
<section id = "sec" value = 10 >
Inside the section
<p > 1</p >
</section >
console. log ( document. getElementById( " sec " ) . innerHTML) ; // Inside the section <p>1</p>
document. getElementById( " sec " ) . innerHTML = " <b>Whatever " ;
console. log ( document. getElementById( " sec " ) . innerHTML) ; // <b>Whatever</b> , >> 注意,原來的<p>被洗掉了
console. log ( document. getElementById( " sec " ) . innerText) ; // Whatever
document. getElementById( " sec " ) . innerText = " New Text " ;
console. log ( document. getElementById( " sec " ) . innerText) ; // New Text
console. log ( document. getElementById( " sec " ) . innerHTML) ; // New Text
而outerHTML屬性則跟innerHTML類似,只是他連結點本身的HTML都傳回了。
console. log ( document. getElementById( " sec " ) . outerHTML) ;
document. getElementById( " sec " ) . outerHTML = " <p id='p2' style='color: blue;'>A new p</p> " ;
console. log ( document. getElementById( " p2 " ) . outerHTML) ;
注意上例中的section被換成p,且連id都被換了。
element.isContentEditable: 傳回element是否editable。
element.lang: 設定或傳回一個element的lang屬性值。
element.lastChild: 傳回element的last child。
element.lastElementChild: 傳回element的last child element。
element.namespaceURI: 傳回一個element的namespace URI。
element.nextSibling: 傳回一個element的下一個sibling。
element.nextElementSibling: 傳回一個element的下一個sibling element。
element.nodeName: 傳回node的名字。
element.nodeType: 傳回node的型態
More about nodeType:
使用nodeType會傳回一個整數值,下表列出其代表的常數名稱與意義。
Node.ELEMENT_NODE
1
HTML 元素 (Element) 節點,像是 <p>
或 <div>
Node.TEXT_NODE
3
文字 (Text) 或屬性 (Attr) 節點
Node.COMMENT_NODE
8
註解節點 (Comment)
Node.DOCUMENT_NODE
9
根節點 (Document)
Node.DOCUMENT_TYPE_NODE
10
DocumentType 節點,像是 <!DOCTYPE html>
Node.DOCUMENT_FRAGMENT_NODE
11
DocumentFragment 節點
例如:
<section >
<span > Inside the section</span >
<p > 1</p >
<p > 2</p >
<p > 3</p >
</section >
<script>
let sec = document .getElementsByTagName("section" )[0 ];
console .log(sec.firstChild.nodeType);
console .log(sec.children[1 ].nodeType);
console .log(sec.children[3 ].nodeType == Node.ELEMENT_NODE);
</script>
element.nodeValue: 設定或傳回node的值。
More about nodeValue:
傳回節點內容,若是屬性節點則傳回屬性內容。
<section id = "sec" value = 10 >
Inside the section
<p > 1</ p >
<p > 2</ p >
<p > 3</ p >
</ section>
< script>
let s = document. getElementById( ' sec ' ) ;
let f = document. getElementsByTagName( ' form ' ) [ 0 ] ;
console. log ( s. firstChild. nodeValue) ; // Inside the section
console. log ( s. attributes. id. nodeValue) ; // sec
console. log ( s. attributes. value. nodeValue) ; // 10
console. log ( s. children) ; // HTMLCollection { 0: p, 1: p, 2: p, length: 3 }
console. log ( s. children[ 0 ] . firstChild. nodeValue) ; // 1
console. log ( s. childNodes) ; // NodeList(7) [ #text, p, #text, p, #text, p, #text ]
console. log ( s. childNodes[ 3 ] . firstChild. nodeValue) ; // 2
< / script>
此處要注意children與childNodes傳回有所不同。
element.offsetHeight: 傳回element的height,包含padding,border跟scrollbar。
element.offsetWidth: 傳回element的width,包含padding,border跟scrollbar。
element.offsetLeft: 傳回element水平向的offset position。
element.offsetParent: 傳回element對其container的offset position。
element.offsetTop: 傳回element垂直向的offset position。
element.ownerDocument: 傳回root element。
element.parentNode: 傳回element的parent node。
element.parentElement: 傳回element的parent element。
element.previousSibling: 傳回node的前一個sibling node。
element.previousElementSibling: 傳回前一個sibling element。
element.scrollHeight: 傳回element的entire height,包含padding。
element.scrollLeft: 設定或傳回element content水平向scrolled的pixels數值。
element.scrollTop: 設定或傳回element content垂直向scrolled的pixels數值。
element.scrollWidth: 傳回element的entire width,包含padding。
element.style: 設定或傳回element的style。
Hint:
<p id ="ap" >
inside p
</p >
使用style.ooxx可以改變CSS的值,不過在程式碼內容易變得繁瑣使得日後不易維護,因此可以改為修改classList。首先設計相關的CSS:
<style >
.blueText {
color : blue;
}
.orangeBackground {
background-color : orange;
}
</style >
在JS的code中改變classList。
let p = document. getElementById( ' ap ' ) ;
p. classList. add( ' blueText ' ) ;
p. classList. add( ' orangeBackground ' ) ;
console. log ( p. classList) ;
p. classList. remove( ' blueText ' ) ;
此外,我們也可以使用style.cssText屬性來取得或給定其cssText的內容。此方式可以與classList混用。
document. getElementById( ' ap ' ) . style. cssText = ' border:1px solid red; background: pink; ' ;
console. log ( document. getElementById( " ap " ) . style. cssText) ;
也可以取得某元素之對應CSS樣式。
console. log ( p. style) ; // 取得style(不完全,因為不包含<style>與外部CSS style sheet)
console. log ( window. getComputedStyle( p) ) ; // 完全版
element.tabIndex: 設定或傳回element的tabindex屬性值。
element.tagName: 傳回element的tag name。
element.textContent: 設定或傳回node及其後代之文字內容。
element.title: 設定或傳回element的title屬性值。
方法:
element.addEventListener(): 附加event handler到element。
element.appendChild(): 附加一個child到element。
element.blur(): 移除element的焦點。
element.click(): 對element模擬mouse-click。
element.cloneNode(): 複製一個element。
element.compareDocumentPosition(): 比較兩個element的document position。
element.contains(): 是否一個node為另一個node的descendant,傳回boolen。
element.focus(): 給一個element focus。
element.getAttribute(): 傳回element的屬性值。
element.getAttributeNode(): 傳回特定屬性node。
element.hasAttribute(): 傳回element是否有某特定屬性。
element.hasAttributes(): 傳回element是否有任何屬性。:
element.hasChildNodes(): 傳回element是否有任何child nodes。
element.insertAdjacentElement(): 插入一個HTML element至目前element的相對位置(beforebegin, afterbegin, beforeend, afterend)。
element.insertAdjacentHTML(): 插入HTML formatted text至目前element的相對位置。
element.insertAdjacentText(): 插入text至目前element的相對位置。
element.insertBefore(): 插入一個新的child node至一child node之前。
More about insert:
一些關於加入(insert)的用法。
<article id = "art" class ="container" >
Inside article
</article >
< style>
. blueText{
color: blue;
}
. orangeBackground{
background- color: orange;
}
. container{
border: 1px solid red;
}
< / style>
< script>
let art = document. getElementById( " art " ) ; // id=art的article
art. insertAdjacentHTML( ' afterend ' , ' <div class="blueText">A new div.</div> ' ) ; // 在art之後加上新的HTML(div)
let divNode = document. createElement( ' div ' ) ; // 建立新的div node
divNode. setAttribute( ' class ' , ' orangeBackground ' ) ; // 設定div node的屬性
let textNode = document. createTextNode( " some text " ) ; // 建立新的text node
divNode. appendChild( textNode) ; // 將textNode加入至divNode內
art. insertAdjacentElement( ' afterend ' , divNode) ; // 將divNode�"�置在art的後面
divNode. insertAdjacentText( ' beforebegin ' , " Some text to insert " ) ; // 將一些文�-�"�置到divNode的前面
let pNode = document. createElement( ' p ' ) ; // 建立p node
let pText = document. createTextNode( ' Something inside p node ' ) ; // 建立文�-node
pNode. appendChild( pText) ; // 文�-node加入p node
art. insertBefore( pNode, art. childNodes[ 0 ] ) ; // 在art內,將pNode加入至第一個子節點的前面
< / script>
element.isDefaultNamespace(): 是否特定的namespaceURI是default。
element.isEqualNode(): 兩個elements是否相等。
element.isSameNode(): 兩個elements是否是相同node。
element.isSupported(): 是否特定feature在element上被支援。
element.normalize(): 加入相鄰文字節點並移除空白文字節點至一個element。
element.querySelector(): 傳回符合特定CSS selector(s)之element的first child。
element.querySelectorAll(): 傳回符合特定CSS selector(s)之element的所有child elements。
element.removeAttribute(): 移除element的一個特定屬性。
element.removeAttributeNode(): 移除特定屬性的Node,並傳回該node。
element.removeChild(): 移除一個element的child node。
More about remove:
Remove是對應Insert,一樣可以作用在Node、Attribute Node、與Attribute上。
< article id = "art" class= "container" >
Inside article
</ article >
< br >
< button id= "ta" > toggle attribute</ button >
< button id= "tar" > toggle add Remove</ button >
<style >
.blueText {
color : blue;
}
.orangeBackground {
background-color : orange;
}
.container {
border : 1px solid red;
}
</style >
< script >
let art = document. getElementById( " art " ) ; // id=art的article
let divNode = document. createElement( ' div ' ) ; // 建立新的div node
divNode. setAttribute( ' class ' , ' orangeBackground ' ) ; // 設定div node的屬性
let textNode = document. createTextNode( " some text for div " ) ; // 建立新的text node
divNode. appendChild( textNode) ; // 將textNode加入至divNode內
art. insertAdjacentElement( ' afterend ' , divNode) ; // 將divNode放置在art的後面
// toggle add remove attribute
let isContainer = false ;
ta. addEventListener( ' click ' , ( ) = > {
if ( isContainer) {
art. setAttribute( ' class ' , ' container ' ) ;
isContainer = false ;
} else {
art. removeAttribute( ' class ' ) ;
isContainer = true ;
}
} )
// toggle add remove child
let isRemoved = false ;
tar. addEventListener( ' click ' , ( ) = > {
if ( isRemoved) {
art. insertAdjacentElement( ' afterend ' , divNode) ;
isRemoved = false ;
} else {
document. getElementsByTagName( ' body ' ) [ 0 ] . removeChild( divNode) ;
isRemoved = true ;
}
} )
</ script >
element.removeEventListener(): 移除一個element的一個event handler。
element.replaceChild(): 替換一個element的child node。
element.scrollIntoView(): Scrolls某一element至視窗的可見區域。
element.setAttribute(attribute, value): 設定某屬性之值。
element.setAttributeNode(): 設定或改變某一屬性之node。
element.toString(): 傳回代表某一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
顯然跟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
記得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) ;
}
click
按下按鈕後可以發現兩者皆得到相同的文字,對於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
Button C
Button D
此時按下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 " ;
}
click
跟之前使用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表示由外而內。
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 : 74 px ; height : 45 px ; background-color : pink; " >
< button id = "inner" style= " position : absolute; left : 20 px ; top : 20 px " > 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 : 20 px ; top : 20 px " > 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);
})
</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++){
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'
let ww = document .getElementById('wlist' );
console .log(ww.childNodes);
for (let ele of ww.children){
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" )
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
AJAX是Asynchronous JavaScript And XML的簡稱,AJAX並不是一個程式語言,所以不能單獨使用,僅能跟browser內建的XMLHttpRequest物件合用,或是JavaScript跟HTML DOM並用。雖然根據名稱是使用XML來傳遞資訊,事實上一樣可以使用純文字或是JSON。使用AJAX可以傳遞要求(Request)給伺服器,也可以接受來自伺服器的訊息。
How it works?
<!DOCTYPE html>
< html >
< head >
< title > AJAX html</ title >
< meta http-equiv= "Content-Type" content= "text/html; charset=utf-8" >
</ head >
< body >
< div id = "div1" >
< h3 > Change Text</ h3 >
< button type= "button" onclick= "loadDoc()" > Change</ button >
</ div >
< script >
function loadDoc( ) {
var count = 0 ;
var xhttp = new XMLHttpRequest( ) ;
xhttp. onreadystatechange = function ( ) {
//console.log(count); Different browser has different response >> FireFox is OK. Chrome is not.
//count++;
if ( this . readyState == 4 && this . status == 200 ) {
document. getElementById( " div1 " ) . innerHTML = this . responseText;
}
}
xhttp. open( " GET " , " ajax_1_info.txt " , true ) ;
//xhttp.open("GET", " http://www2.nkfust.edu.tw/~shanhuen/JavaScript/ajax_1.json ", true);
//xhttp.open("GET", " http://localhost/WorkSpace/ajax1_info.txt ", true);
xhttp. send( ) ;
}
</ script >
</ body >
</ html >
在網頁(web page)產生一個事件(event),e.g. page loaded or button clicked。
使用JavaScript建立一個XMLHttpRequest物件。
XMLHttpRequest物件傳送一個需求(request)給伺服器(web server)。
伺服器處理需求後,傳回回應(response)給網頁。
JavaScript接收回應然後進行動作(e.g. 更新網頁)。
使用open()時,第一個參數可使用"GET"或"POST",GET比較簡單且快速,用於大多數的情況。使用POST的時機一般為
不使用cached file(e.g. 更新伺服器上的檔案)
傳送大資料到伺服器(POST沒有大小限制)
傳送使用者輸入的資料(可能包含未知字元)
open()的第二個參數為資料名稱與位置,第三個參數表示是否使用asynchronous(使用true)。
open()之後須send()才會傳送。
伺服器傳回回應時,onreadystatechange是定義一個函數,當readyState改變時呼叫。
readyState紀錄XMLHttpRequest的狀態,狀態定義如下:
0: request not initialized
1: server connection established
2: request received
3: processing request
4: request finished and response is ready
status與statusText兩性質紀錄XMLHttpRequest物件的狀態(See List of HTTP status codes or HTTP Status Messages for more details.)。
200: "OK"
403: "Forbidden"
404: "Page not found"
statusText傳回描述文字,e.g. "OK" or "Not Found"
使用Chrome需與伺服器連結,所以首先先打開XAMPP Control Panel,開啟Apache,讓電腦成為主機。將檔案ajax1_info.txt複製到C:\xampp\htdocs\WorkSpace(php的workspace)。
Callback Function
在呼叫時使用callback function,可產生不同事件
: 讓callback function 成為事件的參數,不同的事件呼叫不同的function。利用這個方式,可以顯示不同資料來源的內容,之後僅要更新資料來源即可。
Ajax_2_callback.html
<!DOCTYPE html>
<html >
<head >
<title > AJAX</title >
<meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" >
</head >
<body >
<h3 > Change Text</h3 >
<button type ="button"
onclick ="loadDoc('http://www2.nkfust.edu.tw/~shanhuen/JavaScript/ajax_1.json', fun1)" >
Get JSON</button >
<button type ="button"
onclick ="loadDoc('http://www2.nkfust.edu.tw/~shanhuen/JavaScript/ajax1_info.txt', fun2)" >
Get TXT</button >
<div id = "div1" >
</div >
<script >
function loadDoc (url, func ) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function ( ) {
if (this .readyState == 4 && this .status == 200 ){
func(this );
}
};
xhttp.open("GET" , url, true );
xhttp.send();
}
function fun1 (xhttp ) {
document .getElementById("div1" ).innerHTML = xhttp.responseText;
}
function fun2 (xhttp ) {
document .getElementById("div1" ).innerHTML = xhttp.responseText;
}
</script >
</body >
</html >
deal XML
<!DOCTYPE html>
< html >
< head >
< title > AJAX XML</ title >
< meta http-equiv= "Content-Type" content= "text/html; charset=utf-8" >
< style >
table , th , td {
border : 1 px solid blue;
border-collapse : collapse;
}
th , td {
padding : 3 px ;
}
th {
background : tomato;
color : gold;
}
</ style >
</ head >
< body onload= "loadDoc('http://www2.nkfust.edu.tw/~shanhuen/JavaScript/Ajax3.xml', showChar)" >
< h3 > WOW</ h3 >
<!--button type="button" onclick="loadDoc(' http://www2.nkfust.edu.tw/~shanhuen/JavaScript/Ajax3.xml ', showChar)">Characters</button-->
< br > < br >
< div id = "div1" >
</ div >
< script >
function loadDoc( url, func) {
var xhttp = new XMLHttpRequest( ) ;
xhttp. onreadystatechange = function ( ) {
if ( this . readyState == 4 && this . status == 200 ) {
func( this ) ;
}
} ;
xhttp. open( " GET " , url, true ) ;
xhttp. send( ) ;
}
function showChar( xhttp) {
var xmlDoc = xhttp. responseXML;
var table = " <table><tr><th>occupation</th><th>level</th><th>side</th><th>race</th><th>gender</th><th>weapon</th></tr> " ;
var x = xmlDoc. getElementsByTagName( " role " ) ;
for ( let i = 0 ; i < x. length ; i++ ) {
table += " <tr><td> " + x[ i] . getElementsByTagName( " occupation " ) [ 0 ] . childNodes[ 0 ] . nodeValue + " </td><td> " +
x[ i] . getElementsByTagName( " level " ) [ 0 ] . childNodes[ 0 ] . nodeValue + " </td><td> " +
x[ i] . getElementsByTagName( " side " ) [ 0 ] . childNodes[ 0 ] . nodeValue + " </td><td> " +
x[ i] . getElementsByTagName( " race " ) [ 0 ] . childNodes[ 0 ] . nodeValue + " </td><td> " +
x[ i] . getElementsByTagName( " gender " ) [ 0 ] . childNodes[ 0 ] . nodeValue + " </td><td> " +
x[ i] . getElementsByTagName( " weapon " ) [ 0 ] . childNodes[ 0 ] . nodeValue + " </td></tr> " ;
}
table += " </table> " ;
document. getElementById( " div1 " ) . innerHTML = table;
}
</ script >
</ body >
</ html >
使用XMLHttpRequest物件的responseXML取得response xml DOM物件(responseText則傳回response字串),並使用其getElementsByTagName()取得xml中的tag。
使用getElementsByTagName("role")來取得所有的role,並存成一個array。
JSONP
JSONP是另一個取得伺服器資料的方式,而且此方式不使用XMLHttpRequest物件且不用考慮跨域的問題。
<?php
$warrior = '{"name":"Warr", "level":20, "side":"Aliance", "gender": "Male", "race":"Human"}' ;
echo "jFunc(" . $warrior . ");"
?>
將此檔案放置於C:\xampp\htdocs\WorkSpace資料夾中,也就是說放在伺服器(使用本機當作伺服器),之後的html檔案要到此讀取資料。
此php內儲存一個物件warrior,記得需在XAMPP Control Panel中activate Apache主機。
接下來建立以下的html檔案。
Ajax_4_jsonp.html
<!DOCTYPE html>
< html >
< head >
< title > JSONP</ title >
< script >
function jFunc( obj) {
document. getElementById( " div1 " ) . innerHTML = obj. name;
}
var script = document. createElement( ' script ' ) ;
script. src = ' http://XXX.XXX.X.XXX/workspace/Ajax_4_jsonp.php ' ;
//document.getElementsByTagName('head')[0].appendChild(script);
document. querySelector( ' head ' ) . appendChild( script) ;
</ script >
</ head >
< body >
< div id= "div1" >
</ div >
</ body >
</ html >
XXX.XXX.X.XXX是你的電腦IP位址,你可以使用cmd打開DOS,在其中輸入ipconfig指令,會看到IPv4位址。
將檔案執行(或是上傳至你的html主機再執行),此時雖在不同網域,但仍能取得php檔案內的資料,在html內顯示得到的資料。
定義函數jFunc(obj),此於php檔內的設定要呼叫的函數名稱相同。
使用document.getElementsByTagName('head')[0]與document.querySelector('head')效果相同。
之後只要修改主機內的資料(php),顯示端的網頁內容就可跟著改變,與AJAX效果相同。
use JQuery
也可以使用JQuery來處理上述的Ajax與JSONP
jFunc(
{
" name " : " Warr " , " level " : 20 , " side " : " Aliance " , " gender " : " Male " , " race " : " Human "
}
)
主要還是名稱訂為jFunc,之後才能使用。
請記得將檔案儲存於C:\xampp\htdocs\WorkSpace資料夾中,並開啟XAMPP之Apache伺服器。
接下來建立以下的html檔案。。
Ajax_4_json_jquery.html
<!DOCTYPE html>
< html >
< head >
< title > JSONP</ title >
< script src= "https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js" > </ script >
< script >
function jFunc( obj) {
var json = JSON. stringify ( obj) ;
//document.getElementById("div1").innerHTML = obj.name + "<br>" + obj.level;
document. getElementById( " div1 " ) . innerHTML = json;
}
$. ajax( { // You can have only url & dataType
type: " get " ,
async: false ,
url: " http://XXX.XXX.X.XXX/workspace/Ajax_4_jsonp_jquery.php?callback=? " ,
dataType: " jsonp " ,
jsonCallback: " jFunc " ,
} ) ;
</ script >
</ head >
< body id= "body" >
<!--button id = "but">Click</button-->
< div id= "div1" >
</ div >
</ body >
</ html >
因為要使用JQuery,所以要記得先納入標頭檔。接著直接使用.ajax並標明type, async, url, dataType, jsonCallback等參數即可。也可以只給url與dataType資訊,其他由程式自行產生。
也可以使用.getJSON來取得伺服器中的JSON資料,如下所示。
Ajax_4_json_jquery1.html
<!DOCTYPE html>
< html >
< head >
< title > JSONP</ title >
< script src= "https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js" > </ script >
< script >
function jFunc( obj) {
var json = JSON. stringify ( obj) ;
//document.getElementById("div1").innerHTML = obj.name + "<br>" + obj.level;
document. getElementById( " div1 " ) . innerHTML = json;
}
$. getJSON(
" http://XXX.XXX.X.XXX/workspace/Ajax_4_jsonp_jquery.php?callback=? " ,
) ;
</ script >
</ head >
< body id= "body" >
<!--button id = "but">Click</button-->
< div id= "div1" >
</ div >
</ body >
</ html >
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 ;
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 ;
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 ;
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);
}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);
});
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 ;
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 ;
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);
}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>
fetch跟XMLHttpRequest一樣用來發送與接收伺服器的資料,而且用法簡單(fetch就是一個設計好的Promise)。
首先建立一個要被讀取的檔案內容,Json是好選擇,命名為data.txt如下:
{ " Name " : " Jenny " , " Occupation " : " Warrior " , " Level " : 1 }
之後將資料顯示於console,所以一個空白的html網頁即可,在body內加上javascript檔案連結即可。(也可以直接將javascript的code寫在html檔案內)
< script src= "js1.js" > </ script >
接下來完成javascript檔案(js1.js)內容如下:
fetch( ' http://www2.nkfust.edu.tw/~shanhuen/JAVA_Tutorials/data.txt ' ) // 檔案路徑
. then( function ( response) {
return response. json( ) ;
} )
. then( function ( myJson) {
console. log ( myJson)
} )
。
將上列三個檔案上傳到伺服器的對應路徑內,接著打開網頁即可看到Json內容,完成讀取伺服器檔案資料。
上例中的response物件代表返回的回應。
response物件有一些相關屬性,如headers、ok、status、statusText、type、url,看個例子:
fetch( ' http://www2.nkfust.edu.tw/~shanhuen/JAVA_Tutorials/data.txt ' )
. then( function ( response) {
if ( response. status === 200 ) {
console. log ( ` ok = ${ response . ok } ` ) ; // true
console. log ( ` status text = ${ response . statusText } ` ) ; // OK
console. log ( ` type = ${ response . type } ` ) ; // basic
console. log ( ` url = ${ response . url } ` ) ; // url
return response. json( ) ;
} else {
console. log ( " Something wrong: " + response. status + " " + response. statusText) ;
}
} )
. then( function ( text) {
console. log ( text) ;
} )
status、ok、與statusText表示連線狀態,status = 200表示成功(ok),400+表示錯誤(e.g. 404表示Not Found,試著將上例的data.txt改為data1.txt)。可參考HTTP 狀態碼 。
response物件除了json()之外,還有其他方法,例如text()、blob()、arrayBuffer、formData()等,例如將上例的資料用文字傳回:
fetch( ' http://www2.nkfust.edu.tw/~shanhuen/JAVA_Tutorials/data.txt ' )
. then( function ( response) {
if ( response. status === 200 ) {
console. log ( ` ok = ${ response . ok } ` ) ; // true
console. log ( ` status text = ${ response . statusText } ` ) ; // OK
console. log ( ` type = ${ response . type } ` ) ; // basic
console. log ( ` url = ${ response . url } ` ) ; // url
return response. text( ) ; // << get the text
} else {
console. log ( " Something wrong. " + response. status + " " + response. statusText) ;
}
} )
. then( function ( text) {
console. log ( text) ;
} )
此時得到的是文字而不是json 。
blob()是取得非結構化物件資料(raw data):
fetch( ' http://www2.nkfust.edu.tw/~shanhuen/css/images/img1.jpg ' )
. then( function ( response) {
return response. blob( ) ; // << get the image
} )
. then( function ( blob) {
console. log ( " obtain the image " ) ;
let imgNode = document. createElement( " img " ) ;
let objectURL = URL. createObjectURL( blob) ;
imgNode. src = objectURL;
document. getElementsByTagName( " body " ) [ 0 ] . appendChild( imgNode) ;
} )
. catch ( function ( error) {
console. log ( error) ;
} )
。
fetch連結方式的預設值是get,若是要使用post,則要設定第二個參數,例如:
html內容:
< form id= "postData" >
< div >
< label > title</ label > < br >
< input type= "text" name= "" id= "title" >
</ div >
< div >
< label > content</ label > < br >
< textarea name= "" id= "body" cols= "20" rows= "5" > </ textarea >
</ div >
< input type= "submit" value= "Post" >
</ form >
< script src= "js1.js" > </ script >
原則上是建立一個表單跟一個按鈕,按下按鈕後執行fetch。javascript code如下:
let title = document. getElementById( ' title ' ) . value;
let body = document. getElementById( ' body ' ) . value;
document. getElementById( ' postData ' ) . addEventListener( ' submit ' , event = > {
event. preventDefault( ) ;
fetch( ' https://jsonplaceholder.typicode.com/posts ' , {
method: ' POST ' ,
headers: {
" Content-type " : " application/json; charset=UTF-8 "
} ,
body: JSON. stringify ( { title: title, body: body, userId: 1 } )
} )
. then( function ( response) {
return response. json( ) ;
} )
. then( function ( jsondata) {
console. log ( jsondata) ;
} )
. catch ( function ( error) {
console. log ( error) ;
} )
} ) ;
event.preventDefault()的意思是取消事件的預設行為,但不影響事件傳遞,事件仍會繼續傳遞。
fetch的第一個參數是JSONPlaceholder 的網址,此網站可讓我們使用虛假資料。
因為使用post,所以需要給定fetch的第二個參數來設定method、header與body,需要注意的是body要給字串,所以使用json.stringify()方法。類似的做法,以下是官方網站的例子:
fetch( ' https://jsonplaceholder.typicode.com/posts ' , {
method: ' POST ' ,
headers: new Headers( ) ,
body: JSON. stringify ( {
title: ' foo ' ,
body: ' bar ' ,
userId: 1
} ) ,
headers: {
" Content-type " : " application/json; charset=UTF-8 "
}
} )
. then( function ( response) {
return response. json( ) ;
} )
. then( function ( jsondata) {
console. log ( jsondata) ;
} )
XML
XML與JSON都是用來儲存與傳遞交換資料的資料格式。
XML
XML類似HTML,都是使用標籤,但是XML的標籤可以自己定義。
: XML須有一個標籤作為root,其內的標籤稱為element(在<tag>與</tag>之內的元件)。element可以為空(empty),也可以包含其他element(nested)。 。
xml1_xml.xml
<? xml version = " 1.0 " encoding = " UTF-8 " ?>
< wow >
< role >
< occupation > Warrior</ occupation >
< level > 10</ level >
< side > Alliance</ side >
< race > Dwarf</ race >
< gender > Male</ gender >
< weapon >
< lefthand > Sword</ lefthand >
< righthand > Axe</ righthand >
</ weapon >
</ role >
< role >
< occupation > Mage</ occupation >
< level > 20</ level >
< side > Alliance</ side >
< race > Human</ race >
< gender > Female</ gender >
< weapon >
< lefthand > Lamp</ lefthand >
< righthand > Wand</ righthand >
</ weapon >
</ role >
</ wow >
第一行<?xml version="1.0" encoding="UTF-8"?>是optional,可以不寫。
標籤可以有屬性(attribute),e.g. <role language="human-language">,也可以將其變成role內的一個element,可自行決定。
: 之前已提到可以使用HttpRequest 來取得伺服器中的XML檔案內容,取得資料後可以parse然後得到我們想要的資訊。假定已經取得xml資料字串,依以下方式parse。
xml1_parse.html
<!DOCTYPE html>
< html >
< head >
< title > XML Parse</ title >
< meta http-equiv= "Content-Type" content= "text/html; charset=utf-8" >
</ head >
< body >
< h3 > WOW</ h3 >
< div id = "div1" >
</ div >
< script >
var text = " <wow><role> " +
" <occupation>Warrior</occupation> " +
" <level>10</level> " +
" <side>Alliance</side> " +
" <race>Dwarf</race> " +
" <gender>Male</gender> " +
" <weapon> " +
" <lefthand>Sword</lefthand> " +
" <righthand>Axe</righthand> " +
" </weapon> " ;
var parser = new DOMParser( ) ;
var xmlDoc = parser. parseFromString( text, " text/xml " ) ;
document. getElementById( " div1 " ) . innerHTML = xmlDoc. getElementsByTagName( " occupation " ) [ 0 ] . childNodes[ 0 ] . nodeValue;
//document.getElementById("div1").innerHTML = xmlDoc.getElementsByTagName("weapon")[0].childNodes[1].childNodes[0].nodeValue; // Axe
</ script >
</ body >
</ html >
script寫在</body>之前,才讀取得到。
text是取得的資料字串,建立DOMParser()物件,然後使用parseFromString(text, "text/xml")方法parse。
使用getElementsByTagName("occupation")取得tag,跟之前提到的DOM一樣,此標籤的childNode[]為其中之文字node,取得其nodeValue即為文字內容。
若使用getElementsByTagName("weapon")[0],表示得到weapon標籤,其下的childNodes[1]為righthand標籤,再其下的childeNodes[0]則為文字node。
: 以下的例子為使用HttpRequest 取得XML資料來parse然後顯示,亦可參考Ajax 的說明。
xml1_parse1.html
<!DOCTYPE html>
<html >
<head >
<title > XML Parse</title >
<meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" >
</head >
<body >
<h3 > WOW</h3 >
<div id = "div1" >
</div >
<script >
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function ( ) {
if (this .readyState == 4 && this .status == 200 ) {
var text = xhttp.responseText;
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(text, "text/xml" );
document .getElementById("div1" ).innerHTML = xmlDoc.getElementsByTagName("occupation" )[0 ].childNodes[0 ].nodeValue
+ "<br><span style=\"color:blue;\">"
+ xmlDoc.getElementsByTagName("lefthand" )[0 ].childNodes[0 ].nodeValue
+ " " +xmlDoc.getElementsByTagName("righthand" )[0 ].childNodes[0 ].nodeValue
+ "</style>" ;
}
};
xhttp.open("GET" , "http://www2.nkfust.edu.tw/~translab/xml1_xml.xml" , true );
xhttp.send();
</script >
</body >
</html >
在此直接取得lefthand與righthand標籤來得到其中的資料。
: 在xml1_xml.xml檔案中包含兩個role,在選取xmlDoc.getElementsByTagName()會得到哪一個?原則上可以得到一個陣列,以下例說明。
xml1_parse2.html
<!DOCTYPE html>
< html >
< head >
< title > XML Parse</ title >
< meta http-equiv= "Content-Type" content= "text/html; charset=utf-8" >
</ head >
< body >
< h3 > WOW</ h3 >
< div id = "div1" >
</ div >
< script >
var xhttp = new XMLHttpRequest( ) ;
xhttp. onreadystatechange = function ( ) {
if ( this . readyState == 4 && this . status == 200 ) {
var text = xhttp. responseText;
var parser = new DOMParser( ) ;
var xmlDoc = parser. parseFromString( text, " text/xml " ) ;
var x = xmlDoc. getElementsByTagName( " occupation " ) ;
var t = " " ;
for ( let i = 0 ; i < x. length ; i++ ) {
t += x[ i] . childNodes[ 0 ] . nodeValue + " <br> " ;
//t += x[i].childNodes[5].childNodes[0].nodeValue + "<br>"; // var x = xmlDoc.getElementsByTagName("role");
}
document. getElementById( " div1 " ) . innerHTML = t;
}
} ;
xhttp. open( " GET " , " http://www2.nkfust.edu.tw/~translab/xml1_xml.xml " , true ) ;
xhttp. send( ) ;
</ script >
</ body >
</ html >
在前例中,若改為取得getElementsByTagName("occupation")[1],則會得到mage,若是siblings數量較大,則可以使用for loop來traverse,因此使用xmlDoc.getElementsByTagName("occupation")來得到所有occupation的陣列。
也可以使用xmlDoc.getElementsByTagName("role")來得到role的陣列,然後使用DOM來traverse(會比較複雜),例如x[i].childNodes[5]會得到第3個child(1,3,5,...)。
: 類似的element(siblings)可以將其定義為不同的namespace ,此時element的tag便視為不同了,e.g. xml2.xml。
xml2.xml
<? xml version = " 1.0 " encoding = " UTF-8 " ?>
< wow >
< w : role >
< w : occupation > Warrior</ w : occupation >
< w : level > 10</ w : level >
< w : side > Alliance</ w : side >
< w : race > Dwarf</ w : race >
< w : gender > Male</ w : gender >
< w : weapon >
< w : lefthand > Sword</ w : lefthand >
< w : righthand > Axe</ w : righthand >
</ w : weapon >
</ w : role >
< m : role >
< m : occupation > Mage</ m : occupation >
< m : level > 20</ m : level >
< m : side > Alliance</ m : side >
< m : race > Human</ m : race >
< m : gender > Female</ m : gender >
< m : weapon >
< m : lefthand > Lamp</ m : lefthand >
< m : righthand > Wand</ m : righthand >
</ m : weapon >
</ m : role >
</ wow >
此xml中w:role與m:role為兩個不同的tag,其下的element也是位於不同的namespace。使用下例來parse。
xml1_parse3.html
<!DOCTYPE html>
< html >
< head >
< title > XML Parse</ title >
< meta http-equiv= "Content-Type" content= "text/html; charset=utf-8" >
</ head >
< body >
< h3 > WOW</ h3 >
< div id = "div1" >
</ div >
< script >
var xhttp = new XMLHttpRequest( ) ;
xhttp. onreadystatechange = function ( ) {
if ( this . readyState == 4 && this . status == 200 ) {
var text = xhttp. responseText;
var parser = new DOMParser( ) ;
var xmlDoc = parser. parseFromString( text, " text/xml " ) ;
var x = xmlDoc. getElementsByTagName( " w:role " ) ;
var t = " " ;
for ( let i = 0 ; i < x. length ; i++ ) {
//t += x[i].childNodes[0].nodeValue + "<br>";
t += x[ i] . childNodes[ 1 ] . childNodes[ 0 ] . nodeValue + " <br> " ; // var x = xmlDoc.getElementsByTagName("role");
}
document. getElementById( " div1 " ) . innerHTML = t;
}
} ;
xhttp. open( " GET " , " http://www2.nkfust.edu.tw/~translab/xml2.xml " , true ) ;
xhttp. send( ) ;
</ script >
</ body >
</ html >
在此例中使用xmlDoc.getElementsByTagName("w:role"),所以僅會得到namespace為w的element,所以最後印出所有在此namespace內的role occupation。
XSLT
XSLT(eXtensible Stylesheet Language Transformations)可用來將XML轉換成HTML,所以可以用來顯示XML的內容。XSLT比CSS來得精細,不只可以設計style,還可以增加移除元件,或是重新排列元件。
: XSL是Style Sheets for XML的意思,而XSLT是XSL Transformations,是XSL最重要的部分。以下例子使用xslt1.xml 為例,此檔案內容與Ajax3.xml內容相同。
xslt1.xsl
<? xml version = " 1.0 " encoding = " UTF-8 " ?>
< xsl : stylesheet version= " 1.0 " xmlns : xsl= " http : // www.w3.org /1999/XSL/Transform " >
< xsl : template match= " / " >
< html >
< body >
< h2 > WOW</ h2 >
< table border= " 1 " >
< tr bgcolor= " tomato " >
< th > Occupation</ th >
< th > Level</ th >
< th > Side</ th >
< th > Race</ th >
< th > Gender</ th >
< th > Weapon</ th >
</ tr >
< xsl : for-each select= " wow/role " >
< tr >
< td > < xsl : value-of select= " occupation " /> </ td >
< td > < xsl : value-of select= " level " /> </ td >
< td > < xsl : value-of select= " side " /> </ td >
< td > < xsl : value-of select= " race " /> </ td >
< td > < xsl : value-of select= " gender " /> </ td >
< td > < xsl : value-of select= " weapon " /> </ td >
</ tr >
</ xsl : for-each >
</ table >
</ body >
</ html >
</ xsl : template >
</ xsl : stylesheet >
因為要將xslt1.xsl的顯示設定連結至xslt1.xml,所以在xslt1.xml的最上方須加上這一行<?xml-stylesheet type="text/xsl" href="xslt1.xsl"?>。而因為xslt.xsl是一種XML文件,所以可加上<?xml version="1.0" encoding="UTF-8"?>。
將兩檔案上傳至伺服器,然後開啟xslt1.xml即可。
在xslt1.xsl中,需有根標籤(root)為<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">或<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">。
XSLT內包含一或多個顯示準則,稱之為templates。使用xsl:template標籤來實現,其match屬性是用來連結一個template一個XML element,match="/"表示定義整個文件。template內定義一些HTML來顯示資料。
<xsl:value-of select="occupation" />中的value-of為select屬性所對應的標籤資料,也可以定義其路徑(XPath),e.g. select="/wow/role/occupation"
。
XPath是XSLT標準中的主要元件,可用來找到elements and attributes。
語法是使用"/"來自root node開始選擇,"//"是從目前的node來選擇,"."表示選擇目前的node,".."表示選擇目前node的parent,"@"表示選擇屬性,"*"是wild card,匹配所有的element node,"@*"對應任何有屬性的點,"node()"表示任何的點。
"/wow/role[1]"表示選擇所有的wow的子節點中的第一個role節點,"/wow/role[last()]"表示最後一個role節點,"/wow/role[position()<3]則表示選擇前兩個。"
使用for-each來選擇每一個element,可以使用選擇條件,e.g. <xsl:for-each select="wow/role[occupation='warrior']">。
若想針對其中的某屬性排序,可使用e.g. <xsl:sort select="level"/>,將此行加入到<xsl:for-each select="wow/role">之後一行然後觀察結果。
也可以使用if來做判斷,e.g. <xsl:if test = "level > 22"> or <xsl:if test = "gender = 'Female'">。
< xsl : for-each select= " wow/role " >
< xsl : sort select= " level " />
< xsl : if test = " level > 22 " >
< tr >
< td > < xsl : value-of select= " ./occupation " /> </ td >
< td > < xsl : value-of select= " level " /> </ td >
< td > < xsl : value-of select= " side " /> </ td >
< td > < xsl : value-of select= " race " /> </ td >
< td > < xsl : value-of select= " gender " /> </ td >
< td > < xsl : value-of select= " weapon " /> </ td >
</ tr >
</ xsl : if >
</ xsl : for-each >
還可以使用choose(when...otherwise)來進行較複雜的條件選擇,例如將其中部分程式碼改為如下。
< xsl : for-each select= " wow/role " >
< xsl : sort select= " level " />
< xsl : if test = " level > 22 " >
< tr >
< td > < xsl : value-of select= " ./occupation " /> </ td >
< td > < xsl : value-of select= " level " /> </ td >
< xsl : choose >
< xsl : when test= " side='Alliance' " >
< td bgcolor= " blue " > < xsl : value-of select= " side " /> </ td >
</ xsl : when >
< xsl : otherwise >
< td bgcolor= " red " > < xsl : value-of select= " side " /> </ td >
</ xsl : otherwise >
</ xsl : choose >
< td > < xsl : value-of select= " race " /> </ td >
< td > < xsl : value-of select= " gender " /> </ td >
< td > < xsl : value-of select= " weapon " /> </ td >
</ tr >
</ xsl : if >
</ xsl : for-each >
JSON
JSON(JavaScript Object Notation)也是用來儲存跟交換資料,原則上內容就是文字,但其可以輕易地轉換成JavaScript物件。JSON跟XML的不同處是JSON不使用end tag,所以內容較為簡短,且可以較快的讀取。最大的不同是XML必須使用XML parser來parse,而JSON可以經由標準的JavaScript函數來parse。
JSON的內容格式類似Python的dict或是Java的Map,原則上就是key(name)跟value的資料對。當我們得到JSON的資料字串(如前所述之JSONP),我們可以輕易地使用JSON.parse() 函數來parse資料。將JSON資料parse成JavaScript物件後,便可以以物件的方式操作。e.g. 使用obj.occupation或obj["level"]來取得資料,或是obj.race = "Night-elf";(obj["race"] = "Night-elf";)來修改資料。
json1.html
< div id = "div1" />
< script >
var json = ' {"occupation":"Warrior", "level": 20, "race": "Human"} ' ;
var obj = JSON. parse ( json) ;
obj. race = " Night-elf " ;
document. getElementById( " div1 " ) . innerHTML = obj. occupation + " " + obj[ " level " ] + " " + obj. race;
</ script >
JSON的格式與JavaScript物件格式幾乎一模一樣,JSON的key(name)需要使用雙引號包覆,所以必須是字串,而在JavaScript的物件中,key可以是字串或是其他,且不需要使用雙引號包覆。例如:
var character = { occupation: " Warrior " , level: 20 , race: " Human " } ; //Javascript object
JSON的value可以是string, number, object(JSON object), array, boolean, null,而JavaScript物件除了上述的內容,還可以是function, date, undefined 。JSON中的value若是字串需使用雙引號包覆,而JavaScript則可以使用單引號。例如Json:
var characterdata = ` {
"occupation":"Warrior",
"level": 20,
"race": "Human",
"weapon": {"type":"Sword", "Power": 100},
"defence": [
{"cap": 20, "color": "grey"},
{"chained armor": 36, "color": "blue"},
{"short pant": 30, "color": "black"}
]
} `
var character = JSON. parse ( characterdata) ;
console. log ( character. occupation) ;
console. log ( character. weapon) ;
console. log ( character. defence) ;
defence為一array,其內每一個元素皆為一個jason物件。而Javascript物件如下:
var characterdata = {
occupation: " Warrior " ,
level: 20 ,
race: " Human " ,
weapon: { type: " Sword " , Power: 100 } ,
toString: function ( ) {
return ` ${ this . occupation } ${ this . weapon . Power } ` ;
}
}
console. log ( characterdata. toString ( ) ) ;
上述是將JSON變成物件,若是要將物件變成JSON物件字串,則使用stringify 函數。
json2.html
< div id = "div1" > </ div >
< div id = "div2" > </ div >
< script >
var obj = { occupation: " Warrior " , level: 20 , race: " Human " , time: new Date( ) } ;
var json1 = JSON. stringify ( obj) ;
document. getElementById( " div1 " ) . innerHTML = json1;
var array = [ " Warrior " , 20 , " Human " ] ;
var json2 = JSON. stringify ( array) ;
document. getElementById( " div2 " ) . innerHTML = json2;
</ script >
stringify可以用在物件或是陣列(也就是說也可以傳遞陣列)。
若是物件中含有method,stringify會將其移除,若是想要留下method,需先將其轉為字串(使用toString()方法)。例如:
var obj = {
occupation: " Warrior " ,
level: 20 ,
race: " Human " ,
time: function ( ) {
return new Date( ) ;
}
} ;
// obj.time = obj.time.toString();
var json1 = JSON. stringify ( obj) ;
console. log ( json1) ;
之前提過JSON物件跟Javascript物件的不同是JSON物件由大括號{}包覆且其key是字串,看一個例子。
json3.html
var rolejson = ` {
"occupation":"Warrior",
"level": 20,
"race": "Human",
"weapon": {"left_hand":"Axe", "right_hand":"Sward"}
} ` ;
var roleobj = JSON. parse ( rolejson) ;
document. getElementById( " div1 " ) . innerHTML = roleobj. weapon. left_hand;
// use for ... in ... to loop through the json object
for ( x in roleobj) {
if ( x== " weapon " ) {
for ( y in roleobj[ x] ) {
document. getElementById( " div2 " ) . innerHTML += ( x + " & nbsp ; & nbsp ; " + y + " & nbsp ; & nbsp ; " + roleobj[ x] [ y] + " <br> " ) ;
}
} else {
document. getElementById( " div2 " ) . innerHTML += ( x + " & nbsp ; & nbsp ; " + roleobj[ x] + " <br> " ) ;
}
}
在此例中JSON包含Nested的資料,若要取得其中資料,可使用roleobj.weapon.left_hand。
因為parse成為JavaScript物件,所以可以使用for loop來iterate,不過在取得物件資料時須使用roleobj[x],不可使用roleobj.x。
若是要刪除物件屬性,使用delete關鍵字(e.g. delete roleboj.weapon.left)。
Data Storage
之前提過可以將資料儲存在cookie,不過cookie僅有4KB空間,除此之外,也可以將資料儲存在空間較大的session storage、local storage等。
Session Storage: 繼承自Storage,與頁面連結,儲存資料會在該頁面(tab)關閉時消失。相關指令如下:
sessionStorage. setItem( " username " , " Tom Cruise " ) ;
console. log ( sessionStorage. getItem ( " username " ) + " \t " + sessionStorage. length ) ;
sessionStorage. removeItem( " username " ) ;
console. log ( sessionStorage. getItem ( " username " ) + " \t " + sessionStorage. key( 1 ) ) ;
length:長度屬性(read only)。
sessionStorage.key(n):傳回第n個值的key。
sessionStorage.setItem(key, value):儲存資料。
sessionStorage.getItem(key):取得資料。
sessionStorage.removeItem(key):刪除資料。
sessionStorage.clear():清理所有資料。
。
Local Storage: 原則上與sessionStorage語法用法相同,不同的是其中的內容須明確刪除(explicitly deleted)才會消失,也就是資料是persistence,頁面關閉之後依然存在。。
localStorage. setItem( " username " , " Tom Cruise " ) ;
localStorage. setItem( " age " , 18 ) ;
for ( i = 0 ; i < localStorage. length ; i++ ) {
console. log ( localStorage. key( i) + " : " + localStorage. getItem ( localStorage. key( i) ) ) ;
}
localStorage. removeItem( " username " ) ;
console. log ( localStorage. getItem ( " username " ) + " \t " + localStorage. key( 0 ) ) ;
做個練習,首先在html檔內加上以下元件:一個文字輸入框,三個按鈕,Restore Auto按鈕回復系統儲存內容,Save按鈕儲存目前內容,而Restored Save則回復至儲存(Save)時狀態。
< textarea name = "" id= "txtarea" cols = "20" rows= '5' > </ textarea >
< br >
< input type= "button" id = "restore_auto" value= "Restore Auto" > </ input >
< input type= "button" id = "save" value= "Save" > </ input >
< input type= "button" id = "restore_saved" value= "Restore Saved" > </ input >
對應的Javascript code如下:
var txtarea = document. getElementById( ' txtarea ' ) ;
window. setInterval ( ( ) = > {
sessionStorage. setItem( " autosave " , txtarea. value) ;
} , 60000 ) ;
document. getElementById( ' restore_auto ' ) . addEventListener( ' click ' , ( event) = > {
if ( sessionStorage. getItem ( " autosave " ) ) {
txtarea. value = sessionStorage. getItem ( " autosave " ) ;
}
} )
document. getElementById( ' save ' ) . addEventListener( ' click ' , ( event) = > {
localStorage. setItem( " saved " , txtarea. value) ;
} )
document. getElementById( ' restore_saved ' ) . addEventListener( ' click ' , ( event) = > {
if ( localStorage. getItem ( " saved " ) ) {
txtarea. value = localStorage. getItem ( " saved " ) ;
}
} )
此地設定每一分鐘系統會自動儲存一次。
瀏覽器會限制可用的記憶體大小,為了避免被惡意使用來耗光使用者的記憶體空間。Storage的建議記憶體大小為5MB。
為了保護使用者隱私,瀏覽器傾向避免第三方接觸Storage,表示僅相同domain能夠接觸storage資訊。
IndexedDB: session storage與local storage可以簡單便利的讓我們儲存小量資料,而IndexedDB可以允許較大量的資料有結構性的儲存,且可快速搜尋(through indexes)並可供離線使用。IndexedDB的工作是設計為非同步的(asynchronously),也就是當操作完成後才執行我們提供的callback函數。
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);
let ctx = canvas.getContext("2d");
</script >
上述程式碼等同以下的html程式碼。
< canvas id= "canvas" width= "2048" height= "1024" style= " border : 1 px dashed # 000000 ; " > </ canvas >
有了畫布之後便可以開始繪圖。
繪製線段(Line)
首先可以設定線的屬性。
ctx.strokeStyle = "blue" ;
ctx.lineWidth = 3 ;
接下來繪製線型。
ctx.beginPath();
ctx.moveTo(0 ,0 );
ctx.lineTo(128 , 128 );
ctx.lineTo(200 , 256 );
ctx.closePath();
ctx.fillStyle = "green" ;
ctx.fill();
ctx.stroke();
再試一個例子:
ctx.strokeStyle = "rgba(128, 128, 128, 0.5)" ;
ctx.beginPath();
ctx.moveTo(100 , 100 );
ctx.lineTo(200 , 200 );
ctx.lineTo(125 , 0 );
ctx.closePath();
ctx.stroke();
設定線端,有round, butt, square三種型式可供選擇。
ctx.lineWidth = 15 ;
ctx.strokeStyle = "#00ffff" ;
ctx.beginPath();
ctx.moveTo(256 , 30 );
ctx.lineTo(300 , 100 );
ctx.lineCap = "round" ;
ctx.stroke();
現在我們可以藉著上面所言的內容,設計函數來快速產生多邊形。
function draw (shape, ss="black", fs="pink", linejoin="round", isEdge=true, isFill=true ) {
const lastPoint = shape.length-1 ;
ctx.strokeStyle = ss;
ctx.beginPath();
ctx.lineJoin = linejoin;
ctx.moveTo(shape[lastPoint][0 ], shape[lastPoint][1 ]);
shape.forEach(point => {
ctx.lineTo(point[0 ], point[1 ]);
});
if (isEdge){
ctx.stroke();
}
if (isFill) {
ctx.fillStyle = fs;
ctx.fill();
}
}
const coords = [[30 , 256 ], [20 ,360 ],[100 , 300 ], [150 , 300 ]];
draw(coords);
draw([[300 , 200 ],[400 , 200 ],[400 , 100 ],[300 , 100 ]], "green" , "olive" , "bevel" , true , false );
繪製矩形(Rectangle)
簡單幾步驟即可繪製矩形。
ctx.beginPath();
ctx.rect(300 , 300 , 100 , 100 );
ctx.stroke();
ctx.fill();
也可以使用以下替代方法:
ctx.strokeStyle = "pink" ;
ctx.lineWidth = 3 ;
ctx.fillStyle = "rgba(100, 200, 300, 0.7)" ;
ctx.fillRect(500 , 100 , 100 , 100 );
ctx.strokeRect(500 , 100 , 100 , 100 );
使用漸層填滿。
ctx.strokeStyle = "black" ;
ctx.lineWidth = 3 ;
let gradient = ctx.createLinearGradient(700 , 100 , 800 , 200 );
gradient.addColorStop(0.15 , "white" );
gradient.addColorStop(0.5 , "green" );
gradient.addColorStop(0.85 , "black" );
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.rect(700 , 100 , 100 , 100 );
ctx.stroke();
ctx.fill();
上例為線型漸層,也可以是圓形漸層。
ctx.strokeStyle = "white" ;
ctx.lineWidth = 3 ;
let gradient2 = ctx.createRadialGradient(800 , 400 , 2 , 800 , 400 , 100 );
gradient2.addColorStop(0.15 , "white" );
gradient2.addColorStop(0.5 , "yellow" );
gradient2.addColorStop(0.8 , "white" );
ctx.fillStyle = gradient2;
ctx.beginPath();
ctx.rect(700 , 300 , 200 , 200 );
ctx.stroke();
ctx.fill();
繪製圓與弧(Circle & Arc)
弧的繪製語法為arc(centerX, centerY, circleRadius, startAngle, endAngle, false)。
ctx.strokeStyle = "white" ;
let gradient3 = ctx.createRadialGradient(1000 , 100 , 2 , 1000 , 100 , 64 );
gradient3.addColorStop(0 , "white" );
gradient3.addColorStop(0.05 , "yellow" );
gradient3.addColorStop(0.25 , "yellow" );
gradient3.addColorStop(0.5 , "yellow" );
gradient3.addColorStop(1 , "white" );
ctx.fillStyle = gradient3;
ctx.beginPath();
ctx.arc(1000 , 100 , 64 , 0 , 2 *Math .PI, false );
ctx.stroke();
ctx.fill();
若是要畫弧。
ctx.strokeStyle = "red" ;
ctx.beginPath();
ctx.arc(1000 , 300 , 64 , 0 , Math .PI/2 , false );
ctx.stroke();
ctx.strokeStyle = "blue" ;
ctx.beginPath();
ctx.arc(1128 , 364 , 128 , Math .PI, Math .PI/2 , true );
ctx.stroke();
畫曲線。
ctx.beginPath();
ctx.moveTo(500 , 300 );
ctx.quadraticCurveTo(685 , 350 , 625 , 480 );
ctx.stroke();
貝茲曲線(Bezier curve)。
ctx.strokeStyle = "brown" ;
ctx.beginPath();
ctx.moveTo(100 , 500 );
ctx.bezierCurveTo(145 , 350 , 200 , 550 , 320 , 450 );
ctx.stroke();
橢圓
語法:ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise)
範例:
let canvas = document .createElement("canvas" );
canvas.setAttribute("width" , "2048" );
canvas.setAttribute("height" , "1024" );
canvas.style.border = "1px dashed red" ;
document .body.appendChild(canvas);
let ctx = canvas.getContext("2d" );
ctx.beginPath();
ctx.lineWidth = 10 ;
ctx.strokeStyle = "blue" ;
ctx.fillStyle = "gold" ;
ctx.ellipse(x=200 , y=200 , radiusX=100 , radiusY=200 , rotation=Math .PI*30 /180 ,
startAngle=0 , endAngle=Math .PI*2 , counterclockwise=false )
ctx.stroke();
ctx.fill();
效果
陰影。
ctx.shadowColor = "rgba(128, 128, 128, 0.9)" ;
ctx.shadowOffsetX = 10 ;
ctx.shadowOffsetY = 10 ;
ctx.shadowBlur = 10 ;
ctx.fillStyle = "green" ;
ctx.beginPath();
ctx.rect(1200 , 10 , 80 , 80 );
ctx.stroke();
ctx.fill();
旋轉與縮放(Rotate & Scale)。
ctx.save();
ctx.translate(100 , 600 );
ctx.rotate(0.5 );
ctx.scale(1 ,1 );
ctx.globalAlpha = 0.6 ;
ctx.beginPath();
ctx.rect(-50 ,-50 ,100 ,100 );
ctx.stroke();
ctx.fill();
ctx.restore();
混色與交疊效果(Blend Modes & Compositing effects):混色表示兩個交疊物件之交疊部分的顏色顯示。交疊效果指的是交疊物件如何組合的效果。都是修改此參數內容(ctx.globalCompositeOperation)。
ctx.globalAlpha=0.5 ;
ctx.globalCompositeOperation = "xor" ;
ctx.save();
draw([[125 ,600 ],[125 ,700 ],[225 ,700 ],[225 , 600 ]],"pink" ,"tomato" ,"bevel" );
ctx.restore();
ctx.save();
draw([[145 , 650 ],[145 , 750 ],[245 , 750 ],[245 , 650 ]],"blue" , "brown" , "bevel" );
ctx.restore();
圖片
可以使用createPattern方法來使用圖片填滿圖形。
let img = new Image();
img.addEventListener("load" , loadHandler, false );
img.src = "flower2_1.jpg" ;
function loadHandler ( ) {
ctx.strokeStyle = "black" ;
ctx.lineWidth = 3 ;
ctx.beginPath();
ctx.rect(300 , 600 , 256 , 256 );
let pattern = ctx.createPattern(img, "repeat" );
ctx.fillStyle = pattern;
ctx.save();
ctx.translate(300 , 600 );
ctx.globalAlpha = 0.5 ;
ctx.stroke();
ctx.fill();
ctx.restore();
}
僅顯示圖片。
let img1 = new Image();
img.addEventListener("load" , loadHandler1, false );
img1.src = "flower2_1.jpg" ;
function loadHandler1 ( ) {
ctx.globalAlpha = 1 ;
ctx.shadowOffsetX = 0 ;
ctx.shadowOffsetY = 0 ;
ctx.shadowBlur = 0 ;
ctx.drawImage(img1, 700 , 600 );
}
面具(Masking):顯示特定圖形部分的圖片。
let img2 = new Image();
img2.addEventListener("load" , loadHandler2, false );
img2.src = "flower2_1.jpg" ;
function loadHandler2 ( ) {
ctx.globalAlpha = 1 ;
ctx.shadowOffsetX = 0 ;
ctx.shadowOffsetY = 0 ;
ctx.shadowBlur = 0 ;
ctx.beginPath();
ctx.arc(865 , 664 , 64 , 0 , Math .PI*2 , false );
ctx.clip();
ctx.drawImage(img2, 800 , 600 );
}
裁剪(blitting):取得圖片之特定部位。animeCharacters3.jpg
let tileset = new Image()
tileset.addEventListener("load" , loadHandler3, false )
tileset.src = "animeCharacters3.jpg"
function loadHandler3 ( ) {
ctx.drawImage(
tileset,
115 , 90 ,
128 , 225 ,
1200 , 200 ,
128 , 225
);
}
文字(text)。
let content = "Hello World!"
ctx.font = "96px 'Rockwell Extra Bold', 'Futura', sans-serif"
ctx.fillStyle = "red"
let width = ctx.measureText(content).width,
height = ctx.measureText("M" ).width;
ctx.textBaseline = "top"
ctx.fillText(
content,
1000 ,
600
)
Exercise
Dices
One dice: 可直接將以下程式碼置於html內之<script></script>tag內。
const dicex = 50 ;
const dicey = 50 ;
const dicewidth = 100 ;
const diceheight = 100 ;
const dotrad = 6 ;
const canvas = document .createElement("canvas" );
canvas.setAttribute("width" , "400" );
canvas.setAttribute("height" , "300" );
canvas.style.border = "1px dashed red" ;
document .body.appendChild(canvas);
const ctx = canvas.getContext("2d" );
const hr = document .createElement("hr" );
document .body.appendChild(hr);
const roll = document .createElement("button" );
roll.innerText = "Roll" ;
roll.addEventListener("click" , init);
document .body.appendChild(roll);
function init ( ) {
let ch = Math .floor(Math .random()*6 )+1 ;
drawface(ch);
}
function drawface (n ) {
ctx.lineWidth = 5 ;
ctx.clearRect(dicex, dicey, dicewidth, diceheight);
ctx.strokeRect(dicex, dicey, dicewidth, diceheight);
ctx.fillStyle = "black" ;
switch (n){
case 1 :
dice1("red" , 12 );
break ;
case 2 :
dice2();
break ;
case 3 :
dice3();
break ;
case 4 :
dice4("red" );
break ;
case 5 :
dice4();
dice1();
break ;
case 6 :
dice6();
break ;
}
}
function dice1 (color="black", radius=dotrad ) {
let dotx;
let doty;
ctx.beginPath();
ctx.fillStyle = color;
dotx = dicex + 0.5 *dicewidth;
doty = dicey + 0.5 *diceheight;
ctx.arc(dotx, doty, radius, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
function dice2 ( ) {
let dotx;
let doty;
ctx.beginPath();
dotx = dicex + 4 *dotrad;
doty = dicey + 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dicex + dicewidth - 4 *dotrad;
doty = dicey + diceheight - 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
function dice3 ( ) {
dice2();
dice1("black" )
}
function dice4 (color="black" ) {
let dotx;
let doty;
ctx.beginPath();
ctx.fillStyle = color;
dotx = dicex + 4 *dotrad;
doty = dicey + 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dicex + 4 *dotrad;
doty = dicey + diceheight - 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
ctx.beginPath();
dotx = dicex + dicewidth - 4 *dotrad;
doty = dicey + 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dicex + dicewidth - 4 *dotrad;
doty = dicey + diceheight - 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
function dice6 ( ) {
dice4();
let dotx;
let doty;
ctx.beginPath();
dotx = dicex + 4 *dotrad;
doty = dicey + diceheight/2 ;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dicex + diceheight - 4 *dotrad;
doty = dicey + diceheight/2 ;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
init();
two dices: 與前不同處是要控制骰子的位置(dicex & dicey)。
HTML code:
<canvas id ="canvas" > </canvas >
<hr >
<button id ="roll" > Roll</button >
<br >
<p id ="points" > </p >
JavaScript code:
const dicex = 50 ;
const dicey = 50 ;
const dicewidth = 100 ;
const diceheight = 100 ;
const dotrad = 6 ;
const dicex2 = dicex + dicewidth + 50 ;
const dicey2 = dicey;
const canvas = document .getElementById("canvas" );
canvas.setAttribute("width" , "400" );
canvas.setAttribute("height" , "300" );
canvas.style.border = "1px dashed red" ;
const ctx = canvas.getContext("2d" );
const roll = document .getElementById("roll" );
roll.addEventListener("click" , init);
const points = document .getElementById("points" );
function init ( ) {
let ch1 = Math .floor(Math .random()*6 )+1 ;
drawface(ch1);
let ch2 = Math .floor(Math .random()*6 )+1 ;
drawface(ch2, dicex2, dicey2, false );
console .log(ch1, ch2);
points.innerText = "Total points:" + (ch1+ch2);
}
function drawface (n, x = dicex, y = dicey, isOne=true ) {
ctx.lineWidth = 5 ;
if (isOne){
ctx.clearRect(dicex, dicey, dicewidth, diceheight);
ctx.strokeRect(dicex, dicey, dicewidth, diceheight);
}else {
ctx.clearRect(dicex2, dicey2, dicewidth, diceheight);
ctx.strokeRect(dicex2, dicey2, dicewidth, diceheight);
}
ctx.fillStyle = "black" ;
switch (n){
case 1 :
dice1("red" , 12 , x, y);
break ;
case 2 :
dice2(x, y);
break ;
case 3 :
dice3(isOne);
break ;
case 4 :
dice4("red" , x, y);
break ;
case 5 :
if (isOne){
dice4();
dice1();
}else {
dice4("black" , x, y);
dice1("black" , dotrad, x, y);
}
break ;
case 6 :
dice6(dx = x, dy = y, isOne);
break ;
}
}
function dice1 (color="black", radius=dotrad, dx=dicex, dy=dicey ) {
let dotx;
let doty;
ctx.beginPath();
ctx.fillStyle = color;
dotx = dx + 0.5 *dicewidth;
doty = dy + 0.5 *diceheight;
ctx.arc(dotx, doty, radius, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
function dice2 (dx, dy ) {
let dotx;
let doty;
ctx.beginPath();
dotx = dx + 4 *dotrad;
doty = dy + 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dx + dicewidth - 4 *dotrad;
doty = dy + diceheight - 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
function dice3 (isOne ) {
if (isOne){
dice2(dicex, dicey);
dice1("black" );
}else {
dice2(dicex2, dicey2);
dice1("black" , dotrad, dicex2, dicey2);
}
}
function dice4 (color="black", dx = dicex, dy = dicey ) {
let dotx;
let doty;
ctx.beginPath();
ctx.fillStyle = color;
dotx = dx + 4 *dotrad;
doty = dy + 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dx + 4 *dotrad;
doty = dy + diceheight - 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
ctx.beginPath();
dotx = dx + dicewidth - 4 *dotrad;
doty = dy + 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dx + dicewidth - 4 *dotrad;
doty = dy + diceheight - 4 *dotrad;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
function dice6 (dx = dicex, dy = dicey, isOne=true ) {
if (isOne){
dice4();
}else {
dice4(color="black" , dx=dicex2, dy=dicey2);
}
let dotx;
let doty;
ctx.beginPath();
dotx = dx + 4 *dotrad;
doty = dy + diceheight/2 ;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
dotx = dx + diceheight - 4 *dotrad;
doty = dy + diceheight/2 ;
ctx.arc(dotx, doty, dotrad, 0 , Math .PI*2 , true );
ctx.closePath();
ctx.fill();
}
init();
如果要設計三顆以上,可以調整一下函數的參數,應該可以容易的達成。
Sprites
上述的繪圖方式屬於比較低階(low-level)的API(Application Programming Interface),需要針對每一個圖形的細節做控制。當我們想要製作多個圖形時(尤其是同類的圖形),較好的方式是建立sprite來讓其變成高階(high-level)的語言形式。所謂sprite,原則上就是建立物件來描繪某圖形,之後直接產生該物件的instance便可以繪製。一般來說,sprite可以包含兩個部分,
一是描繪該圖型的參數,
此外便是提交(render)該圖形,也就是將其繪製在canvas上。
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();
ctx.rect(-this .width / 2 , -this .height / 2 , this .width, this .height);
if (this .strokeStyle !== "none" )
ctx.stroke();
ctx.fill();
};
}
先試試看物件的建構是否成功,加入以下的程式碼:
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"){
this .canvas = this .initCanvas(width, height, outline, backgroundColor);
this .ctx = this .canvas.getContext("2d" );
this .children = [];
}
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;
}
render(){
this .ctx.clearRect(0 , 0 , this .canvas.width, this .canvas.height);
this .children.forEach(sprite => {
this .displaySprite(sprite);
});
}
displaySprite(sprite) {
if (sprite.visible) {
this .ctx.save();
this .ctx.translate(
sprite.x + sprite.width / 2 ,
sprite.y + sprite.height /2
);
this .ctx.rotate(sprite.rotation);
this .ctx.globalAlpha = sprite.alpha;
this .ctx.scale(sprite.scaleX, sprite.scaleY);
sprite.render(this .ctx);
this .ctx.restore();
}
}
addChild(sprite){
this .children.push(sprite);
}
}
接下來加上以下的主程式,便可以看繪製出來的結果。
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 ;
this .dotxy = [];
}
roll(){
this .point = Math .floor(Math .random()*6 )+1 ;
switch (this .point){
case 1 :
this .fillStyle = "red" ;
this .radius = 18 ;
this .dotxy = [[0 ,0 ]];
break ;
case 2 :
this .fillStyle = "black" ;
this .radius = 6 ;
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 ;
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 ;
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 ;
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 ;
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 ;
}
}
render(ctx) {
ctx.strokeStyle = this .strokeStyle;
ctx.lineWidth = this .lineWidth;
ctx.fillStyle = "white" ;
ctx.beginPath();
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;
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();
}
};
}
class Canvas {
constructor (width = 256, height = 256, outline = "1px dashed black", backgroundColor = "white"){
this .canvas = this .initCanvas(width, height, outline, backgroundColor);
this .ctx = this .canvas.getContext("2d" );
this .children = [];
this .pointSum = 0 ;
}
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;
}
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);
}
displaySprite(sprite) {
if (sprite.visible) {
this .ctx.save();
this .ctx.translate(
sprite.x + sprite.width / 2 ,
sprite.y + sprite.height /2
);
this .ctx.rotate(sprite.rotation);
this .ctx.globalAlpha = sprite.alpha;
this .ctx.scale(sprite.scaleX, sprite.scaleY);
sprite.render(this .ctx);
this .ctx.restore();
}
}
addChild(...sprite){
for (let s of sprite)
this .children.push(s);
}
}
function draw ( ) {
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 );
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());
document .body.appendChild(roll);
}
draw();
現在想要有幾顆骰子就有幾顆,之後便可以設計骰子相關遊戲,例如Big & Small、骰子梭哈等。
Nested Sprite
所謂nested就是sprite中有sprite,原則上重點是下層的sprite之座標是對應上層的相對座標,當上層的座標改變(移動或旋轉等),下層物件應跟著對應變動。為了達成此目的,須至少於class Rectangle中新增以下變數及方法:
children: 這是用來儲存以某sprite為parent的所有sprite之list。
parent: 用來記錄每個sprite的parent,預設值為undefined。
layer: 是用來協助區分當sprite重疊時之上下關係,此處設定layer值較大者於上方。因為繪製(render)有先後,後來居上,所以如果可以控制繪製的先後次序即可。此處的做法是在繪製之前,先將children內的物件根據layer排序,如此便可以控制繪製(render)次序。
gx, gy: 是儲存任一sprite相對於canvas的座標。在此沒甚麼用處,因為只要使用相對其parent的座標即可,順便寫下來做為需使用時之用。對應的函數為getGx()與getGy()。
addChild(sprite): 用來將某sprite加入至children內。
removeChild(sprite): 移除children內的某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;
this .children = [];
this .parent = undefined ;
this .layer = 0 ;
this .gx = this .getGx();
this .gy = this .getGy();
}
getGx(){
if (this .parent){
return this .x + this .parent.gx;
}else {
return this .x;
}
}
getGy(){
if (this .parent){
return this .y + this .parent.gy;
}else {
return this .y;
}
}
addChild(sprite){
if (sprite.parent){
sprite.parent.removeChild(sprite);
}
sprite.parent = this ;
this .children.push(sprite);
}
removeChild(sprite){
if (sprite.parent==this ){
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();
ctx.rect(-this .width / 2 , -this .height / 2 , this .width, this .height);
if (this .strokeStyle !== "none" )
ctx.stroke();
ctx.fill();
};
}
接著是建立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"){
this .canvas = this .initCanvas(width, height, outline, backgroundColor);
this .ctx = this .canvas.getContext("2d" );
this .children = [];
}
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;
}
render(){
this .ctx.clearRect(0 , 0 , this .canvas.width, this .canvas.height);
this .children.sort((a, b)=> a.layer-b.layer);
this .children.forEach(sprite => {
this .displaySprite(sprite);
});
}
displaySprite(sprite) {
if (sprite.visible) {
this .ctx.save();
this .ctx.translate(
sprite.x + sprite.width / 2 ,
sprite.y + sprite.height /2
);
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(this .ctx);
if (sprite.children.length>0 ){
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);
})
}
this .ctx.restore();
}
}
addChild(...sprite){
for (let s of sprite)
this .children.push(s);
}
}
最後將主程式步驟寫成一個函數即可,範例如下:
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);
blueBox.addChild(greenBox);
blueBox.addChild(redBox);
blueBox.rotation = 0.25 ;
canvas.addChild(blueBox);
canvas.addChild(tomatoBox);
canvas.render();
}
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 .rotation = 0 ;
this .alpha = 1 ;
this .scaleX = 1 ;
this .scaleY = 1 ;
this .visible = true ;
this .children = [];
this .parent = undefined ;
this ._layer = 0 ;
this .pivotX = 0.5 ;
this .pivotY = 0.5 ;
this .shadow = false ;
this .shadowColor = "rgba(150, 150, 150, 0.5)" ;
this .shadowOffsetX = 3 ;
this .shadowOffsetY = 3 ;
this .shadowBlur = 3 ;
}
get gx(){
if (this .parent){
return this .x + this .parent.gx;
}else {
return this .x;
}
}
get gy(){
if (this .parent){
return this .y + this .parent.gy;
}else {
return this .y;
}
}
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){
if (sprite.parent){
sprite.parent.removeChild(sprite);
}
sprite.parent = this ;
this .children.push(sprite);
}
}
removeChild(sprite){
if (sprite.parent==this ){
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});
}
render(ctx){
ctx.strokeStyle = this .strokeStyle;
ctx.lineWidth = this .lineWidth;
ctx.fillStyle = this .fillStyle;
ctx.beginPath();
ctx.rect(-this .width / 2 , -this .height / 2 , this .width, this .height);
if (this .strokeStyle !== "none" )
ctx.stroke();
ctx.fill();
};
}
圓形:
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();
ctx.arc(this .radius + (-this .radius*2 *this .pivotX),
this .radius + (-this .radius*2 *this .pivotY),
this .radius, 0 , 2 *Math .PI, false );
if (this .strokeStyle !== "none" )
ctx.stroke();
ctx.fill();
};
}
橢圓形:
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;
}
render(ctx){
ctx.strokeStyle = this .strokeStyle;
ctx.lineWidth = this .lineWidth;
ctx.fillStyle = this .fillStyle;
ctx.beginPath();
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();
};
}
線段:
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();
ctx.moveTo(this .x1, this .y1);
ctx.lineTo(this .x2, this .y2);
if (this .strokeStyle !== "none" )
ctx.stroke();
};
}
文字:
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();
};
}
骰子:是一個矩形跟圓形的組合,因為是矩形內含圓形,以矩形為容器,所以將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 ;
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);
}
}
}
Canvas: 跟之前類似,這個class完成canvas建立以及包含全域render()方法。
class Canvas {
constructor (width = 360, height = 360, outline = "1px dashed black", backgroundColor = "white"){
this .canvas = this .initCanvas(width, height, outline, backgroundColor);
this .ctx = this .canvas.getContext("2d" );
this .children = [];
}
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;
}
render(){
this .ctx.clearRect(0 , 0 , this .canvas.width, this .canvas.height);
this .children.forEach(sprite => {
this .displaySprite(sprite);
});
}
displaySprite(sprite) {
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();
this .ctx.translate(
sprite.x + (sprite.width * sprite.pivotX),
sprite.y + (sprite.height * sprite.pivotY)
);
this .ctx.rotate(sprite.rotation);
this .ctx.scale(sprite.scaleX, sprite.scaleY);
if (sprite.parent)
this .ctx.globalAlpha = sprite.alpha * sprite.parent.alpha;
if (sprite.shadow){
this .ctx.shadowColor = sprite.shadowColor;
this .ctx.shadowOffsetX = sprite.shadowOffsetX;
this .ctx.shadowOffsetY = sprite.shadowOffsetY;
this .ctx.shadowBlur = sprite.shadowBlur;
}
if (sprite.render)
sprite.render(this .ctx);
if (sprite.children && sprite.children.length > 0 ){
this .ctx.translate(-sprite.width * sprite.pivotX, -sprite.height * sprite.pivotY);
sprite.children.forEach(child=>{
this .displaySprite(child);
})
}
this .ctx.restore();
}
}
addChild(...sprite){
for (let s of sprite)
this .children.push(s);
}
}
主程式(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);
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.addChild(blueBox, tomatoBox, cir1, ell1);
root.addChild(d1, d2, d3, d4, d5, d6);
root.addChild(line1, line2, line3);
root.addChild(text1);
canvas.addChild(root);
greenBox.layer = 2 ;
redBox.layer = 1 ;
greyBox.layer = 3 ;
cir1.layer = 1 ;
tomatoBox.layer = 5 ;
blueBox.layer = 2 ;
ell1.layer = 5 ;
canvas.render();
}
main();
若直接讓canvas做為畫布也可以,此處無設計畫布層的layer關係,可以將class Canvas中的render()修改如下(就是讓其children先行排序)即可:
render(){
this .ctx.clearRect(0 , 0 , this .canvas.width, this .canvas.height);
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){
super ();
this .x = x;
this .y = y;
this .source = source;
this .width = this .source.width;
this .height = this .source.height;
this .sourceX = sourceX;
this .sourceY = sourceY;
this .sourceWidth = sourceWidth;
this .sourceHeight = sourceHeight;
}
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 );
};
}
修改原來的主程式如下:
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);
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.addChild(blueBox, tomatoBox, cir1, ell1);
canvas.addChild(d1, d2, d3, d4, d5, d6);
canvas.addChild(line1, line2, line3, text1, img1, img2);
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();
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;
let leftBoundary = 10 ;
let rightBoundary = canvas.canvas.width+10 ;
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();
可以加上加速跟摩擦力來改變速度。當球碰觸到邊的時候,改變其加速度及摩擦力。
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 ;
ball.accY = 0.2 ;
ball.friX = 1 ;
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;
let leftBoundary = 10 ;
let rightBoundary = canvas.canvas.width+10 ;
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();
}
}
模擬重力。
加上球的質量(ball.mass),讓每次碰撞時速度減少(利用速度/質量,亦即質量應大於1)。
因為模擬重力,所以僅在y向有加速度。
摩擦力的部分僅在碰觸地面時產生,可根據情況自行調整。
碰觸時將x, y的位置調整為碰觸時狀況,避免出現球超出邊界的狀況。
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 ;
ball.accY = 0.3 ;
ball.friX = 1 ;
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;
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 ;
}else {
ball.friX = 1 ;
ball.friY = 1 ;
}
canvas.addChild(ball);
canvas.render();
}
}
控制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 ;
ball.accY = 0.3 ;
ball.friX = 1 ;
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;
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 ;
}else {
ball.friX = 1 ;
ball.friY = 1 ;
}
canvas.addChild(ball);
}
let fps = 500 ;
let start = 0 ;
let duration = 1000 /fps;
loop();
function loop (timestamp ) {
requestAnimationFrame(loop);
if (timestamp>=start) {
logic();
canvas.render();
start = timestamp + duration;
}
}
}
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 ;
this .children = [];
this .parent = undefined ;
this ._layer = 0 ;
this .pivotX = 0.5 ;
this .pivotY = 0.5 ;
this .shadow = false ;
this .shadowColor = "rgba(150, 150, 150, 0.5)" ;
this .shadowOffsetX = 3 ;
this .shadowOffsetY = 3 ;
this .shadowBlur = 3 ;
}
get gx(){
if (this .parent){
return this .x + this .parent.gx;
}else {
return this .x;
}
}
get gy(){
if (this .parent){
return this .y + this .parent.gy;
}else {
return this .y;
}
}
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){
if (sprite.parent){
sprite.parent.removeChild(sprite);
}
sprite.parent = this ;
this .children.push(sprite);
}
}
removeChild(sprite){
if (sprite.parent==this ){
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 ;
}
}
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 ;
}
toString(){
return "rectangle" ;
}
render(ctx){
ctx.strokeStyle = this .strokeStyle;
ctx.lineWidth = this .lineWidth;
ctx.fillStyle = this .fillStyle;
ctx.beginPath();
ctx.rect(-this .width / 2 , -this .height / 2 , this .width, this .height);
if (this .strokeStyle !== "none" )
ctx.stroke();
ctx.fill();
};
}
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 ;
}
toString(){
return "circle" ;
}
render(ctx){
ctx.strokeStyle = this .strokeStyle;
ctx.lineWidth = this .lineWidth;
ctx.fillStyle = this .fillStyle;
ctx.beginPath();
ctx.arc(this .radius + (-this .radius*2 *this .pivotX),
this .radius + (-this .radius*2 *this .pivotY),
this .radius, 0 , 2 *Math .PI, false );
if (this .strokeStyle !== "none" )
ctx.stroke();
ctx.fill();
};
}
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;
}
render(ctx){
ctx.strokeStyle = this .strokeStyle;
ctx.lineWidth = this .lineWidth;
ctx.fillStyle = this .fillStyle;
ctx.beginPath();
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();
};
}
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();
ctx.moveTo(this .x1, this .y1);
ctx.lineTo(this .x2, this .y2);
if (this .strokeStyle !== "none" )
ctx.stroke();
};
}
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();
};
}
class Picture extends Sprite {
constructor (x=0, y=0, source='none', sourceX=0, sourceY=0, sourceWidth=0, sourceHeight=0){
super ();
this .x = x;
this .y = y;
this .source = source;
this .width = this .source.width;
this .height = this .source.height;
this .sourceX = sourceX;
this .sourceY = sourceY;
this .sourceWidth = sourceWidth;
this .sourceHeight = sourceHeight;
}
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 );
};
}
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 ;
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);
}
}
}
class Canvas {
constructor (width = 360, height = 360, outline = "1px dashed black", backgroundColor = "white"){
this .canvas = this .initCanvas(width, height, outline, backgroundColor);
this .ctx = this .canvas.getContext("2d" );
this .children = [];
}
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" ;
document .body.appendChild(thecanvas);
return thecanvas;
}
render(){
this .ctx.clearRect(0 , 0 , this .canvas.width, this .canvas.height);
this .children.sort((a,b)=>a.layer-b.layer);
this .children.forEach(sprite => {
this .displaySprite(sprite);
});
}
displaySprite(sprite) {
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();
this .ctx.translate(
sprite.x + (sprite.width * sprite.pivotX),
sprite.y + (sprite.height * sprite.pivotY)
);
this .ctx.rotate(sprite.rotation);
this .ctx.scale(sprite.scaleX, sprite.scaleY);
if (sprite.parent)
this .ctx.globalAlpha = sprite.alpha * sprite.parent.alpha;
if (sprite.shadow){
this .ctx.shadowColor = sprite.shadowColor;
this .ctx.shadowOffsetX = sprite.shadowOffsetX;
this .ctx.shadowOffsetY = sprite.shadowOffsetY;
this .ctx.shadowBlur = sprite.shadowBlur;
}
if (sprite.render)
sprite.render(this .ctx);
if (sprite.children && sprite.children.length > 0 ){
this .ctx.translate(-sprite.width * sprite.pivotX, -sprite.height * sprite.pivotY);
sprite.children.forEach(child=>{
this .displaySprite(child);
})
}
this .ctx.restore();
}
}
addChild(...sprite){
for (let s of sprite)
this .children.push(s);
}
}
class Pointer {
constructor (element="none", scale=1){
this .element = element;
this .scale = scale;
this ._x = 0 ;
this ._y = 0 ;
this .press = undefined ;
this .enter = undefined ;
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) {
let element = event.target;
this ._x = (event.pageX - element.offsetLeft);
this ._y = (event.pageY - element.offsetTop);
if (this .press)
this .press();
event.preventDefault();
}
moveHandler(event) {
let element = event.target;
this ._x = (event.pageX - element.offsetLeft);
this ._y = (event.pageY - element.offsetTop);
if (this .enter)
this .enter();
event.preventDefault();
}
}
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 ) {
for (let s=0 ; s<sprites.length; s++){
if (sprites[s].isInside(pointer.x, pointer.y)){
sprites[s].fillStyle = `${randomcolor()} ` ;
canvas.render();
}
}
}
function enterSprite (...sprites ) {
let inside=false ;
for (let s=0 ; s<sprites.length; s++){
if (sprites[s].isInside(pointer.x, pointer.y)){
inside=true ;
}
}
if (inside)
canvas.canvas.style.cursor='pointer' ;
else
canvas.canvas.style.cursor='auto' ;
}
let pointer = new Pointer(canvas.canvas);
pointer.press = ()=>{hitSprite(ball_1, ball_2, box_1, box_2)};
pointer.enter = ()=>{enterSprite(ball_1, ball_2, box_1, box_2)};
}
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 ;
}
只要控制四個邊的範圍,而圓形則偵測與圓心的距離。至於其他比較複雜的圖形則需要比較複雜的方式來判斷。
接著建立Pointer物件來描繪游標位置。
其中的element為游標對應的物件,在此表示canvas。
scale沒用到,主要是設計用來在需要的情況下縮放x,y位置。
press&enter兩者分別為press跟enter時所使用的函數,在主程式中定義。
x(),y(),centerX(),centerY(),position()等方法為了方便取得x,y座標,順便寫下,此處沒用到。
clickHandler()&moveHandler()為addEventListener()所對應的方法,主要是先更新x,y座標,然後執行對應的函數(press&enter)。
在主程式中首先布置(canvas.render())所需要的sprites。而hitSprite()與enterSprite()則分別為press與enter所對應的函數內容,在此函數內描述圖形該對應之反應。然後使用箭頭函數分別將此兩函數指派給pointer.press與pointer.enter即可。
根據此架構可以自行增加圖形與對應的反應(針對滑鼠或鍵盤)。然後設計自己的邏輯架構與圖形互動(例如設計一個簡單的game)。
Exercise
踩地雷