JavaScript THIS




this 是 JavaScript 的一個關鍵字, this 會在不同的情境下指稱到不同的物件。

this 在物件導向裡面,它所代表的就是那個 instance 本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Car {
setName(name) {
this.name = name
}

getName() {
return this.name
}
}

const myCar = new Car()
myCar.setName('Ford')

console.log(myCar.getName()) // Ford

在上面我們宣告了一個 class Car,寫了 setName 跟 getName 兩個方法,在裡面用this.name來取放這個 instance 的屬性,myCar.setName('Ford'),所以 this 就會是myCar

this 不等於 function

如果直接調用函式,此函式的 this 會指向 window 。

1
2
3
4
5
6
window.say = 'Hi';
function CallSay() {
console.log(this.say); // Hi
}

CallSay();

this 不會指到 CallSay 這個 function,實際上是指向 window

再看一個範例:

1
2
3
4
5
6
7
8
9
var call = function() {  
    console.log( this.name );
};
var say = function() {
    var name = 'Felix';
    this.call();
};

say();

在這個範例中, say 可以透過 this.call 取得 call ,是因為 this.call 實際上是指向 window.call
callthis.name 並非是 say 中的 Felix ,而是指向 window.name ,所以會得到 undefined 的結果。


下個例子是將 function 內在包覆著 function,但只要是直接呼叫,this 都是屬於全域。

1
2
3
4
5
6
7
8
9
10
11
12
window.say = 'Hi';
function CallSay () {
console.log('call:', this.say); //call: Hi

// function 內的 function
function CallSayNow () {
console.log('call me now:', this.say);//call me now: Hi
}
CallSayNow();
}

CallSay();

來看一下巢狀迴圈中的 this :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {

func1: function(){
console.log(this); //指到 func1


var func2 = function(){
// 這裡的 this 跟上層不同!
console.log(this);// 指到 window

};

func2();
}
};

obj.func1();

obj.func1() 裡面的 this 會指向 func1 ;是因為 func1 透過 obj 來呼叫的。

obj.func1() 裡面的 func2() 在執行時的 this 卻會指向 window

也就是當沒有特定指明 this 的情況下,預設綁定 (Default Binding) this 為 「全域物件」,也就是 window。

以上這幾種情況, this 的值在瀏覽器底下就會是 window ,在 node.js 底下會是 global ,如果是在嚴格模式,this 的值就會是 undefined

強制指定 this 的方式

在 JavaScript 有三個可以強制指定 this 的方式,分別是 call()apply() 以及 bind()

callapply 是很類似的方法,這兩種都是能夠呼叫 fucntion 的函式

1
2
3
4
5
6
7
8
9
10
11
'use strict';
function funA(a, b){

console.log(this, a, b)

}

funA(1, 2) // undefined 1 2
funA.call("Hector", 1, 2) // Hector 1 2

funA.apply("Ray", [1, 2]) // Ray 1 2

因為是嚴格模式所以 funA(1, 2) 的 this 是 undefined

callapply 就是你第一個參數傳什麼,裡面 this 的值就會是什麼。儘管原本已經有 this,也依然會被覆蓋:

而兩者的差別只在於 apply 傳進去的參數是一個 array,所以上面這三種呼叫 function 的方式是等價的。除了直接呼叫 function 以外,你也可以用 call 或是 apply 去呼叫,差別在於傳參數的方式不同。

除了以上兩種以外,還有最後一種可以改變 this 的方法:bind。
1
2
3
4
5
6
7
8
'use strict';
function funA() {
console.log(this)
}

const myFunA = funA.bind('Hi')

myFunA() // Hi

在這邊我們把 funA 這個 function 用 Hi 來綁定,所以最後呼叫 myFunA 時會輸出 Hi

在看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
LastName: "Lincoln"

};

var func = function () {

console.log(this.LastName);

};

func(); // undefined
func.bind(obj)(); // Lincoln

加上了 bind 之後的 func.bind(obj)() ,會替我們將 functhis 暫時指向我們所設定的 obj
於是 console.log(this.LastName) 的結果自然就是 obj.LastName 也就是 Lincoln 了。

重新指向 this

假設我們今天在某個元素上透過 addEventListener 註冊了 click 事件,而在事件中的 this 指的是「觸發事件的元素」。

要是我們在事件的 callback function 加入 ajax 的請求,那麼根據前面所說的,預設綁定 (Default Binding) 會把這個 callback function 的 this 指定給 global object ,也就是 window

如果需調用的則是物件本身的話,可以先用一個變數指向 this,等到調用後再重新使用它。

1
2
3
4
5
6
7
8
9
10
11
12
el.addEventListener("click", function(event) {

// 透過 that 參考
var that = this;
console.log( this.textContent );

$ajax('[URL]', function(res) {
// this.textContent => undefined
console.log(that.textContent, res);
});

}, false);

像這樣,我們將事件內的 this 先用一個叫 that 的變數儲存它的參考,那麼在 ajax 的 callback function 就可以透過 that 來存取到原本事件中的 this 了。


如果我們把 call 跟 bind 同時用會怎樣!?

1
2
3
4
5
6
'use strict';
function funA() {
console.log(this)
}
const myFunA = funA.bind('Hi')
myFunA.call('Hello') // Hi

答案是不會改變,一但 bind 了以後值就不會改變了。

在非嚴格模式底下,無論是用 call、apply 還是 bind,你傳進去的如果是基本型別都會被轉成 object

舉例來說:

1
2
3
4
5
6
function funA() {
console.log(this)
}
funA.call(1) // Number {1}
const myFunA = funA.bind('Hi')
myFunA() // String {"Hi"}

物件中的 this

最前面我們示範了在物件導向 class 裡面的 this,但在 JavaScript 裡面還有另外一種方式也是物件:

1
2
3
4
5
6
7
8
9
10
11
const obj = {
value: 1,
say: function() {

console.log(this.value)
}
}

obj.say() // 1
const hello = obj.say
hello() // undefined

這種跟一開始的物件導向範例不太一樣,這個範例是直接創造了一個物件而沒有透過 class,所以你也不會看到 new 這個關鍵字的存在。

舉個簡單的例子來幫大家複習一下 Scope Chain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var value = 1

function say(){

console.log(value)
}

const obj = {
value: 'value',

say1: function() {
console.log(this.value); // value

say() // 1

},
say2: function() {
console.log(this.value); // value

var value = 2

say() // 1

}
}

say() // 1

obj.say1()

obj.say2()

無論怎麼呼叫 say 這個 function,印出來的 value 永遠都會是全域變數的 value ,因為 say 在自己的作用域底下找不到 value 於是往上一層找,就會找到 global scope ,這跟你在哪裡呼叫 say 一點關係都沒有。 say 這個 function 在「定義」的時候就把 scope 給決定好了。

但 this 卻是完全相反,this 的值會根據你怎麼呼叫它而變得不一樣,像是先前我們剛講過的 call、apply 跟 bind ,你可以用不同的方式去呼叫 function,讓 this 的值變得不同。


this 的值跟作用域跟程式碼的位置在哪裡完全無關,只跟「如何呼叫」有關。

讓我們看複雜一點的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const obj = {
value: 1,
say: function() {

console.log(this.value)
},
func2: {

value: 2,
say: function() {

console.log(this.value)
}
}
}

const obj2 = obj.func2
const say= obj.func2.say

obj.func2.say()

obj2.say()

say()

say 因為沒有傳參數進去,所以是預設綁定,在非嚴格模式底下是 window ,所以會 window.value 也就是 undefined

可以透過把 function 的呼叫轉成用 call 的形式,就會較容易看出 this 的值是什麼。

1
2
3
obj.func2.say() // obj.func2.say.call(obj.func2) => 2
obj2.say() // obj2.say.call(obj2) => 2
say() // say.call() => undefined

箭頭函式的 this

從 ES6 開始新增了一種叫做 「箭頭函式表示式」 (Arrow Function expression) 的函式表達式。

而箭頭函式有兩個重要的特性:

  • 更簡短的函式寫法
  • this 變數強制綁定

ES6 新增的箭頭函式中的 this 有不一樣的運作方式,只要記住「在宣告它的地方的 this 是什麼,它的 this 就是什麼」,什麼意思呢?讓我們看個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const obj = {
Firstname: "Vincent",

say: function(){

// 這邊印出來的 this 是什麼,func1 的 this 就會是什麼
// 在宣告它的地方的 this 是什麼,func1 的 this 就是什麼
console.log(this)
const func1 = () => {

console.log(this.Firstname)

}
func1()

}
}

obj.say()
// {Firstname: "Vincent", say: ƒ}
// Vincent

const say = obj.say

say()
// Window
// undefined

我們在 say 這個 function 裡面宣告了 func1 這個箭頭函式,所以 say 的 this 是什麼, func1 的 this 就會是什麼。

箭頭函式的 this 不是取決於在宣告時那個地方的 this。

參考文獻

  1. https://blog.techbridge.cc/2019/02/23/javascript-this/

  2. https://ithelp.ithome.com.tw/articles/10193193

  3. https://wcc723.github.io/javascript/2017/12/12/javascript-this/

JavaScript Closure


Scope Chain

在開始談閉包之前,我們現來談談「範圍鏈」(Scope Chain) 的觀念。

Scope Chain(範圍鏈)的特性,是指使用變數的時候,會遵循著 Scope Chain 一層一層往外找,也就是看看函式自身的 context 物件上是否有該特性,如果沒有就往外頭的 context 物件看看有沒有該特性。

用 var 所宣告的變數,作用範圍是在當時所在環境(函式內),而不使用 var 直接指定值而建立的變數,則是全域物件(window)上的一個屬性,也就全域範圍。

例如像下面這段程式碼:

1
2
3
4
5
6
7
var myVar = "outer";
function func(){
console.log(myVar); // outer
myVar = "inner";
}
func();
console.log(myVar); // inner

func 的變數myVar 因為沒有使用 var 進行宣告,所以 myVar 會變成「全域變數」。

修改一下上面範例,若是在func 中的 myVar,有透過 var 宣告時,變數就會作用在當時的環境,也就是 func

由於 JavaScript 提升 (Hoisting)的特性,Hoisting 是 JavaScript 的預設行為,把所有宣告效果提到當前 Scope 的頂端,也就是變數可以在宣告之前進行初始化和使用,而不會拋錯:

1
2
3
4
5
6
7
var myVar = "outer";
function func(){
console.log(myVar); // undefined
var myVar = "inner";
}
func();
console.log(myVar); // outer

func 運作上等同於:

1
2
3
4
5
function func(){
var myVar;
console.log(myVar);// undefined
myVar = "inner";
}

讓我們在看一個例子,每一個 function 執行的時候都會建立一個新的 context,所以 funcfund 各自為獨立的一個 Function execution context(執行環境):

1
2
3
4
5
6
7
8
9
10
11
12
var myVar = "outer";
//另外一個執行環境
function func(){
console.log(myVar); // outer

}
//一個執行環境
function fund(){
var myVar = "inner";
func();
}
fund();

fund 呼叫 func 時,由於 fundfunc 都處於全域的環境,而 myVar 變數是被定義在 fund 的函式裡面;當你試圖在 func 中 使用 myVar 變數時,它會查看 func 的 context 物件上是否有該特性,如果有就使用,沒有就往外面一層找。由於 func 的上一層是 global context ,所以就存取到全域變數的 myVar,最終印出的結果就會是 outer


如果函式 func 的位置是被 fund 所包裹,當 func 找不到 myVar ,就會往它的向外一層找;所以就存取到 fund 裡面的 myVar ,輸出 inner

1
2
3
4
5
6
7
8
9
10
11
12
var myVar = "outer";
function fund(){
var myVar = "inner";
func();

function func(){

console.log(myVar); // inner
}

}
fund();

閉包 Closure

閉包是個特殊的物件,他包含了一個函式,以及函式被建立時所在的環境。

在 JavaScript 中,只要有巢狀的函數定義,就會形成閉包。因為內層的函數需要引用到外層函數中定義的變數(建立範圍鏈 Scope Chain),所以外層函數中變數的狀態就好像被內層函數「關閉」起來了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function OuterFunction() {
// 一個局部變數
var outerVariable = 100;

function InnerFunction() {
// 內部函數可以存取外部函式的變數

alert(outerVariable);
}

// 返回一個內部函式,並創建一個 closure
return InnerFunction;
}
var innerFunc = OuterFunction();

innerFunc(); // 100

上面例子中,OuterFunction() 執行時返回一個 function,同時自動創建了一個 closure 環境。closure 像是一個特殊的物件 (指定給了 innerFunc ),closure 中包含一個函數 InnerFunction ,以及函數 OuterFunction 執行當時的環境,讓你在函數返回後還是可以持續存取 closure 保存的環境 ,像是能存取 outerVariable 變數, outerVariable 變數不會因為函數返回後而被刪除。


在這個例子中,我們把 counter 封裝在 Counter() 當中,不但可以讓裡面的 counter 不會暴露在 global 環境造成變數衝突,也可以確保內部 counter 被修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Counter() {
var counter = 0;

function IncreaseCounter() {
return counter += 1;
};

return IncreaseCounter;
}

var counter = Counter();
alert(counter()); // 1
alert(counter()); // 2
alert(counter()); // 3
alert(counter()); // 4

如果我們需要新增另一個計數器的話,透過一個閉包可以很輕鬆地達成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Counter() {
    var counter = 0;

    function IncreaseCounter() {
        return counter += 1;
    };

return IncreaseCounter;

}
var counter = Counter();
var counter2 = Counter();

alert(counter()); // 1
alert(counter()); // 2
alert(counter()); // 3

alert(counter2()); // 1
alert(counter2()); // 2

countercounter2 各自是「獨立」的計數器實體,彼此不會互相干擾。

參考文獻

  1. https://ithelp.ithome.com.tw/articles/10193009

  2. https://www.fooish.com/javascript/function-closure.html

  3. https://ithelp.ithome.com.tw/articles/10029457

  4. https://ithelp.ithome.com.tw/articles/10199934

  5. https://www.tutorialsteacher.com/javascript/closure-in-javascript

JavaScript IIFEs




IIFEs

立即被呼叫的函式 (Immediately Invoked Function Expression, IIFE)

Immediately 是立即的意思,invoked 則是執行某個函式時,「執行」的意思,function expression 是一種用來建立 function 的方法,總括來說,就是用 function expression 的方式建立函式後並立即執行它

Function Statement 和 Function Expression

以下為 Function Statement 和 Function Expression 的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// function statement
function fn1(num) {

console.log(num);

}
fn1(1);


// function expression
var fn2= function(num) {

console.log(num);

}
fn2(2);

先來看一下範例,印出 fn1() 的內容:

1
2
3
4
5
6
// function expression
var fn1 = function(num) {
console.log(num);
}

console.log(fn1);

呼叫 fn1() 後,它會回傳整個函式的內容,這是尚未 執行(Invoked)的結果。


如果是 IIFEs 就在這段程式碼的最後,加上一個執行的指令,也就是括號 ( ):

1
2
3
var fn1 = function(num) {
console.log(num);
}(10);

在我們建立函式的同時,這段函式就會立即被執行了,印出 10

試著把 function 裡面的 console.log 改成 return

原本 function expression 的程式碼如下:

1
2
3
4
5
// function expression
var fn1 = function(num) {
return(num);
}
console.log(fn1);

可以發現結果還是一個函式:


如果把它改成 IIFEs 的話,就會得到 10

1
2
3
4
5
6
// Immediately Invoked Functions Expressions (IIFEs)
var fn1 = function(num) {
return(num);
}(10);

console.log(fn1);

在利用 IIFE 的寫法後,原本的變數 fn1 已經變成函式執行後回傳的「Number」,它已經是數字而不是 function ,如果還使用 () 去 Invoke(執行) 的話會出現錯誤。

1
2
3
4
5
// Immediately Invoked Functions Expressions (IIFEs)
var fn1 = function(num) {
return(num);
}(10);
console.log(fn1());

產生錯誤:


下面的範例,我們建立了一個匿名函式,並且透過 IIFEs 馬上執行:

1
2
3
4
5
6
7
8
9
var value= 1;

(function(num) {
var value= 3;
console.log(value)
console.log(num);
})(2);

console.log(value);

以下為執行結果:

1
2
3
3
2
1

由執行結果可看到,在IIFEs中所建立的變數,不會影響到 Global Execution Context 所建立的變數,也就是說,透過IIFEs,達成不同 execution context 的變數之間不會互相影響。

不過我們可以透過填入物件 window ,直接針對 window 裡面的物件去做改變:

1
2
3
4
5
6
7
8
9
10
11
var value= 1;

(function(global,num) {
var value= 3;
//透過 global 去修改外面的 value
global.value=4;
console.log(value)
console.log(num);
})(window,2);// 新增一個參數 window

console.log(value);

結果如下,成功變更在 Global Execution Context 的 value4

1
2
3
3
2
4

參考文獻

  1. https://pjchender.blogspot.com/2016/05/javascriptiifesimmediately-invoked.html

  2. https://pjchender.blogspot.com/2016/05/iifesimmediately-invoked-functions.html

  3. https://dotblogs.com.tw/h20/2019/03/06/162305

JavaScript Callback Function


Callback Function

把函式當作另一個函式的參數,透過另一個函式來呼叫它,他可以「控制多個函式間執行的順序」。

來看個例子,首先定義兩個 function ,因為 function 會立即執行,所以先印出 1 ,在印出 2

1
2
3
4
5
6
7
8
9
function first(){
console.log(1);
}
function second(){
console.log(2);
}
first();//1

second();//2

透過 setTimeoutfirst() 設定延遲 500毫秒 在執行, JavaScript 不會等待 first() 執行完才執行 second() ;所以就算我們先呼叫了 first() ,也會是 second() 先被執行,結果就會是先印出 2 ,在印出 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function first(){
// Simulate a code delay
setTimeout( function(){
console.log(1);
}, 500 );
}
function second(){
console.log(2);
}
first();

second();
//2
//1

像這種時候,為了確保執行的順序,就會透過 Callback function 的形式來處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 為了確保先執行 first 再執行 second
// 我們在 first 加上 callback 參數
var first=function(callback){

setTimeout(function(){
console.log(1);
callback();
}, 500);
};


var second=function(){
console.log(2);
};
// 將 second 作為參數帶入 first()
first(second);
//1
//2

像這樣,無論 first 在執行的時候要等多久, second 都會等到 console.log(1); 之後才執行。

另一個例子:

1
2
3
4
5
6
7
8
9
10
11
function doHomework(subject, callback) {
alert(`Starting my ${subject} homework.`);
callback();
}
function alertFinished(){
alert('Finished my homework');
}

doHomework('math', alertFinished);
//Starting my math homework.
//Finished my homework

不過需要注意的是,當函式之間的相依過深,callback 多層之後產生的「Callback Hell」維護起來就會很複雜。



參考文獻

  1. https://ithelp.ithome.com.tw/articles/10192739

  2. https://codeburst.io/javascript-what-the-heck-is-a-callback-aba4da2deced

DOM - 事件處裡及傳遞機制




這篇教學介紹瀏覽器事件的運作原理,包含 EventTarget 、 Event Bubbling 、 Event Capturing 、 Event Delegation 。

Event Flow

事件流程 (Event Flow) 指的就是「網頁元素接收事件的順序」。

事件流程可以分成兩種機制:

  • 事件冒泡 (Event Bubbling)
  • 事件捕獲 (Event Capturing)

事件冒泡 (Event Bubbling)

Event Bubbling 指的是當某個事件發生在某個 DOM element 上(如:點擊),這個事件會觸發 DOM element 的 event handler ,接下來會再觸發他的 parent 的 event handler ,以及 parent 的 parent 的 event handler …直到最上層。


圖片來源: Event Flow: capture, target, and bubbling


事件冒泡指的是「從啟動事件的元素節點開始,逐層往上傳遞」,直到整個網頁的根節點,也就是 document

事件捕獲 (Event Capturing)

和 Event Bubbling 相反, event 發生在某個 DOM element 上的時候,會從他的最上層 parent 開始觸發 event handler ,再來是倒數第二上層的 event handler ,以此類推,直到觸發事件的 DOM element 本身的 event handler 。


圖片來源: Event Flow: capture, target, and bubbling


事件捕獲則和事件冒泡機制相反。


例如我們看這一個 HTML DOM 結構:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<title>event flow example</title>
</head>
<body>
<div>
<ul>
<li></li>
</ul>
</div>
</body>
</html>

當使用者點擊 li 元素時,事件觸發的順序是:

  1. Capturing 捕獲階段:Document -> <html> -> <body> -> <div> -> <ul> -> <li>


  2. Bubbling 氣泡階段:<li> -> <ul> -> <div> -> <body> -> <html> -> Document


要檢驗事件流程,我們可以透過 addEventListener() 方法來綁定 click 事件,透過 addEventListener 指定事件的綁定,第三個參數 true / false 分別代表捕獲/ 冒泡 機制,不加參數的話,預設值是 false:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<!DOCTYPE html>
<html>
<body>
<ul id="list">
<li id="list_item">
<a id="list_item_link" target="_blank" href="https://cheng-yi-ting.github.io/">
Click link!
</a>
</li>
</ul>


<script>

var list = document.getElementById('list');
var list_item = document.getElementById('list_item');
var list_item_link = document.getElementById('list_item_link');

// list 的捕獲
list.addEventListener('click', (e) => {
console.log('list capturing', e.eventPhase);
}, true)

// list 的冒泡
list.addEventListener('click', (e) => {
console.log('list bubbling', e.eventPhase);
}, false)

// list_item 的捕獲
list_item.addEventListener('click', (e) => {
console.log('list_item capturing', e.eventPhase);
}, true)

// list_item 的冒泡
list_item.addEventListener('click', (e) => {
console.log('list_item bubbling', e.eventPhase);
}, false)

// list_item_link 的捕獲
list_item_link.addEventListener('click', (e) => {
console.log('list_item_link capturing', e.eventPhase);
}, true)

// list_item_link 的冒泡
list_item_link.addEventListener('click', (e) => {
console.log('list_item_link bubbling', e.eventPhase);
}, false)



</script>
</body>
</html>

點一下超連結,console 輸出以下結果:

1
2
3
4
5
6
list capturing 1
list_item capturing 1
list_item_link capturing 2
list_item_link bubbling 2
list_item bubbling 3
list bubbling 3

1 是CAPTURING_PHASE,2 是AT_TARGET,3 是BUBBLING_PHASE

從這邊就可以很明顯看出,事件的確是從最上層一直傳遞到 target,再從 target 不斷冒泡傳回去,先傳到上一層的#list_item,再傳到上上層的#list

事件傳遞到我們點擊的超連結(a#list_item_link)本身,在這邊無論你使用addEventListener的第三個參數是true還是false,這邊的e.eventPhase都會變成AT_TARGET

Capturing 或 Bubbling 的目標執行順序

既然是先捕獲,再冒泡,意思就是無論那些addEventListener的順序怎麼變,輸出的東西應該還是會一樣才對。我們把捕獲跟冒泡的順序對調,看一下輸出結果是否一樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// list 的冒泡
list.addEventListener('click', (e) => {
console.log('list bubbling', e.eventPhase);
}, false)

// list 的捕獲
list.addEventListener('click', (e) => {
console.log('list capturing', e.eventPhase);
}, true)

// list_item 的冒泡
list_item.addEventListener('click', (e) => {
console.log('list_item bubbling', e.eventPhase);
}, false)

// list_item 的捕獲
list_item.addEventListener('click', (e) => {
console.log('list_item capturing', e.eventPhase);
}, true)

// list_item_link 的冒泡
list_item_link.addEventListener('click', (e) => {
console.log('list_item_link bubbling', e.eventPhase);
}, false)

// list_item_link 的捕獲
list_item_link.addEventListener('click', (e) => {
console.log('list_item_link capturing', e.eventPhase);
}, true)

點一下超連結,console 輸出以下結果:

1
2
3
4
5
6
list capturing 1
list_item capturing 1
list_item_link bubbling 2
list_item_link capturing 2
list_item bubbling 3
list bubbling 3

list_item_link先執行了添加在冒泡階段的 listener,才執行捕獲階段的 listener。

前面有提到,當事件傳遞到點擊的真正對象,也就是 e.target 的時候,無論addEventListener的第三個參數是true還是false,這邊的e.eventPhase都會變成AT_TARGET

既然已經是AT_TARGET,就不會區分捕獲跟冒泡,執行順序會根據addEventListener的順序來決定,先添加的先執行,後添加的後執行。


關於這些事件的傳遞順序,只需記住兩個原則:

  • 先捕獲,再冒泡
  • 當事件傳到 target 本身,沒有分捕獲跟冒泡

阻擋事件冒泡傳遞 event.stopPropagation()

如果我們想要阻擋事件向上冒泡傳遞,那麼就可以利用 event object 提供的另一個方法: event.stopPropagation() ,讓事件的傳遞中斷,不會繼續往下傳遞。

根據上面的例子,我添加 event.stopPropagation()#list 的捕獲階段:

1
2
3
4
5
// list 的捕獲
list.addEventListener('click', (e) => {
console.log('list capturing', e.eventPhase);
e.stopPropagation();
}, true)

點一下超連結,console 輸出以下結果:

1
list capturing 1

因為事件的傳遞被停止,所以剩下的 listener 都不會再收到任何事件。

如果在同一個節點上不只一個 listener,其他 listener 還是會被執行。

1
2
3
4
5
6
7
8
9
10
// list 的捕獲 A
list.addEventListener('click', (e) => {
console.log('list capturing-A');
e.stopPropagation();
}, true)

// list 的捕獲 B
list.addEventListener('click', (e) => {
console.log('list capturing-B');
}, true)

點一下超連結,console 輸出以下結果:

1
2
list capturing-A
list capturing-B

如果要讓其他同一層級的 listener 也不要被執行,可以使用 e.stopImmediatePropagation()

1
2
3
4
5
6
7
8
9
10
// list 的捕獲 A
list.addEventListener('click', (e) => {
console.log('list capturing-A');
e.stopImmediatePropagation();
}, true)

// list 的捕獲 B
list.addEventListener('click', (e) => {
console.log('list capturing-B');
}, true)

點一下超連結,console 輸出以下結果:

1
list capturing-A

阻擋預設行為 event.preventDefault()

HTML 部分元素會有預設行為,像是 <a> 的連結,或是表單的 submit 等等…,
如果我們需要在這些元素上綁定事件,那麼可以透過 event.preventDefault()取消它們的預設行為

1
2
3
4
// list_item_link 的冒泡
list_item_link.addEventListener('click', (e) => {
e.preventDefault();
}, false)

當點擊超連結的時候,就不會執行原本預設的行為(新開分頁)。event.preventDefault()在之後傳遞下去的事件裡面也會有效果。

1
2
3
4
5
// list 的捕獲 A
list.addEventListener('click', (e) => {
console.log('list capturing-A');
e.preventDefault();
}, true)

就算是把 event.preventDefault() 添加到 #list 的捕獲事件裡面,等之後事件傳遞到#list_item_link的時候,一樣可以取消超連結的預設行為(新開分頁)。

事件委派(Event Delegation)

假設同時有很多 DOM element 都有相同的 event handler ,與其在每個 DOM element 上個別附加 event handler ,不如利用 event bubbling 的特性,統一在他們的 ancestor 的 event handler 處理,可以有效地減少監聽器數量。

假設有一個三個 li 節點的 ul ,接著動態產生剩下的節點資料:

1
2
3
4
5
<ul id="list">
<li >one</li>
<li >two</li>
<li >three</li>
<ul>

雖然可以對個別的li附加 click event hander ,但有幾個li就要加幾次 addEventListener。我們可以透過 event delegation 的方式統一管理,任何點擊 li 的事件都傳到 ul ,因此我們可以在 ul 身上放一個 addEventListener

以下例子為將點擊的 li 元素,設置 hidden 屬性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 var list = document.getElementById('list');
list.addEventListener('click', function (e) {

//點擊元素後,該元素隱藏
e.target.style.visibility = 'hidden';

// e.target refers to the clicked <li> element
// This is different than e.currentTarget which would refer to the     parent <ul> in this context
console.log(e.target.tagName);//LI
console.log(e.currentTarget.tagName);//UL

//this 代表的會是「觸發事件的目標」元素,也就是 event.currentTarget 而不是 e.target。
console.log(this.tagName);//UL

}, false);

// 取得 #list ul
var List = document.getElementById('list');
for (var i = 0; i < 10; i++) {
// 建立 li 標籤
var node = document.createElement("LI");
// 建立 文字節點
var textnode = document.createTextNode(i);
// 透過 appendChild 將 文字節點 加入至 List
node.appendChild(textnode);
document.getElementById("list").appendChild(node);
}

輸出結果:

  • one
  • two
  • three
  • 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  • 當新增或刪除一個 li 的時候,無需去處理和那個元素相關的 listener ,因為 listener 是放在 ul ,透過父節點來處理子節點的事件,就稱為事件代理。

    E.CURRENTTARGET VS. E.TARGET

    event.currentTarget 屬性會指向目前於冒泡或捕捉階段正在處理該事件之事件處理器所註冊的 DOM 物件,而 event.target 屬性則是會指向觸發事件的 DOM 物件。

    如果放置一個 event listener 到 p 節點, event.currentTarget 就會指到 pevent.target 則還是指到 span ,如果把 event listener 放置到 body 上,則 event.currentTarget 就會指到 body

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <p id="list">

    <a href="#">some

    <span>text</span>

    </a>

    </p>
    1
    2
    3
    4
    5
    6
    var list = document.getElementById('list');

    list.addEventListener('click', function (e) {
    console.log(e.target.tagName);
    console.log(e.currentTarget.tagName);
    }, false);

    EventTarget.addEventListener()

    addEventListener 方法可以用來綁定元素的事件處理函數,有三個參數,分別是「事件名稱」、「事件的處理器」(事件觸發時執行的 function),以及一個「Boolean」值,由這個 Boolean 決定事件是以「捕獲」或「冒泡」機制執行,若不指定則預設為「冒泡」。


    事件監聽範例:
    1
    2
    3
    4
    <table id="outside">
    <tr><td id="t1">one</td></tr>
    <tr><td id="t2">two</td></tr>
    </table>

    addEventListener 可以對同一個元素指定多個事件處理函數,當點擊 li 元素時,跳出警告視窗,並執行 modifyText 函數;將 t2 的值 修改為 four

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Function to change the content of t2
    function modifyText() {
    var t2 = document.getElementById("t2");
    t2.firstChild.nodeValue = "four";
    }

    // add event listener to table
    var el = document.getElementById("outside");
    el.addEventListener("click", modifyText, false);

    el.addEventListener('click', function(){
    alert('HI');
    }, false);

    可以修改為匿名函數的事件監聽:

    1
    el.addEventListener("click", function(){modifyText("four")}, false);

    EventTarget.removeEventListener()

    取消透過 addEventListener 綁定的事件處理函數,三個參數與 addEventListener() 一樣,分別是「事件名稱」、「事件的處理器」以及「捕獲」或「冒泡」的機制。

    但是需要注意的是,由於 addEventListener() 可以同時針對某個事件綁定多個 handler,所以透過 removeEventListener() 解除事件的時候,第二個參數的 handler 必須要與先前在 addEventListener() 綁定的 handler 是同一個「實體」。

    1
    2
    3
    4
    5
    6
    var el = document.getElementById("outside");

    el.addEventListener('click', function(){alert('Hi');}, false);

    //remove event listener
    el.removeEventListener('click',function(){alert('Hi');}, false);

    即使執行了 removeEventListener 來移除事件,但 click 時仍會出現 ‘HI’。因為 addEventListenerremoveEventListener 所移除的 handler 實際上是兩個不同實體的 function 物件。

    需修改為以下:

    .\2020-02-22-斜槓的50道難題\2020-02-22-斜槓的50道難題-1.jpg

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     var el = document.getElementById("outside");
      
    // 把 event handler 先存到變數
    var clickHandler =function(){
        alert('Hi');
    };

    el.addEventListener('click', clickHandler, false);


    //成功移除 clickHandler
    el.removeEventListener('click',clickHandler, false);

    參考文獻

    1. https://blog.techbridge.cc/2017/07/15/javascript-event-propagation/

    2. https://ithelp.ithome.com.tw/articles/10191970

    3. https://ithelp.ithome.com.tw/articles/10192015

    4. http://shubo.io/event-bubbling-event-capturing-event-delegation/

    5. https://developer.mozilla.org/zh-TW/docs/Web/API/EventTarget/addEventListener

    6. https://developer.mozilla.org/zh-TW/docs/Web/API/Event/target

    7. http://www.qc4blog.com/?p=650

    JavaScript DOM 查找元素


    DOM (Document Object Model) 定義了一組標準 API (Application Programming Interface) 讓我們可以用 JavaScript 對 HTML 文件做操作。

    DOM 將一份 HTML 文件看作是一個樹狀結構的物件,讓我們可以方便直觀的存取樹中的節點 (node) 來改變其結構、樣式 (CSS) 或內容等。

    document 物件是 DOM tree 的根節點,表示整份 HTML 文件,通常你要存取 HTML 都是從 document 物件開始:



    圖片來源: Wikipedia DOM


    HTML DOM 規範中定義了這些類型的 API:

    • 讓我們可以對 HTML 的元素 (element) 當作是 JavaScript 物件 (object) 來操作
    • 定義了 HTML 元素有哪些屬性 (properties) 可以來做存取
    • 定義了 HTML 元素有哪些方法 (methods) 可以來被操作
    • 定義了 HTML 元素事件 (events),讓我們可以針對特定元素來綁定事件處理函式 (例如使用者按下滑鼠、在鍵盤打字都是所謂的事件)

    document.getElementById(id)

    透過 id 取得一個 HTML 元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!DOCTYPE html>
    <html>
    <body>

    <p id="demo">Click the button to change the text in this paragraph.</p>

    <button onclick="myFunction()">Try it</button>

    <script>
    function myFunction() {
    document.getElementById("demo").innerHTML = "Hello World";
    }
    </script>

    </body>
    </html>

    document.getElementsByTagName(name)

    用來根據 HTML 標籤 (tag) 名稱取得所有這個標籤的元素集合 (HTMLCollection),返回的結果是一個像陣列 (array) 的物件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <!DOCTYPE html>
    <html>
    <body>

    <p>An unordered list:</p>
    <ul>
    <li>Coffee</li>
    <li>Tea</li>
    <li>Milk</li>
    </ul>

    <p>Click the button to display the innerHTML of the second li element (index 1).</p>

    <button onclick="myFunction()">Try it</button>

    <p id="demo"></p>

    <script>
    function myFunction() {
    var x = document.getElementsByTagName("LI");
    document.getElementById("demo").innerHTML = x[1].innerHTML;

    }
    </script>

    </body>
    </html>

    document.getElementsByName(name)

    用來取得特定名稱 (name) 的 HTML 元素集合 (HTMLCollection),返回的結果是一個像陣列 (array) 的物件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!DOCTYPE html>
    <html>
    <body>

    First Name: <input name="fname" type="text" value="Michael"><br>
    First Name: <input name="fname" type="text" value="Doug">

    <p>Click the button to get the tag name of the first element in the document that has a name attribute with the value "fname".</p>

    <button onclick="myFunction()">Try it</button>

    <p id="demo"></p>

    <script>
    function myFunction() {
    var x = document.getElementsByName("fname")[0].tagName;
    document.getElementById("demo").innerHTML = x;

    }
    </script>

    </body>
    </html>

    document.getElementsByClassName(names)

    用來取得特定類別名稱 (class name) 的 HTML 元素集合 (HTMLCollection),返回的結果是一個像陣列 (array) 的物件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <!DOCTYPE html>
    <html>
    <body>

    <div class="example">First div element with class="example".</div>

    <div class="example">Second div element with class="example".</div>

    <p>Click the button to change the text of the first div element with class="example" (index 0).</p>

    <button onclick="myFunction()">Try it</button>

    <p><strong>Note:</strong> The getElementsByClassName() method is not supported in Internet Explorer 8 and earlier versions.</p>

    <script>
    function myFunction() {
    var x = document.getElementsByClassName("example");
    x[0].innerHTML = "Hello World!";
    }
    </script>

    </body>
    </html>

    可以使用空白隔開多個 class name,元素必須有所有指定的 class name 才符合。例如:

    1
    2
    3
    // 取得同時有 demo 和 test 兩個 class name 的所有元素

    document.getElementsByClassName('demo test');

    document.querySelector(selectors)

    使用 CSS 選擇器 (CSS selectors) 來尋找符合條件且第一個找到的 HTML 元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!DOCTYPE html>
    <html>
    <body>

    <h2 class="example">A heading with class="example"</h2>
    <p class="example">A paragraph with class="example".</p>

    <p>Click the button to add a background color to the first element in the document with class="example".</p>

    <button onclick="myFunction()">Try it</button>

    <script>
    function myFunction() {
    document.querySelector(".example").style.backgroundColor = "red";
    }
    </script>

    </body>
    </html>

    document.querySelectorAll(selectors)

    使用 CSS 選擇器 (CSS selectors) 來尋找所有符合條件的 HTML 元素集合 (NodeList)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <!DOCTYPE html>
    <html>
    <body>

    <h2 class="example">A heading with class="example"</h2>
    <p class="example">A paragraph with class="example".</p>

    <p>Click the button to add a background color all elements with class="example".</p>

    <button onclick="myFunction()">Try it</button>

    <p><strong>Note:</strong> The querySelectorAll() method is not supported in Internet Explorer 8 and earlier versions.</p>

    <script>
    function myFunction() {
    var x, i;
    x = document.querySelectorAll(".example");
    for (i = 0; i < x.length; i++) {
    x[i].style.backgroundColor = "red";
    }
    }
    </script>

    </body>
    </html>

    DOM tree 節點間位置的相互關係

    由於 DOM 節點有分層的概念,於是節點與節點之間的關係,我們大致上可以分成兩種:

    • 父子關係
      除了 document 之外,每一個節點都會有個上層的節點,我們通常稱之為「父節點」 (Parent node),而相對地,從屬於自己下層的節點,就會稱為「子節點」(Child node)。

    • 兄弟關係:有同一個「父節點」的節點,那麼他們彼此之間就是「兄弟節點」(Siblings node)。



    圖片來源:重新認識 JavaScript: Day 12 透過 DOM API 查找節點 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

    Node.childNodes

    所有的 DOM 節點物件都有 childNodes 屬性 (read-only property),可以用來取得該元素下的所有子元素集合 (NodeList)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var demo = document.querySelector('#demo');


    // 如果 node 內有子元素
    if( demo.hasChildNodes() ) {

    // 可以透過 demo.childNodes[n] (n 為數字索引) 取得對應的節點

    // 注意,NodeList 物件內容為即時更新的集合
    for (var i = 0; i < demo.childNodes[i].length; i++) {
    // ...
    };
    }

    Node.childNodes 回傳的可能會有這幾種:

    • HTML 元素節點 (element nodes)
    • 文字節點 (text nodes),包含空白
    • 註解節點 (comment nodes)

    Node.children

    DOM 節點物件的 children 屬性和 childNodes 屬性類似,差異在於 childNodes 返回的子元素會包含文字節點 (text nodes) 和註解節點 (comment nodes),children 屬性則只會返回 HTML 元素節點 (HTMLCollection)。

    Node.firstChild

    Node.firstChild 可以取得 Node 節點的第一個子節點,如果沒有子節點則回傳 null

    要注意的是,子節點包括「空白」節點,所以像下面範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <p>
    <span>span 1</span>
    <span>span 2</span>
    <span>span 3</span>
    </p>

    <script>
    var p = document.querySelector('p');

    // tagName 屬性可以取得 node 的標籤名稱
    console.log(p.firstChild.tagName); // undefined
    </script>

    因為拿到的是 <p> 與第一個 <span> 中間的「換行字元」,所以 p.firstChild.tagName 會得到 undefined

    另一個例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <p id="demo">

    <span>First span</span>
    </p>

    <script>
    var p = document.getElementById('demo');

    // 會顯示 "#text",因為第一個子元素是換行字元

    alert(p.firstChild.nodeName);
    </script>

    可以把第一個例子修改成以下:

    1
    2
    3
    4
    5
    6
    7
    8
    <p><span>span 1</span><span>span 2</span><span>span 3</span></p>

    <script>
    var p = document.querySelector('p');

    // tagName 屬性可以取得 node 的標籤名稱
    console.log(p.firstChild.tagName); // "SPAN"
    </script>

    把中間的換行與空白移除,就會得到預期中的 "SPAN" 了。

    第二個例子修改方式也一樣:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <p id="demo"><span>First span</span></p>


    <script>
    var p = document.getElementById('demo');

    // 會顯示 "SPAN"
    alert(p.firstChild.nodeName);
    </script>

    Node.lastChild

    Node.lastChild 可以取得 Node 節點的最後一個子節點,如果沒有子節點則回傳 null

    Node.firstChild 一樣的是,子節點包括「空白」節點:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <p id="demo"><span>First span</span><span>Second span</span><span>Last span</span></p>


    <script>
    var p = document.getElementById('demo');

    // 會顯示 "Last span"
    alert(p.lastChild.innerHTML);
    </script>

    Node.parentNode

    透過 Node.parentNode 可以用來取得父元素,回傳值可能會是一個元素節點 (Element node)、根節點 (Document node) 或 DocumentFragment 節點。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <p>
    <span id="demo">my span</span>
    </p>


    <script>
    var elem = document.getElementById('demo');

    // 會顯示 "P"
    alert(elem.parentNode.nodeName);
    </script>

    Node.previousSibling

    透過 Node.previousSibling 可以取得同層之間的「前一個」節點,如果 node 已經是第一個節點,則回傳 null

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div><span id="s1">s1</span><span id="s2">s2</span></div>

    <script>
    // 會顯示 null
    alert(document.getElementById('s1').previousSibling);

    // 會顯示 "s1"
    alert(document.getElementById('s2').previousSibling.id);
    </script>

    第二個例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <p><span>span 1</span><span>span 2</span><span>span 3</span></p>

    <script>
    var el = document.querySelector('span');
    console.log( el.previousSibling ); // null

    // document.querySelectorAll 會取得所有符合條件的集合,
    // 而 document.querySelectorAll('span')[2] 指的是「第三個」符合條件的元素。
    var el2 = document.querySelectorAll('span')[2];
    console.log( el2.previousSibling.textContent ); // "span 2"
    </script>

    Node.nextSibling

    透過 Node.previousSibling 可以取得同層之間的「下一個」節點,如果 node 已經是最後一個節點,則回傳 null

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div><span id="s1">s1</span><span id="s2">s2</span></div>

    <script>
    // 會顯示 "s2"
    alert(document.getElementById('s1').nextSibling.id);

    // 會顯示 null
    alert(document.getElementById('s2').nextSibling);
    </script>

    第二個例子:

    1
    2
    3
    4
    5
    6
    7
    8
    <p><span>span 1</span><span>span 2</span><span>span 3</span></p>

    <script>
        // document.querySelector 會取得第一個符合條件的元素
        var el = document.querySelector('span');

        console.log( el.nextSibling.textContent ); // "span 2"
    </script>

    上面介紹的很多 DOM 查找方式會返回一個元素集合,是一個像陣列的物件 - 有 length 屬性、可以用 for 迴圈遍歷結果,雖然不能使用陣列型別的 method,但這兩種都可以用「陣列索引」的方式來存取內容。

    而 NodeList 和 HTMLCollection 的差別在於,NodeList 包含任何的節點類型,除了 HTML element 節點,也包含文字節點、屬性節點等。HTMLCollection 則只包含 HTML 元素節點 (Element nodes)

    像是 document.getElementsBy** (注意,有個 s) 以及 document.querySelectorAll 分別回傳 「HTMLCollection」 與 「NodeList」。

    另一個需要注意的地方是,HTMLCollection / NodeList 在大部分情況下是即時更新的,但透過 document.querySelectorAll 會回傳一個靜態的 NodeList

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div id="outer">
    <div id="inner">inner</div>
    </div>

    <script>

    // <div id="outer">
    var outerDiv = document.getElementById('outer');

    // 所有的 <div> 標籤
    var allDivs = document.getElementsByTagName('div');

    console.log(allDivs.length); // 2

    // 清空 <div id="outer"> 下的節點
    outerDiv.innerHTML = '';

    // 因為清空了<div id="outer"> 下的節點,所以只剩下 outer
    console.log(allDivs.length); // 1
    </script>

    如果改成 document.querySelector 的寫法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div id="outer">
    <div id="inner">inner</div>
    </div>

    <script>

    // <div id="outer">
    var outerDiv = document.getElementById('outer');

    // 所有的 <div> 標籤
    var allDivs = document.querySelectorAll('div');

    console.log(allDivs.length); // 2

    // 清空 <div id="outer"> 下的節點
    outerDiv.innerHTML = '';

    // document.querySelector 回傳的是靜態的 NodeList,不受 outerDiv 更新影響
    console.log(allDivs.length); // 2
    </script>

    參考文獻

    1. https://ithelp.ithome.com.tw/articles/10191765

    2. https://www.fooish.com/javascript/dom/

    3. https://www.fooish.com/javascript/dom/traversing.html

    JavaScript DOM Node(新增、修改、刪除)


    除了在之前提過的 DOM Node 的類型、以及節點之間的查找與關係。我們在這一篇文章會介紹其他新增、修改和刪除 DOM 節點的方法。

    document.createElement(tagName)

    用來建立一個新的元素。

    在建立新的 p 元素 newP 後,這時候我們在瀏覽器上還看不到它,直到透過 appendChild()insertBefore()replaceChild() 等方法將新元素加入至指定的位置之後才會顯示。

    1
    2
    3
    // 建立一個新的 <p>

    var newP = document.createElement('p');

    新建立的 newP 我們也可以同時對它指定屬性:

    1
    2
    3
    newP.id = "myNewP";

    newP.className = "demo";

    document.createTextNode()

    用來建立文字節點,在 TextNode 被加入至某個節點後,才會顯示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var newSpan = document.createElement('span');


    // 建立 textNode 文字節點
    var textNode = document.createTextNode("Hello world!");

    // 透過 newSpan.appendChild 將 textNode 加入至 newSpan

    newSpan.appendChild(textNode);

    document.createDocumentFragment()

    它是一種沒有父層節點的「片段文件結構」,透過操作 DocumentFragment 與直接操作 DOM 最關鍵的區別在於 DocumentFragment 不是真實的 DOM 結構,所以 DocumentFragment 的變動並不會影響目前的網頁文件。

    我們把可以先建立一個DocumentFragment,把所有的新節點先放到文檔碎片裡面,然後再一次性地添加至 document中,這樣就減少了頁面渲染 DOM 元素的次數。當需要進行大量的 DOM 操作時,使用 DocumentFragment 會比直接操作 DOM 的效能要好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <!DOCTYPE html>
    <html>
    <body>

    <p>Click the button to make changes to a list item, using the createDocumentFragment method, then appending the list item as the last child of the list.</p>

    <ul id="myList"><li>Coffee</li><li>Tea</li></ul>

    <script>
    // 取得外層容器 myList
    var ul = document.getElementById("myList");

    // 建立一個 DocumentFragment,可以把它看作一個「虛擬的容器」
    var fragment = document.createDocumentFragment();

    for (var i = 0; i < 3; i++){
    // 生成新的 li,加入文字後置入 fragment 中。
    let li = document.createElement("li");
    li.appendChild(document.createTextNode("Item " + (i+1)));
    fragment.appendChild(li);

    }

    // 最後將組合完成的 fragment 放進 ul 容器
    ul.appendChild(fragment);
    </script>

    </body>
    </html>

    ParentNode.appendChild(childNode)

    用來新增一個子節點到指定元素節點的最後面:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <!DOCTYPE html>
    <html>
    <body>

    <div id="name"><span>hello</span></div>

    <script>
    // 建立一個新 <div>
    var newDiv = document.createElement('div');

    // 建立一個新的文字節點
    var newContent = document.createTextNode('I am Cheng-Yi-Ting.');

    // 將文字節點加到剛建立的 <div> 元素中
    newDiv.appendChild(newContent);

    // 取得目前頁面上的 name 元素
    var currentDiv = document.getElementById('name');

    // 將剛建立的 <div> 元素加入 name 元素中
    currentDiv.appendChild(newDiv);

    // 顯示 <span>hello</span><div>I am Cheng-Yi-Ting.</div>
    alert(currentDiv.innerHTML);
    </script>

    </body>
    </html>

    ParentNode.insertBefore(newNode, referenceNode)

    用來將一個新的元素加到某個元素的前面。將新節點 newNode 插入至指定的 referenceNode 節點的前面:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <!DOCTYPE html>
    <html>
    <body>

    <div id="demo">
    <span id="s1">安安</span>
    <span id="s2">你好</span>
    </div>

    <script>
    // 建立一個新的 <span>
    var newSpan = document.createElement('span');
    // 增添一些內容
    newSpan.innerHTML = 'Hi';

    // 取得目前頁面上的 demo 元素
    var demo = document.getElementById('demo');

    // 取得目前頁面上的 s2 元素
    var s2 = document.getElementById('s2');

    // 將新的 span 元素放到 demo 元素中的 s2 子元素前面
    demo.insertBefore(newSpan, s2);

    // 顯示 <span id="s1">安安</span><span>Hi</span><span id="s2">你好</span>
    alert(demo.innerHTML);
    </script>

    </body>
    </html>

    ParentNode.removeChild(childNode)

    將指定元素的某個指定的子節點移除:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <!DOCTYPE html>
    <html>
    <body>

    <!-- Note that the <li> elements inside <ul> are not indented (whitespaces).
    If they were, the first child node of <ul> would be a text node
    -->
    <ul id="myList">
    <li>Coffee</li>
    <li>Tea</li>
    <li>Milk</li>
    </ul>

    <p>Click the button to remove the first item from the list.</p>

    <button onclick="myFunction()">Try it</button>

    <script>
    function myFunction() {
    var list = document.getElementById("myList");
    list.removeChild(list.childNodes[0]);
    }
    </script>

    </body>
    </html>

    ParentNode.replaceChild(newChild, oldChild)

    用新節點來取代某個子節點,這個新節點可以是某個已存在的節點或是新節點。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    <!DOCTYPE html>
    <html>
    <body>


    <div>
    <span id="childSpan">foo bar</span>
    </div>

    <script>


    // Create an empty element node
    // without an ID, any attributes, or any content
    var sp1 = document.createElement("span");

    // Give it an id attribute called 'newSpan'
    sp1.id = "newSpan";

    // Create some content for the new element.
    var sp1_content = document.createTextNode("new replacement span element.");

    // Apply that content to the new element
    sp1.appendChild(sp1_content);

    // Build a reference to the existing node to be replaced
    var sp2 = document.getElementById("childSpan");
    //<span id="childSpan">foo bar</span>

    var parentDiv = sp2.parentNode;


    // Replace existing node sp2 with the new span element sp1
    parentDiv.replaceChild(sp1, sp2);

    // Result:
    // <div>
    // <span id="newSpan">new replacement span element.</span>
    // </div>
    </script>

    </body>
    </html>

    document.write(html)

    document 物件要將某內容寫入網頁可以用 write() 方法,當瀏覽器讀取頁面,解析到 document.write() 的時候就會停下來,並且將 HTML 內容輸出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <html>
    <head>
    <title>document.write example</title>
    </head>
    <body>
    <p>first paragraph</p>
    <script>
    document.write('<p>second paragraph</p>');
    </script>
    </body>
    </html>

    如果你在頁面載入後,才執行 document.write 則會將裡面的內容完全覆蓋目前的畫面,現在實務上也比較少在使用 document.write

    1
    2
    3
    window.onload = function(){
    document.write("Hello world!");
    };

    參考文獻

    1. https://ithelp.ithome.com.tw/articles/10191867

    2. https://www.fooish.com/javascript/dom/manipulating.html

    script tag 的 async 和 defer 屬性


    script標籤

    針對 <script> 標籤放哪裡,一般會有兩種版本:

    • 放在 <head> ... </head> 之間
    • 放在 </body> 之前

    <script> 標籤放在 </body> 之前,網頁可以正常運作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE html>
    <html>

    <head>
    <meta charset="utf-8">
    </head>

    <body>
    <h1 id="title"></h1>
    <script>
    document.querySelector('#title').textContent = "這是標題"
    </script>
    </body>

    </html>

    接著,我們試著把 <script> 標籤移到 <head> ... </head> 之間,這時候會發現網頁一片空白:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html>

    <head>
    <meta charset="utf-8">

    <script>
    document.querySelector('#title').textContent = "這是標題"
    </script>
    </head>

    <body>
    <h1 id="title"></h1>

    </body>

    </html>

    圖片來源:Asynchronous vs Deferred JavaScript



    瀏覽器解析 HTML 是一行一行依序向下讀取,在傳統的寫法中,當瀏覽器讀到 <script> 時,便會 暫停解析 DOM,立刻開始下載 <script> 的資源,並在下載完成後立刻執行。由於這樣的特性,便可能造成在 DOM 樹建構不完全時就執行 JavaScript,其中需要操作 DOM 的程式可能就因此無法正確運作,許多衍伸的問題也就因此產生;若是透過 src 外聯 .js 檔案,瀏覽器會「同步地」下載 .js 檔案,在下載完成並執行完程式碼之前,後續的其他資源下載、頁面剖析等,都會被阻斷。執行過程中,使用者便會卡在白畫面,並產生覺得網站太慢、使用者體驗不好等感受。

    不過,文件資源的完整載入,是指 HTMLCSS 、圖片等都載入完成,而不單指 DOM樹 建立完成;若想在文件剖析完成、 DOM樹 生成時就執行程式碼,建議是將 script 放在文件尾端,通常是 body 標籤之前,因為此時 DOM樹 已經建立,操作 DOM 節點就不是問題了。但在更複雜的網站中,HTMLJavaScript 的檔案都來越大,下載、執行時間也越來越長,需要等到整個 DOM 樹都載入完成才開始下載 <script> 內的資源,從網站讀取完成到可操作之間便會有明顯的延遲感。


    這樣的問題該怎麼解決呢?

    async & defer

    從 HTML4 開始,<script> 便多了 defer 屬性,HTML5 則多了 async,兩者皆是用來幫助開發者控制 <script> 內資源的載入及執行順序,以及避免 DOM 的解析被資源下載卡住。這兩個屬性只適用在外部引入的檔案,對內嵌程式碼的script標籤沒有影響。

    defer

    defer 意旨為 延遲(Deferred),也就是說,加上 defer 屬性後,瀏覽器會繼續解析、渲染畫面,而不會因為需要載入 <script> 內的資源而卡在那邊等;如果有多個設置 deferjs 標籤存在,則會由上到下依照擺放順序觸發。實際上的執行時間,會在 DOM渲染完畢後,DOMContentLoaded 事件執行之前。

    1
    <script defer src="script.js">/script>

    圖片來源:Asynchronous vs Deferred JavaScript



    defer 屬性告訴瀏覽器在 HTML 還在解析時加載 js,但是等到 HTML 整個解析完才執行 js。

    async

    async 即為 非同步(Asynchronous),在 <script> 加上 async 屬性後,與 defer 相同的是會在背景執行下載,但不同的是當下載完成會馬上暫停 DOM 解析(若尚未解析完的話),並開始執行 JavaScript。也因為下載完成後會立即執行,如果有多個 async 屬性的 js ,先下載完的就會先執行,因為下載完成的順序是無法預測的,因此「執行順序也就無法預測」。

    1
    <script async src="script.js"></script>

    圖片來源:Asynchronous vs Deferred JavaScript



    async 屬性告訴瀏覽器可以異步執行(executed asynchronously),在 HTML 還在解析時加載 js ,當 js 完全下載後才暫停解析 HTML ,執行 js

    type=”module”

    在主流的現代瀏覽器中,<script> 的屬性可以加上 type="module"。這時,瀏覽器會將此檔案認為是一個 JavaScript 模組,其中的解析規則、執行環境會略有不同;這時候的 <script> 預設行為會像是 defer 一樣,背景下載,且等待 DOM 解析、渲染完成後才執行,也因此 defer 屬性無法在 type="module" 產生作用。但同樣可以透過 async 屬性讓它在下載完成後即刻執行。

    使用場景

    前面已經針對這兩個屬性進行說明了,那麼該如何正確地使用呢?

    defer 由於背景載入、不打斷渲染及確保執行順序的特色,基本上沒特別需求的話,建議設定在 <script> ;當然,<script> 本身的擺放順序還是要稍微留心注意。
    async 比較特別,因為下載後會立刻執行,且不保證順序,一般常見的應用是設定在完全獨立的小模組,例如 背景 Log、頁面廣告等等,在避免造成使用者體驗變差的同時,盡量提早開始產生效果。

    asyncdefer 是專屬於 <script> 的屬性,而網頁中的其他資源,我們可以透過 <link>preloadprefetch 屬性,來幫我們 延遲載入 未來才需要用到的資源,詳細的請參考 Preload vs Prefetch

    參考文獻

    1. https://bitsofco.de/async-vs-defer/

    2. https://ithelp.ithome.com.tw/articles/10216858?source=post_page—–8205fddbbafc———————-

    3. https://ithelp.ithome.com.tw/articles/10208563

    4. https://ithome.com.tw/voice/132470

    5. https://www.cnblogs.com/jiasm/p/7683930.html

    6. https://kknews.cc/code/4eoxg4v.html

    BOM 和 DOM !?


    BOM 是什麼?

    BOM (Browser Object Model,瀏覽器物件模型),是瀏覽器所有功能的核心,與網頁的內容無關。

    BOM 也有人非正式地稱它為 「Level 0 DOM」。 因為它在 DOM level 1 標準前就已存在,而不是真的有文件去規範這些,所以「Level 0 DOM」與「BOM」兩者實際上指的是同一個東西。

    
    window|  
          |navigator  
          |location  
          |frames  
          |screen  
          |history  
          |document  
                  |forms  
                  |links  
                  |anchors  
                  |images  
                  |all  
                  |cookie
    
    

    window

    window 物件代表瀏覽器視窗本身,擁有一些控制視窗的方法,其中像是 open()、close()、alert()、prompt()、confirm()、setTimeout()等函式,都是以 window 作為名稱空間物件的函式。

    你可以在 W3schools 查詢關於 window 函式或方法的使用方式,在使用 window 物件時,可省略 window 關鍵字,直接使用該函式或方法即可。

    開啟一個新視窗:

    1
    window.open("https://www.w3schools.com");

    開啟一個確認視窗:

    1
    confirm("Press a button!");

    點擊按紐後,等待三秒鐘,開啟一個警告視窗:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!DOCTYPE html>
    <html>
    <body>

    <p>Click the first button to alert "Hello" after waiting 3 seconds.</p>

    <button onclick="myFunction()">Try it</button>


    <script>
    var myVar;

    function myFunction() {
    myVar = setTimeout(function(){ alert("Hello"); }, 3000);
    }

    </script>

    </body>
    </html>

    window.navigator

    navigator 物件主要是包含了瀏覽器的資訊,像是取得瀏覽器的版本、語言以及是否啟用 cookie 等資訊,你可以在 W3schools 查詢這個物件上有哪些資訊可以取得。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!DOCTYPE html>
    <html>
    <body>
    <div id="demo"></div>
    <script>
    var txt = "";
    txt += "<p>Browser CodeName: " + navigator.appCodeName + "</p>";
    txt += "<p>Browser Name: " + navigator.appName + "</p>";
    txt += "<p>Browser Version: " + navigator.appVersion + "</p>";
    txt += "<p>Cookies Enabled: " + navigator.cookieEnabled + "</p>";
    txt += "<p>Browser Language: " + navigator.language + "</p>";
    txt += "<p>Browser Online: " + navigator.onLine + "</p>";
    txt += "<p>Platform: " + navigator.platform + "</p>";
    txt += "<p>User-agent header: " + navigator.userAgent + "</p>";
    document.getElementById("demo").innerHTML = txt;
    </script>
    </body>
    </html>

    輸出結果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    Browser CodeName: Mozilla

    Browser Name: Netscape

    Browser Version: 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36

    Cookies Enabled: true

    Browser Language: zh-TW

    Browser Online: true

    Platform: Win32

    User-agent header: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36

    window.location

    location 物件可以取得瀏覽器目前頁面的URL資訊,也有 reload() 與 replace() 方法,可以重新載入頁面或取代頁面。,你可以在W3schools查詢這個物件上有哪些資訊可以取得。

    取代目前頁面的URL:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE html>
    <html>
    <body>

    <button onclick="myFunction()">Replace document</button>

    <script>
    function myFunction() {
    location.replace("https://cheng-yi-ting.github.io/") // 取代目前的頁面為此URL

    }
    </script>

    </body>
    </html>

    取得目前URL所採用的協定:

    1
    location.protocol; //  https

    window.frames

    frames 物件可以取得瀏覽器中擁有的框架資訊,索引位置是框架在視窗中出現的順序,如果框架有設定id或name屬性,也可以使用[]搭配名稱來取得框架。

    點擊按紐後,更改第一個 iframe 的來源:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!DOCTYPE html>
    <html>
    <body>

    <p>Click the button to change the location of the first iframe element (index 0).</p>

    <button onclick="myFunction()">Try it</button>
    <br><br>

    <iframe src="https://zh.wikipedia.org/wiki/Wiki"></iframe>
    <iframe src="https://www.ettoday.net/"></iframe>

    <script>
    function myFunction() {
    window.frames[0].location = "https://cheng-yi-ting.github.io/";
    }
    </script>

    </body>

    </html>

    點擊按鈕後,修改視窗內所有 iframe 來源URL:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <!DOCTYPE html>
    <html>
    <body>
    <p>Click the button to loop through the frames on this page, and change the location of every frame to "w3schools.com".
    </p>

    <button onclick="myFunction()">Try it</button>
    <br><br>

    <iframe src="https://zh.wikipedia.org/wiki/Wiki"></iframe>
    <iframe src="https://www.ettoday.net/"></iframe>
    <iframe src="https://tw.appledaily.com/new/realtime"></iframe>

    <script>
    function myFunction() {
    var frames = window.frames;
    var i;

    for (i = 0; i < frames.length; i++) {
    frames[i].location = "https://cheng-yi-ting.github.io/";
    }
    }
    </script>

    </body>

    </html>

    window.screen

    screen 物件可以取得目前視窗的螢幕資訊,像是寬、高、顏色深度等,你可以在W3schools查詢這個物件上有哪些資訊可以取得。

    點擊按紐後,顯示視窗寬度:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!DOCTYPE html>
    <html>
    <body>

    <p>Click the button to display the available width of your screen.</p>

    <button onclick="myFunction()">Try it</button>

    <p id="demo"></p>

    <script>
    function myFunction() {
    var x = "Available Width: " + screen.availWidth + "px";
    document.getElementById("demo").innerHTML = x;
    }
    </script>

    </body>
    </html>

    window.history

    history 物件可以取得瀏覽器瀏覽歷史,基於安全與隱私,你無法取得瀏覽歷史,但可以有back()、forward()、go()等方法,指定前進、後退至哪個歷史頁面,像是回到上一面、下一頁的功能。你可以在W3schools查詢這個物件上有哪些資訊可以取得。

    點擊按紐後,回到上一頁的URL:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <!DOCTYPE html>
    <html>
    <body>

    <button onclick="goBack()">Go Back</button>

    <p>Notice that clicking on the Back button here will not result in any action, because there is no previous URL in the history list.</p>

    <script>
    function goBack() {
    window.history.back();
    }
    </script>

    </body>
    </html>

    window.document

    DOM (Document Object Model,文件物件模型),是一個將 HTML 文件以樹狀的結構來表示的模型,而組合起來的樹狀圖,我們稱之為「DOM Tree」。。

    舉個來說,如果有個網頁如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <html>
    <head>
    <title>網站標題</title>

    </head>

    <body>
    <h1>這是一個內文標題</h1>

    <p>這是一個段落</p>

    </body>

    </html>

    DOM API 就是定義了讓 JavaScript 可以存取、改變 HTML 架構、樣式和內容的方法,甚至是對節點綁定的事件。

    JavaScript 就是透過 DOM 提供的 API 來對 HTML 做存取與操作。

    參考文獻

    1. https://openhome.cc/Gossip/JavaScript/Level0DOM.html

    2. https://ithelp.ithome.com.tw/articles/10191666

    JavaScript 邏輯運算子


    邏輯運算子 (Logical Operators)

    邏輯運算子用來做布林值 (boolean) 的運算,運算結果傳回 true 或 false。

    運算子 語法 說明
    && a && b 如果 a 和 b 都是 true,就會傳回 true, 否則傳回 false
    || a || b 如果 a 或 b 是 true,就會傳回 true,否則傳回 false
    ! ! a 如果 a 是 true,就傳回 false,否則傳回 true

    在真假判斷式中,所有東西都可以轉換為布林值,而除了以下運算元可以被轉換為 false,其他的值都是 true。

    • Undefined

    • Null

    • +0, -0, or NaN

    • 空字串 ""''

    1
    2
    3
    4
    5
    Boolean(false)      // false
    Boolean("false") // true
    Boolean("0") // true
    Boolean("")         // false
    Boolean("''")     // true

    &&|| 還有比較特別的地方,如果運算元的值不是布林值,實際上會傳回其中一個運算元的值。

    運算子 語法 說明
    && a && b 假如 a 可以被轉換成 true 的話,則回傳第二個數值,否則回傳第一個數值。
    || a || b 假如 a 可以被轉換成 true 的話,則回傳第一個數值,否則回傳第二個數值。

    範例


    Logical AND (&&)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    a1 = true  && true       // t && t returns true
    a2 = true && false // t && f returns false
    a3 = false && true // f && t returns false
    a4 = false && (3 == 4) // f && f returns false
    a5 = 'Cat' && 'Dog' // t && t returns "Dog"
    a6 = false && 'Cat' // f && t returns false
    a7 = 'Cat' && false // t && f returns false
    a8 = '' && false // f && f returns ""
    a9 = false && '' // f && f returns false

    Logical OR (||)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    o1 = true  || true       // t || t returns true
    o2 = false || true // f || t returns true
    o3 = true || false // t || f returns true
    o4 = false || (3 == 4) // f || f returns false
    o5 = 'Cat' || 'Dog' // t || t returns "Cat"
    o6 = false || 'Cat' // f || t returns "Cat"
    o7 = 'Cat' || false // t || f returns "Cat"
    o8 = '' || false // f || f returns false
    o9 = false || '' // f || f returns ""
    o10 = false || varObject // f || object returns varObject

    Logical NOT (!)

    1
    2
    3
    4
    n1 = !true               // !t returns false
    n2 = !false // !f returns true
    n3 = !'' // !f returns true
    n4 = !'Cat' // !t returns false

    Double NOT (!!)

    1
    2
    3
    4
    5
    6
    n1 = !!true                   // !!truthy returns true
    n2 = !!{} // !!truthy returns true: any object is truthy...
    n3 = !!(new Boolean(false)) // ...even Boolean objects with a false .valueOf()!
    n4 = !!false // !!falsy returns false
    n5 = !!"" // !!falsy returns false
    n6 = !!Boolean(false) // !!falsy returns false

    參考文獻

    1. https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Expressions_and_operators

    2. https://ithelp.ithome.com.tw/articles/10191343

    3. https://www.fooish.com/javascript/operator.html