JavaScript - Async/Await




Async functions

在上一篇中我們提到了 Promise ,避免使用多層 callback 所造成程式碼難以維護的問題。而 ES7 提供了 async/await 語法,可以看成是 Promise 的語法糖,更方便的寫出非同步程式碼。

async 所宣告的函式,會回傳一個 Promise ,使用方法就是在宣告的函式前增加 async 關鍵字,說明這是一個非同步的函式。

下面的範例相當於回傳一個 resolved promise:

1
2
3
async function f() {
return 1;
}

也可以寫成這樣:

1
2
3
async function f() {
return Promise.resolve(1);
}

Await

await 只能在 async 函式中使用,否則會拋出 syntax error 。 await 表示等待 Promise 的狀態為 resolve 或 reject 才會繼續執行函式,讓我們看一下例子:

1
2
3
4
5
6
7
8
9
10
11
12
async function main() {

let p = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});

let result = await p; // 等待 promise resolves
console.log(result); // "done!"

}

main();

範例中的程式會等待 p 一秒鐘後才會進行輸出。

下面為一個簡易的 async/await 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function p(value) {
return new Promise((resolve, reject) => {
setTimeout(function() {
value++
resolve(value)
}, 1000)
})
}

async function main() {
let x1 = await p(1);
let x2 = await p(2);
let x3 = await 3;

console.log(x1 + x2 + x3); // 8
}

main();

因為 async 函式相當於回傳一個 Promise ,所以我們一樣可以用操作 Promise Chain 的方式,把 Promise 一層一層的往下傳:

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
async function main() {
console.log("Start main function" );
let x1 = await p1(1); // 2
let x2 = await p2(2); // 4
let x3 = await 3; // 3
await p2(4); // 6
return x1 + x2; // 6
}

main().then(value => {
return value + 2 ; // 8
}).then(p1)
.then((value) => {
console.log(value); // 9
}).catch((error) => {
console.log('error:', error)
})

function p1(value) {
console.log("P1 value is :" + value);
return new Promise((resolve, reject) => {
setTimeout(function() {
value++
resolve(value)
}, 1000)
})
}

function p2(value) {
console.log("P2 value is :" + value);
return new Promise((resolve, reject) => {
setTimeout(function() {
value+=2
resolve(value)
}, 1000)
})
}

/*Output:
Start main function
P1 value is :1
P2 value is :2
P2 value is :4
P1 value is :8
9
/*

await 可以搭配 Promise.all 和 Promise.race 來使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function main() {
console.log("Start main function" );
const [x1, x2] = await Promise.all([p(1000), p(2000)]);
const x3 = await Promise.race([p(1000), p(2000), p(3000)]);
return x1 + x2 + x3; // 4000
}

main().then(value => {
console.log(`value is ${value}`);
})

function p(value) {
return new Promise((resolve, reject) => {
setTimeout(function() {
if (value < 1000 ){
        reject('Unexpected condition')
}
resolve(value)
}, value)
})
}

錯誤處理

錯誤處理的方式使用 try / catch :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const isEven = (num) => {
return new Promise((resolve, reject) => {
if (num % 2 == 0) {
resolve('even');
} else {
reject('odd');
}
});
}

const verify = async (num) => {
try {
const sign = await isEven(num);
console.log(sign);
} catch (err) {
console.log(err);
}
}

verify(8); // even
verify(15); // odd
verify('Ian'); // odd

只用 catch 也行:

1
2
3
4
5
6
const verify = async (num) => {
const sign = await isEven(num);
console.log(sign);
}

verify('Ian').catch(err => console.log(err)); // odd

如果是採用以下的寫法,因為 await isEven(15) 的結果會是 reject ,所以在 x2 就會進入到 catch ,表示後面的 x3 就不會執行:

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 isEven = (num) => {
return new Promise((resolve, reject) => {
if (num % 2 == 0) {
resolve('even');
} else {
reject('odd');
}
});
}

const verify = async () => {
const x1 = await isEven(8);
const x2 = await isEven(15);
const x3 = await isEven('Ian');
return x1
}

verify()
.then((value) => {
console.log(value)
})
.catch((error) => {
console.log('error:', error)
})

async/await and promise.then/catch

透過在 await 我們就可以很容易的控制非同步執行, 再搭配 try / catch 來處理例外狀況,因為在全域環境中無法使用 await ,所以這時候就可以透過 .then / catch 來控制整個流程。

參考文獻

  1. [JS] Async and Await in JavaScript | PJCHENder 私房菜
  2. JS 原力覺醒 Day16 - Async / Await:Promise 語法糖
  3. Day25 優雅的 Await、Async
  4. Async/await
  5. How to use async/await in JavaScript

JavaScript - Promise




過去以 Callback 撰寫非同步會容易在呼叫多層之後產生「Callback Hell」難以維護的問題,而 ES6 提供 Promise 來更方便的執行非同步。關於 JavaScript 執行方式、非同步及 Callback 原理解釋請參考前文:

在還沒有 Promise 的時候,我們為了控制多個函式間執行的順序,就會透過 Callback 的形式將 function 當作參數傳進去:

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

callback();
}, 1000);
};


var second=function(){
console.log("This is second.");

};
// 將 second 作為參數帶入 first()
first(second);
//"This is first."
//"This is second."

無論 first() 在執行的時候要等多久, second() 都會等到 first() 執行完才會執行。也可以寫成下面這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function first(callback) {
setTimeout(() => {
    console.log("This is first.");
callback()
}, 1000)
}

function second(callback) {
callback(() => {
console.log("This is second.");
})
}
second(first)

//"This is first."
//"This is second."

如果再增加 Callback 的數量:

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
function first(callback2) {
setTimeout(() => {
    console.log("This is first.");
callback2()
}, 1000)
}

function second(callback) {
setTimeout(() => {
    console.log("This is second.");
callback()
}, 1000)

}

function third() {
console.log("This is third.");

}

function main(callback) {
first(() => {
second(() => {
callback()
})
})
}
main(third)

//"This is first."
//"This is second."
//"This is second."

當 Callback 太多層就會變成非常複雜的巢狀結構:

1
2
3
4
5
6
7
8
9
first(() => {
second(() => {
third(() => {
fourth(() => {
// ......
})
})
})
})

Promise

Promise 提供的新語法讓非同步更加直觀,讓我們看一下它的建構函式:

1
new Promise( function(resolve, reject) { ... } )

下面是箭頭函式的寫法:

1
new Promise( (resolve, reject) => { ... } )

Promise 的狀態與流程

Promise 有三種 state(狀態)

  • pending:等待(還在執行中且結束狀態未知)

  • resolved/fullfilled:完成/成功

  • rejected:拒絕/失敗

Promise 運作流程圖:

來源:MDN

Promise 物件中包含兩個參數: resolverejectthen 會接收 resolve 的 value, catch 則接收 reject 的 value。

讓我們看一個基本的 Promise 範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p = new Promise(function(resolve, reject) {
setTimeout(function(){
let value = 1
console.log(value);
resolve(value);
}, 1000);
});

//Promise fullfilled handler
p.then((value) => {
// 在 p 被 resolve 時執行
console.log('success:' + value)
})
//Promise rejected handler
p.catch((error) => {
// 在 p 被 reject 時執行
console.log('error:', error)
})

Pending

Promise 沒有 resolve 或是 reject 的時候,程式將會一直處在 pending 狀態:

1
2
3
4
5
6
7
var p = new Promise(function(resolve, reject) {});
p.then((value) => {
console.log('success');
})
p.catch((error) => {
console.log('error:', error);
})

例如:送出一個請求一直沒有收到回應時, Promise 就會一直處於 pending 狀態。

Promise Chain

我們可以在 Promise 使用 .then 來進行串接,透過在 .then 裡面 return 一個值,讓這個回傳值以 Promise 物件的形式傳到下一個 .then ,形成 Promise Chain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const p = new Promise(function(resolve, reject) {
setTimeout(function(){
let value = 1;
resolve(value);
}, 1000);
});

p.then((value) => {
// 得到 resolve 內的值
console.log(value); // 1
return value+1;
}).then((value) => {
// 得到上一個 .then return 的值
console.log(value); // 2
return value+1;
}).then((value) => {
// 得到上一個 .then return 的值
console.log(value); // 3
}).catch((error) => {
console.log('error:', error)
})

catch 如果是串在中間,在 new Promise 的時候是 reject ,會導致前面 .then 都不會被執行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p = new Promise(function(resolve, reject) {
setTimeout(function(){
let value = 1;
reject(value);
}, 1000);
});

p.then((value) => {
console.log('Start');
return value+1;
}).catch((error) => {
console.log('error:', error)
}).then((value) => {
console.log('End');
})

//error: 1
//End

Promise 使用範例

我們可以在 .thenreturn 一個 new Promise

範例一:

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
const p = new Promise((resolve, reject) => {
setTimeout(() => {
let value = 1;
resolve(value)
}, 1000)
})

p.then((value) => {
console.log("This is the first value :" + value);
return value + 1
})

.then((value) => {
console.log("This is the second value :" + value);
// return 一個 new Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(value + 2)
}, 1000)
})
})

// 等 Promise resolve 後才會執行。
.then((value) => {
console.log("This is the third value :" + value);
})
.catch((error) => {
console.log('error:', error)
})

/*Output:
This is the first value: 1
This is the second value: 2
This is the third value: 4
/*

範例二:

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
function p(value) {
return new Promise((resolve, reject) => {
setTimeout(function() {
value++
resolve(value)
}, 1000)
})
}

p(1)
.then((value) => {
console.log("This is the first value :" + value);
return p(value+1)
})
.then((value) => {
console.log("This is the second value :" + value);
return p(value+1)
})
.then((value) => {
console.log("This is the third value :" + value);
})

/*Output:
This is the first value: 2
This is the second value: 4
This is the third value: 6
/*

範例三:

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
function p(value) {
console.log("This value is :" + value);
return new Promise((resolve, reject) => {
setTimeout(function() {
value++
resolve(value)
}, 1000)
})
}

p(1)
.then(p)
.then((value) => {
console.log(value)
return value+2
})
.then(p)
.catch((error) => {
console.log('error:', error)
})

/*Output:
This value is :1
This value is :2
3
This value is :5
/*

範例四:

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
const p = new Promise(function(resolve, reject) {
setTimeout(function(){
let value = 1;
resolve(value);
}, 1000);
});

function funA (value) {
console.log("The value of function A is :" + value);
const data = funC(value)
return data + 1
}

function funB (value) {
console.log("The value of function B is :" + value);
}

function funC (value) {
return value + 2
}

p.then(funA)
.then(funB)

/*Output:
The value of function A is :1
The value of function B is :4
/*

Promise 錯誤處理

通常我們會將 .catch 放在最後面做錯誤處裡,當其中一個 .then 發生錯誤時,就會跳到 .catch ,而 .catch 之後的 .then 皆不會執行。

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
function p(value) {
return new Promise((resolve, reject) => {
if (value > 2){
reject('Unexpected condition')
}
setTimeout(function() {
value++
resolve(value)
}, 1000)
})
}

p(1)
.then((value => {
console.log("This is the first value :" + value);
return p(value)
}))
.then((value => {
console.log("This is the second value :" + value);
return p(value)
}))
.then((value => {
console.log("This is the third value :" + value);
return p(value)
}))
.catch((error) => {
console.log(error)
})

/*Output:
This is the first value :2
This is the second value :3
Unexpected condition
/*

Promise.all

Promise.all 可以傳入一個以上的 Promise 並同時執行,等到所有的 Promise 都回應狀態後,才會進入 .then

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function p(value) {
return new Promise((resolve, reject) => {
setTimeout(function() {
if (value < 1000 ){
        reject('Unexpected condition')
}
resolve(value)
}, value)
})
}

Promise.all([p(1000), p(2000), p(3000)]).then((value)=> {
console.log('This value is :', value);
}).catch( err => {
console.log(err)
});

/*
[This value is : 1000 , This value is : 2000 , This value is : 3000]
*/

如果其中一個 Promise 中有出現 reject ,則直接進入 .catch 階段,而不會收到其他 Promise resolve 的回傳值。

Promise.race

Promise.race 一樣可以傳入多個 Promise 並同時執行,但只回傳 Promise 中最快完成的,並接著執行 .then ,而其他的 Promise 皆不會執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function p(value) {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve(value)
}, value)
})
}

Promise.race([p(1000), p(2000), p(3000)]).then((value)=> {
console.log('This value is :', value);
}).catch( err => {
console.log(err)
});

/*
This value is : 1000
*/

參考文獻

  1. callback, promise, async/await 使用方式教學以及介紹 Part I - Jack Yu | 傑克
  2. [JS] Promise 的使用 | PJCHENder 私房菜
  3. Day23 Promise 詳解(1/2)
  4. Day24 Promise 詳解(2/2)
  5. JavaScript - Promise (2)
  6. JS 原力覺醒 Day14 - 一生懸命的約定:Promise

JavaScript - Call Stack , Event Queue & Event Loop




JavaScript是同步執行的程式語言

JavaScript 是一個設計為同步執行的程式語言,並且是單執行緒(single-threaded)。單執行緒可以想像成有一個餐廳的員工同時負責櫃檯和廚房的工作,他的能力強大到可以同時在兩份工作之間快速的切換,仿佛有多個員工在做事的錯覺。

竟然 JavaScript 不是非同步執行程式碼,那為什麼可以監聽瀏覽器的一些事件,像是移動滑鼠、點擊按鈕、捲動頁面、資料請求和檔案寫入這類型的非同步呼叫。

其實瀏覽器不是只有 JavaScript Engine 的存在

整個瀏覽器的運行環境並非只由 JS 引擎組成,瀏覽器還包含許多其他的部分,像是渲染引擎(Rendering Engine)和 HTTP 請求(Http Request),而瀏覽器也提供 Web API 讓 JS 做應用,像是操作 DOM 節點 、 AJAX 請求、 setTimeout 計時、 Geolocation 地理位置。雖然 JS 引擎身是同步執行,但透過和瀏覽器的其他引擎互動來達到非同步的效果。

Call Stack

JS 的執行堆疊(call stack)記錄了 function 在 JS engine內的執行順序,意思就是程式目前執行到哪邊, call stack 是以同步的方式由上而下執行,採取後進先出 Last In, First Out (LIFO) ,最先執行的函式會被放在 stack 的最下面,如果在函式中執行了 return ,則會將該函式 pop out 出 stack 。

請參考以下範例:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}

function bar() {
console.log("-> start [bar]");
foo()
console.log("<- end [bar]");
}

bar();

以下為範例各執行階段:

  1. bar() 會先被執行

  2. bar() 印出 “start”

  3. foo() 執行,此時 bar() 仍然在執行中

  4. foo() 印出 “start”

  5. foo() 印出 “end”

  6. foo() 結束執行

  7. bar() 印出 “end”

  8. bar() 結束執行

Stack Overflow

如果遞迴函式不斷呼叫自己而沒有一個中斷點,則會造成瀏覽器產生 stack error :

Event Queue

前面有說過瀏覽器除了有 JS Engine 外還有許多 Web API 可做應用,這些第三方 API 可以與 JS 一起執行,當遇到需要執行 call back 時,則會使用非同步模式,先將這些函式放到 Web APIs 中,在繼續執行主程式,等到時間到或是事件被觸發的時候,再把 call back 推送到 Event Queue(事件佇列) 中,等到整個 JS 執行環境結束後(Call Stack 清空),才依序呼叫 Event Queue 裡面的函式,採取先進先出 first in, first out (FIFO)。

請參考以下範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}

function bar() {
console.log("-> start [bar]");
console.log("<- end [bar]");
}


function baz() {
console.log("-> start [baz]");

setTimeout(foo, 1000);
setTimeout(bar, 1000);

console.log("<- end [baz]");
}

baz();

以下為範例各執行階段:

  1. baz() 會先被執行

  2. baz() 印出 “start”

  3. foo() 先放到 Web APIs ,等待1秒後移至 Event Queue 中。

  4. bar() 先放到 Web APIs ,等待1秒後移至 Event Queue 中。

  5. baz() 印出 “end”

  6. baz() 執行完成,call stack 清空

  7. 透過 Event Loop 機制選取 queue 中的 foo()。

  8. foo() 印出 “start”

  9. foo() 印出 “end”

  10. foo() 執行完成,call stack 清空

  11. 透過 Event Loop 機制選取 queue 中的 bar()。

  12. bar() 印出 “start”

  13. bar() 印出 “end”

  14. bar() 執行完成,call stack 清空

Event Loop

JS 當事件發生時,並不是馬上執行指定的函式,而是將事件排入 Event queue ,接著會重複檢查 call stack 是不是空的?如果是空的,再去看 Event queue 中有沒有待執行的函式,有的話就將函式 pop out ,放入 call stack 中執行。

參考文獻

  1. 所以說event loop到底是什麼玩意兒?| Philip Roberts | JSConf EU

  2. [JS] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式(Learn event loop, stack, queue, and concurrency mode of JavaScript in depth)

  3. Day06 JS是同步還是非同步?

  4. JS 原力覺醒 Day13 - Event Queue & Event Loop 、Event Table

  5. 非同步設計

  6. Understanding the JavaScript call stack

JavaScript 非同步執行 (Ascynchronous)




同步執行 ( Scynchronous )

如果我有很多個任務,同步就是讓每個任務按照順序依序進行,後面的任務必須等待前面的任務執行完成後才會執行。

以生活例子比喻的話,假如我在買電影,我要等到前面所有排隊的人買完之後才能輪到我,而排在我後面的人也要等到我完成買電影票這個動作之後,才能輪到他們。

以程式碼來說明的話,同步執行就表示程式會依照所寫的程式碼一行一行去執行,而順序都是前一個動作執行完才會去接著執行下一個動作。同步程式的設計模式看起來會比較比較直覺,常見的程式語言像是C、C++、Java皆是採用同步模式。

假設有一個同步程式如下:

1
2
3
4
5
6
7
8
9
10
11
12
function func1() {
console.log('func1');
}
function func2() {
console.log('func2');
}
function func3() {
console.log('func3');
}
func1(1); // "func1"
func2(2); // "func2"
func3(3); // "func3"

我們可以發現程式是照順序一行一行進行輸出。

非同步執行 ( Ascynchronous )

像是在買電影的時候,如果前面排隊的人處理很久,就會造成後面所有人買電影票的時間拉長。

由於同步執行的設計模式,使得程式必須等待前一個步驟完成後才能繼續執行,如果前一個動作沒有完成就會無法進行下一個動作而造成阻塞。

而非同步模式則相反,後面的任務不用等待前面的任務完成,各自執行各自的任務。

當你去百貨公司吃飯的時候,你不需要等待其他人點完餐和吃完飯後才輪到你點餐,每個人都可以在拿到餐點的時候就可以吃了,並不需要等待其他人先吃完。如果你點的是一份較花時間的牛排,那我只點一杯飲料就可以優先拿到,而不需要等到你的餐點完成後才輪到我。

如果是以同步的方式去讀取資料,就會在資料回傳之前會持續進行等待的動作,直到確定資料已經讀取完成後才繼續進行下一個動作。

以非同步的方式讀取資料的話,在資料回傳之前我可以先做其他的事情,等待資料回傳後再進行通知。

讓我們用 setTimeout 來示範非同步執行:

1
2
3
4
5
6
7
8
// Say "Hello."
console.log("Hello.");
// Say "Goodbye" two seconds from now.
setTimeout(function() {
console.log("Goodbye!");
}, 2000);
// Say "Hello again!"
console.log("Hello again!");

由於 setTimeout 並不會暫停程式的執行,所以會立即執行後面的動作,等待時間到了才會呼叫並執行函式裡面的內容。

所以輸出的結果依序為:

  • Say “Hello”.

  • Say “Hello again!”

  • 程式等待兩秒鐘

  • Say “Goodbye!”

AJAX Requests

這個範例使用 jQuery 的 Ajax 來進行操作,我們在 button 上加入一個 click 事件,當使用者點擊後會送出 request ,請求成功後會呼叫 callback 並把結果存入 data

1
2
3
4
5
6
7
8
9
10
11
12
$(document).ready(function(){
$("button").click(function(){
let data;

$.ajax({url: "demo_test.txt", success: function(result){
$("#div1").html(result);
data = result;
}});

console.log(data);
});
});

由於程式並不會等待取回資料後才繼續執行,而是會先執行 console.log(data) ,所以不管怎樣程式都會印出 undefined

我們可以透過 callback functions 來讓程式依照我們想要的順序執行,不知道什麼是 callback 的請參考之前的文章。

JavaScript Callback Function

非同步和同步

傳統的網頁在使用者提交資料後(亦即對伺服器發出請求),必須等到伺服器回應並重新整理頁面後,才能繼續進行下一個動作,這段期間內使用者無法對該頁面進行任何的存取。而非同步請求允許使用者在發出請求到伺服器回應的期間內繼續使用頁面(例如:繼續操作網頁、輸入資料),等到完成回應,網頁僅對部份資訊進行更新,藉以達到更有效的即時互動,非同步的核心精神在於減少等待,讓執行緒同時處理更多作業藉以提升產能。

如果網站是採取同步執行的設計模式,當我請求某一個網頁資料時,在資料回傳後才能進行下一個步驟,中途如果網頁無法回應或是阻塞住了,整個網站就會卡住。

若網站是採取非同步執行,等待資料回傳的同時還是能進行其他應用,假使資料無法成功存取也不會影響到其他正常功能的操作。

參考文獻

  1. https://www.pluralsight.com/guides/introduction-to-asynchronous-javascript

  2. https://www.ithome.com.tw/node/74544

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

  4. https://blog.xuite.net/autosun/study/33280250-%5BJS%5D+Ajax+%E5%BB%BA%E7%AB%8B%E9%9D%9E%E5%90%8C%E6%AD%A5%E8%AB%8B%E6%B1%82%EF%BC%88%E4%B8%80%EF%BC%89

JavaScript Arrow Functions




箭頭函式運算式 (Arrow Function expression) 是在 ES6 新增的建立函式語法,這種寫法有著兩個重要的特性:

  • 更簡短的函式寫法

  • this 變數強制綁定

箭頭函式(Arrow Function)

傳統的函式寫法:

1
2
3
4
5
6
7
8
9
10
11
//沒參數
let myFunc = function(){
return 1;
}
//有參數
let fn = function(n1, n2){
return n1+ n2;
}

myFunc(); // 1
fn(1,2); // 3

ES6 的箭頭函式寫法:

1
2
3
4
5
6
7
//沒參數
let myFunc = () => (1);
//有參數
let fn = (n1, n2) => (n1+n2);

myFunc(); // 1
fn(1,2); // 3

在使用箭頭函式中,如果函式沒有帶上參數記得加空括號 () ,有參數直接在 () 內填入參數即可。

如果函式最後要回傳值的話,可以省略 return 不寫。

當函式只有一個參數時,不需要使用括號

從上面的例子可以發現,當函式無參數或有兩個以上的參數時,都要加上括號 ( );當函數只有一個參數時,可以省略括號不寫。

1
2
3
4
5
6
//兩種寫法都可以
//有加括號
let fn = (n1) => (n1);
//沒加括號
let fn = n1 => n1;
fn(1); // 1

我們使用括號 () 來定義函式帶入的參數,而大括號 {} 定義函式功能的一些 JavaScript 語句,如果函式有多行語句(表達式)時就要使用 {} , {} 內需自行加入 return ,否則會出現 undefined

1
2
3
4
5
let fn = (n1) => { n1 }
let myfunc = (n1) => { return n1; }

fn(1); // undefined
myfunc(1); // 1

匿名函式

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
//第一個範例
//傳統的匿名函式
const sum = function(a, b) {
return a+b;
}

//匿名的箭頭函式
const sum_arr = (x, y) => {
return x + y;
};

//第二個範例
//傳統的匿名函式
let fn = function(){
setTimeout(function(){
console.log("1秒");
},100);
}

//匿名的箭頭函式
let fn_arr = function(){
setTimeout(() => {
console.log("1秒");
},100);
}

sum(1,2); // 3
sum_arr(1,2); // 3
fn(); // 1秒
fn_arr(); // 1秒

指定參數的預設值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//傳統寫法
function fn(name){
if(name == undefined){
name= "Cyting";
}
console.log(name);
}

//ES6寫法
function fn(name ="Cyting"){
console.log(name);
}

//箭頭函式寫法
let fn = (name="Cyting") => {
console.log(name);
}

fn("Cheng-Yi-Ting!"); // Cheng-Yi-Ting!
fn(); // Cyting

箭頭函式的 this

不管是使用傳統的函式寫法或箭頭函式, this 都會存取到 window 物件(不清楚原因的話請先參考之前的文章 JavaScript THIS )。

1
2
3
4
5
6
7
8
9
10
let fn = function(){
console.log(this);
}

let fn2 = () => {
console.log(this);
}

fn(); //[Window Object]
fn2(); //[Window Object]

funcsetTimeout 箭頭函式的 this 均會指向 obj

1
2
3
4
5
6
7
8
9
10
11
const obj = { a:1 }

function func() {
console.log(this.a) // 1
setTimeout(() => {
console.log(this.a) // 1
}, 1000)

}

func.call(obj)

如果是傳統的函式寫法, setTimeoutthis 會指向 window 物件:

1
2
3
4
5
6
7
8
9
10
const obj = { a: 1 }

function func() {
console.log(this.a) // 1
setTimeout( function() {
console.log(this.a) // undefined
}, 1000)
}

func.call(obj)

我們可以透過將 this 存在 that 變數中,之後就可以透過 that 存到之前 this 的參照:

1
2
3
4
5
6
7
8
9
10
const obj = { a: 1 }

function func(){
const that = this
setTimeout(function(){
console.log(that.a) // 1
}, 1000)
}

func.call(obj)

也可以透過 .bind() 來強制指定 this 為 () 內的物件,於是可以把 setTimeout 裡面的 this 指定為先前的 this

1
2
3
4
5
6
7
8
9
const obj = { a: 1 }

function func(){
setTimeout(function(){
console.log(this.a) // 1
}.bind(this), 1000)
}

func.call(obj)

傳統函式的 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
var name = 'Cheng-Yi-Ting'
var obj = {
name: 'Cyting',
fun1: function () {
// 注意,這裡是 function,以此為基準產生一個作用域
console.log('1', this.name); // 1 'Cyting',
setTimeout(() => {
console.log('2', this.name); // 2 'Cyting',
console.log('3', this); // 3 obj 這個物件
}, 1000);
},
fun2: () => {
// 注意,如果使用箭頭函式,this 依然指向 window
console.log('4', this.name); // 4 'Cheng-Yi-Ting'
setTimeout(() => {
console.log('5', this.name); // 5 'Cheng-Yi-Ting'
console.log('6', this); // 6 window 物件
}, 1000);
}
}

obj.fun1();
obj.fun2();

一般函式是建立在 window 底下,所以箭頭函式自然會指向 window ;可以透過將箭頭函式宣告在物件內部,來將 this 指向該物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var func = function () {
var func2 = function () {
setTimeout(() => {
console.log(this);
}, 1000);
};

var func3 = {
func: func2,
value: 1
}
func2(); // this = window
func3.func(); // func3 Object
}
func();

func3() 是呼叫在物件內的函式,因此箭頭函式會是使用它所在的物件。

不可使用箭頭函式的情況

apply, call, bind

函式物件中的 callapplybind 這三個方法,無法覆蓋箭頭函式中的 this 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
value: 1
}

const a = () => {
console.log(this);
}

const b = function () {
console.log(this);
}

a.call(obj); // 箭頭函式的情況,this 依然是 window
b.call(obj); // 一般函式 this 則是傳入的物件

建構函式

箭頭函式無法像一般的函式一樣作為建構式使用,使用 new 語法會出現錯誤:

1
2
3
4
5
6
const Message = (text) => {
this.text = text
}

const helloMessage = new Message('Hello World!');
// "TypeError: Message is not a constructor"

DOM事件處理函式

在 HTML 中建立一個 button element,在 button 使用 addEventListener 事件監聽器:

1
2
3
4
5
6
7
8
9
var button = document.querySelector('button');
var fn_arr = () => {
console.log(this) // this 指 Window
};

var fn = function(){
console.log(this) // this 指 HTMLButtonElement
}
button.addEventListener('click', fn_arr);

使用傳統的寫法,在觸發這個事件時所指稱的對象會從原本的 window 變成按鈕物件;若使用的是箭頭函式,則 this 一樣會是 window 物件。

Prototype 中定義的方法

如果在原型中使用箭頭函式,此時箭頭函式內的 this 會指向 window ,若是在嚴格模式則會是 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

Person.prototype.log1 = function () {

console.log(this.firstName+' '+this.lastName+ ', age:' + this.age);
}
Person.prototype.log2 = () => {

console.log(this.firstName+' '+this.lastName+ ', age:' + this.age);
}

const roman = new Person('Roman', 'Gonzalez', 18);

roman.log1(); // Roman Gonzalez, age:18

roman.log2(); // undefined undefined, age:undefined

結論

箭頭函式語法

  1. 沒有參數時要有小括號。
  2. 只有一個參數時可以省略小括號。
  3. 若有兩個以上的參數要有小括號。
  4. 只有一行回傳值可以省略大括號。

箭頭函式限制

  • 函式物件中的 callapplybind 這三個方法,無法覆蓋箭頭函式中的 this 值。
  • 不可作為建構式使用,會在使用 new 時候拋出錯誤。
  • 箭頭函式並沒有原型(prototype)屬性。
  • 沒有一般函式有的隱藏 arguments 物件。
  • 箭頭函式不能當作 generators 使用,使用 yield 會產生錯誤。

參考文獻

  1. https://dotblogs.com.tw/shihgogo/2017/12/11/111122

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

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

  4. https://pjchender.blogspot.com/2017/01/es6-arrow-function.html?m=1

  5. https://wcc723.github.io/javascript/2017/12/21/javascript-es6-arrow-function/

  6. https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/arrow_function.html

JavaScript var , let , const




在 ES6(ES2015) 之後宣告「變數」與「常數」,除了原本的 var 以外,還可以透過 let 與 const 做宣告,在本文中,我們會說明使用 var , let 和 const 的差異,並討論他們的作用域、變數初始化以及 hoisting 。

var 作用域

1
2
3
4
5
6
7
8
9
var Christmas = 'Merry Christmas and happy New Year!'

function greeting () {
var hello = "greeting";
console.log(hello); // greeting
}

greeting();
console.log(hello); // error: hello is not defined

使用 var 宣告的變數是以函式作為作用域的分界,範例中的 Christmas 宣告在函式之外所以視為全域作用範圍(global),而 hello 宣告在函式之內所以其作用範圍於該函數之內。當我們執行 greeting() 可以存取到 hello ,而在 function 外就無法獲得該變數內容,所以在全域範圍中會找不到 hello 而產生 ReferenceError 的錯誤。

var 變數可以重複宣告

JavaScript 允許重複宣告變數,不過實際上並不會進行重覆宣告,只會將其視為指派資料值:

1
2
3
4
5
var Christmas = 'Merry Christmas and happy New Year!'
var Christmas;
console.log(Christmas); // 'Merry Christmas and happy New Year!'
var Christmas = 'We wish you a merry Christmas.'
console.log(Christmas); // 'We wish you a merry Christmas.'

var 的問題

var 是以函式為變數作用域的分界,在一些使用了區塊語句: if , else , for , while 等等區塊語句中,使用 var 宣告的變數會曝露到全域作用範圍:

1
2
3
4
5
6
7
8
9
10
function greeting () {
var hello = "greeting";
}

if(true){
var say = "Christmas time";
}

console.log(hello) // hello is not defined
console.log(say) // 可存取

使用 var 宣告變數容易造成程式上的誤解:

1
2
3
4
5
6
7
8
var hello = "greeting";
var counts = 2;

if(counts > 1){
var hello = "Christmas time";
}

console.log(hello) // Christmas time

因為 counts > 1 成立,所以 hello 會被重新賦予新值,如果在其他程式碼也有使用到 hello 這個變數,可能會不小心就更動了變數的內容造成程式執行錯誤。

透過 ES6 加入的 letconst 來宣告,以區塊語句為分界的作用域,將會更明確且不易發生錯誤。

使用 let 宣告 hellosay ,本來在全域範圍中可以存取到的 say 變成 ReferenceError ,因為 letconst 是用大括號 {} 來區分作用域:

1
2
3
4
5
6
7
8
9
10
function greeting () {
let hello = "greeting";
}

if(true){
const say = "Christmas time";
}

console.log(hello) // hello is not defined
console.log(say ) // say is not defined

下面例子中的 hello 也只能在 {} 內進行存取:

1
2
3
4
5
6
7
8
let counts = 2;

if(counts > 1){
let hello = "Christmas time";
console.log(hello);// "Christmas time";
}

console.log(hello) // hello is not defined

let 不能重複宣告

使用 let 宣告的變數不能再其作用域中重複宣告,但可以更新其值:

1
2
let hello = "greeting";
hello = 'Merry Christmas and happy New Year!';

以下例子會輸出 error:Identifier ‘hello’ has already been declared:

1
2
let hello = "greeting";
let hello = 'Merry Christmas and happy New Year!';

如果同名的變數在不同的作用域就不會出錯:

1
2
3
4
5
6
7
8
let hello = "greeting";

if (true) {
let hello = 'Merry Christmas and happy New Year!';
console.log(hello);//"Merry Christmas and happy New Year!"
}

console.log(hello);// "greeting"

var 使用同名變數不會產生錯誤訊息,而使用 let 宣告的變數只能在同一個作用域中宣告一次,這種做法將可避免更多錯誤的產生。

Hoisting

JavaScript 會將變數宣告放到該 Scope 的最上層,並將該變數的初始值設為 undefined ,這種特性稱為「變數提升」 (Variables Hoisting):

1
2
console.log (Christmas); // undefined
var Christmas = 'Merry Christmas and happy New Year!'

上面程式碼等同於下面:

1
2
3
var Christmas;
console.log(Christmas); // undefined
Christmas = 'Merry Christmas and happy New Year!'

由於 JavaScript 的 Hoisting 特性,建議將變數宣告都放在 Scope 的最上方,養成先宣告完成後再使用的習慣,讓程式邏輯更清楚,也可以避免預期外的結果或錯誤發生。

Temporal dead zone(TDZ)

TDZ 為 ES6 的新用語,它的作用主要是用在 letconst 上; varundefined 來初始化,代表在宣告之前存取變數會得到 undefinedletconst 一樣會有 Hoisting,而 let 不會初始化變數的值,如果再宣告前存取會出現 ReferenceError

1
2
console.log (Christmas); // ReferenceError: Christmas is not defined
let Christmas = 'Merry Christmas and happy New Year!'

TDZ 表示一個尚未被初始化的狀態,有一個變數經過宣告後但未被初始化,此時存取它就會產生 ReferenceError 。下面例子中的變數 Christmas 會先被提升到函式的最上面;此時會產生 TDZ,如果程式流程未執行到 Christmas 的宣告語句時,就算是在 TDZ 作用的期間,此時存取 Christmas 就會出現 ReferenceError 錯誤。

1
2
3
4
5
6
7
let Christmas = 'Merry Christmas and happy New Year!';

(function() {
// 產生 TDZ
console.log(Christmas) // TDZ期間存取,Cannot access 'Christmas' before initialization
let Christmas = 'We wish you a merry Christmas.' // 對 Christmas 的宣告語句,這裡結束 TDZ
})();

如果先初始化值,再存取就不會出錯:

1
2
3
4
5
let Christmas = 'Merry Christmas and happy New Year!';

(function() {
console.log(Christmas) // 'Merry Christmas and happy New Year!'
}());

CONST

letconst 一樣是區塊作用域,唯一的差別是使用 const 宣告的變數代表常數 ,宣告的同時就要指定給值,並且不能重新賦予新值。

1
2
3
4
let hello = "greeting";
const say = "Christmas time";
hello = 'Christmas Eve'
say = 'Santa Claus' // TypeError: Assignment to constant variable.

重新給值就會出錯:

1
2
const say = "Christmas";
const say = "Christmas time"; // Identifier 'say' has already been declared

const 宣告的常數代表它是唯讀的,但並非代表這個參照到的值是不可改變的(immutable)。如果宣告的常數是一個參照類型的值,像是「物件」或是「陣列」,那裡面的值是可以改變的:

1
2
3
4
5
6
7
const person = {
name : 'Cheng Yi-Ting'
}
const test = []

person.name = 'CyTing'         
test[0] = '1'

如果只改變物件屬性的屬性值並不會出錯,但無法對整個物件分配新值:

1
2
3
4
5
const person = {
name : 'Cheng Yi-Ting'
}

person = {} // Assignment to constant variable.

for 迴圈中的 let

假設想要透過迴圈依序印出數字 0 到 4 ,並搭配 setTimeout 來達成每秒鐘輸出一個數字;過去我們可以透過在 for 迴圈中加入 IIFE 來完成,每次將當下的 i 作為參數傳入函式:

1
2
3
4
5
6
7
for( var i = 0; i < 5; i++ ) {
(function(x){
window.setTimeout(function() {
console.log(x);
}, 1000 * x);
})(i);
}

在 ES6 以後可以透過 let 來簡化,因為使用 {} 來區分作用域,所以每次 for 迴圈執行時,用 let 宣告的變數都會重新綁定一次,所以可以保留當下執行 i 的值:

1
2
3
4
5
for( let i = 0; i < 5; i++ ) {
window.setTimeout(function() {
console.log(i);
}, 1000 * i);
}

參考文獻

  1. https://dev.to/sarah_chima/var-let-and-const–whats-the-difference-69e

  2. https://tylermcginnis.com/var-let-const/

  3. https://eddychang.me/es6-tdz/

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

  5. https://ithelp.ithome.com.tw/articles/10185142

JavaScript ES6 Class




JavaScript是一個以原型為基礎(prototype-based)的物件導向程式語言,而 ES6 Class 只是個語法糖(syntactical sugar),目的是提供更簡潔的語法來作物件建立與繼承。

我們過去使用 function constructor 來產生類別物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Employee(name, age,salary) {
    // 定義基本屬性
this.name = name;
this.age = age;
this.salary = salary;
}
// 原型方法
Employee.prototype.getName = function() {
return this.name;
}

Employee.prototype.employeeDetails = function() {
return 'Name: '+this.name+', Age:'+this.age+', salary:'+this.salary;
}

var felix = new Employee('Felix', 18, 22000);
//Name: Felix, Age:18, salary:22000
console.log(felix.employeeDetails());

var hector= new Employee('Hector', 25, 36000);
//Name: Hector, Age:25, salary:36000
console.log(hector.employeeDetails());

ES6 Constructor

類別宣告(Class Declaration)

一個定義類別的方法是使用類別宣告(class declaration),使用關鍵字 class 搭配類別名稱(此例為 “Employee”)。

1
2
3
4
5
6
7
class Employee{
constructor(name, age, salary) {
    this.name= name;
    this.age = age;
    this.salary= salary;
}
}

類別敘述(Class Expression)

類別敘述是定義類別的另一種方法,可區分為有名稱或無名稱的類別敘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// named class expression
var Employee = class {
constructor(name, age, salary) {
    this.name= name;
    this.age = age;
    this.salary= salary;
}
}

// unnamed class expression
var Employee = class Employee{
constructor(name, age, salary) {
    this.name= name;
    this.age = age;
    this.salary= salary;
}
}

var hector= new Employee('Hector', 25, 36000);

Hoisting

函式宣告和類別宣告的一個重要差別在於函式宣告是會 hoisted,而類別宣告則不會。使用類別宣告方法,需要先宣告類別才能進行存取,否則會出現 ReferenceError 的錯誤。

1
2
3
4
//Uncaught ReferenceError: Cannot access 'Employee' before initialization
var Vincent = new Employee();

class Employee{}

以下為將第一個例子修改為 Class 方法:

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
class Employee{
constructor(name, age, salary) {
    // 定義基本屬性
this.name= name;
this.age = age;
this.salary= salary;
}
    // 原型方法
getName() {
return this.name;
}
    
employeeDetails() {
return 'Name: '+this.name+', Age:'+this.age+',                 salary:'+this.salary;
}
}

//Name: Felix, Age:18, salary:22000
var felix = new Employee('Felix', 18, 22000);
console.log(felix.employeeDetails());


//Name: Hector, Age:25, salary:36000
var hector= new Employee('Hector', 25, 36000);
console.log(hector.employeeDetails());

改由 constructor 定義物件內的屬性,並將以前的 prototype 直接寫在 class 內,讓程式碼更為直觀。

靜態方法

我們可以在 class 內定義靜態方法,靜態方法只能被原型使用,無法在創立的物件上使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Employee{
constructor(name, age, salary) {
    this.name= name;
    this.age = age;
    this.salary= salary;
}
static testMethod(){
return 'static method has been called.';
}
}

var hector= new Employee('Hector', 25, 36000);

//hector.testMethod is not a function
hector.testMethod();

// 'static method has been called.'
Employee.testMethod()

Get and Set

get 與 set 關鍵字分別代表取得方法和設定方法,一般的公開的的屬性不需要用到這兩種方法,本來就能直接取值或設定;只有私有屬性才需要用到 get 和 set 方法來作取得或設定。

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
// ES6 get and set
class Employee{
constructor(name, age) {
this.name= name;
this.age = age;
}

get salary() {
console.log("The code executed on getting obj.salary");
if (this._salary!== undefined) {

return this._salary;
} else {
return 'no salary prop'
}
}

set salary(value) {
console.log("The code executed on setting obj.salary = value;")
this._salary=value;
}
}

var hector = new Employee('Hector',18);
//The code executed on setting obj.salary = value;
hector.salary='35000';
//The code executed on getting obj.salary
//"35000"
hector.salary;

getter不會有傳入參數,setter 則只能傳入一個參數。

繼承

我們之前透過函式建構式來達成類別繼承,為了讓子類別繼承父類別建構函式的內容,所以我們必須在子類別建構函式中呼叫父類別建構函式:

1
2
3
4
5
6
7
8
9
10
11
12
// Person 的 constructor function
function Person(name,age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}
// Student 的 constructor function
function Student(name) {
this.name = name;
this.score = 100;
Person.call(this,name);
}

extends

ES6 簡化類別宣告的過程,我們不需要再去修改原型物件,也能直接完成繼承的效果。

這裡會使用到兩個關鍵字:

  • extends: 繼承於另一個原型之下。
  • super : 使用上層的值(屬性)。

創造子類別 Student,並使用 extends 指向 Person,讓 Student 繼承 Person:

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
class Person{
constructor(name, age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}

hi() {
    console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}
}

class Student extends Person{
constructor(name) {
super(name);
this.name = name;
this.score = 100;
}

say() {
    console.log(`Say, ${this.name} ,My score is ${this.score}.`);
}
}

var roman = new Student('roman');

繼承的子類別中的建構式, super() 需要放在建構式第一行,這是標準的呼叫方式。如果有需要傳入參數可以傳入。

繼承的子類別中的屬性與方法,都會覆蓋掉原有的在父母類別中的同名稱屬性或方法,要區分不同的屬性或方法要用 super 關鍵字來存取父母類別中的屬性或方法。

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
class Person{
constructor(name, age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}
hi() {
console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}
}

class Student extends Person{
constructor(name) {
super(name);
this.name = name;
this.score = 100;
}
hi() {
console.log(super.hi());
}
}

var roman = new Student('roman');

// Hi, roman ,My age is 0.
roman.hi();

參考文獻

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

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

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

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

JavaScript Object-oriented programming




Prototype-based Inheritance (基於原型的繼承)

JavaScript 的繼承是 prototype-based,意思就是在 JavaScript 中沒有 class,所有的 object 都繼承自其它的 object

以下為繼承的實作範例,先建立父層類別 Person 和子層類別 Student:

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
// Person 的 constructor function
function Person(name,age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}

// Person 的 prototype
Person.prototype.hi= function () {
console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}

// Student 的 constructor function
function Student(name) {
this.name = name;
this.score = 100;
}

// Student 繼承 Person
Student.prototype = new Person()

// Student的 prototype 方法
Student.prototype.say = function() {
console.log(`Say, ${this.name} ,My score is ${this.score}.`);
};

// 建立物件實例
var roman = new Student('roman');
var hera = new Student('hera');

// Hi, roman ,My age is 0.
roman.hi();

// Say, roman ,My score is 100.
roman.say();

roman.skill.push('JavaScript')

console.log(roman.skill) // ["HTML","CSS","JavaScript"]
console.log(hera.skill) // ["HTML","CSS","JavaScript"]

//物件的繼承關係 (prototype chain)
console.log(roman.__proto__ === Student.prototype)//true
console.log(Student.prototype.__proto__ === Person.prototype)//true

由於 skill 這個屬性是定義在父層元素,而父層元素是會被子層元素所影響;因為 roman 物件中並沒有 skill 屬性,因此會透過原型練存取到 Person 的 skill(roman.__proto__.skill),造成修改 roman.skill 卻連帶的影響到 hera.skill 。

1
2
3
roman.skill.push('JavaScript')
console.log(roman.skill) // ["HTML","CSS","JavaScript"]
console.log(hera.skill) // ["HTML","CSS","JavaScript"]

物件實例不會影響到父層屬性

為了避免子類別實例影響父類別屬性的問題,我們可以使用 Person.call(this) ,把 Person 裡面的 this 指稱對象改成當前透過 Student 建構式所建立的物件實例:

1
2
3
4
5
function Student(name) {
this.name = name;
this.score = 100;
Person.call(this,name);
}

等同於把原本 Person 的內容複製到 Student 中:

1
2
3
4
5
6
7
8
function Student(name) {
//父層屬性
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
//子層屬性
this.score = 100;
}

這時候物件實例就不會共享到父層的屬性了:

1
2
3
4
5
6
7
// 建立物件實例
var roman = new Student('roman');
var hera = new Student('hera');

roman.skill.push('JavaScript')
console.log(roman.skill) // ["HTML","CSS","JavaScript"]
console.log(hera.skill) // ["HTML","CSS"]

Object.create() and new operator 差異

也可以使用此方法來實現繼承:

1
Student.prototype = Object.create(Person.prototype);

為了避免瀏覽器太舊不支援 Object.create() ,可自行 polyfill 來達成一樣的效果:

1
2
3
4
5
6
7
8
9
10
11
12
// 同 Student.prototype = Object.create(Person.prototype)
Student.prototype = inherit(Person.prototype);

// Object.create()
function inherit(proto) {
//先建立一個空的 F constructor function
function F() {};
//將 F.prototype 指向傳進來的 proto
F.prototype = proto;
//用函式建構式的方式回傳
return new F();
}

兩種方法差異在於 new Person() 會執行建構式中的程式碼,而 object.create() 並不會,以下為第一個範例的修改:

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
// Person 的 constructor function
function Person(name,age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}

// Person 的 prototype
Person.prototype.hi= function () {
console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}

// Student 的 constructor function
function Student(name) {
this.name = name;
this.score = 100;
}

// Student 繼承 Person
Student.prototype = Object.create(Person.prototype)

// Student的 prototype 方法
Student.prototype.say = function() {
console.log(`Say, ${this.name} ,My score is         ${this.score}.`);
};

// 建立物件實例
var roman = new Student('roman');
var hera = new Student('hera');

// Hi, roman ,My age is undefined.
roman.hi();
// Say, roman ,My score is 100.
roman.say();

//Uncaught TypeError: Cannot read property 'push' of undefined
roman.skill.push('JavaScript')

因為沒有執行 Person 函式建構式的內容,當呼叫 roman.hi(); 中的 this.age 為 undefined;沒有 skill 屬性所以也無法使用 push 方法。

以下為使用兩種方式建立 roman 物件:

Student.prototype = new Person()



Student.prototype = Object.create(Person.prototype)

Polymorphism (多型)

透過在子類別中重寫覆蓋 (override) 掉父類別中的方法或屬性來完成多型。

1
2
3
Student.prototype.hi = function() {
console.log(`Hi, ${this.name} ,My score is ${this.score}.`);
};

因為原型鏈的關係,當執行 roman.hi() 時,就會優先執行 Student 中定義的 hi。

Encapsulation (封裝)

無法直接存取底線開頭 (underscore) 的屬性或方法,藉由公開的 hi() 來呼叫 __hi() 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name,age) {
this.name = name || 'default';
this.age = age || 0;
this.skill= ['HTML','CSS'];
}

// protected
Person.prototype.__hi= function () {
console.log(`Hi, ${this.name} ,My age is ${this.age}.`);
}

// public
Person.prototype.hi= function () {
this.__hi();
}

var roman = new Person('roman','18');


// Hi, roman ,My age is 18.
roman.hi();

靜態屬性或方法在 JavaScript 中的實作方式,是直接將方法或屬性加在 constructor function 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name,age) {
Person.count++;
}

// 靜態屬性
Person.count = 0;


// 靜態方法
Person.getCount = function() {
console.log(`${Person.count}.`);
};

new Person();
new Person();

// 顯示 2
Person.getCount();

參考文獻

  1. https://www.fooish.com/javascript/oop-object-oriented-programming.html

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

  3. https://pjchender.github.io/2018/08/01/js-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-javascript-object-oriented-javascript/

  4. https://github.com/noahlam/articles/blob/master/JS%E4%B8%AD%E7%9A%84%E7%BB%A7%E6%89%BF(%E4%B8%8A).md

JavaScript 原型鏈(Prototype Chain)




前言

物件導向程式設計(OOP)的基本概念是使用物件的方式來表示,透過類別(class)的函式建構式(function constructor)可以產生物件實例(object instance)。

JavaScript 並非真正的物件導向 (OO) 語言,它是一個以原型為基礎 (Prototype-based) 的語言。因為沒有 Class,而是透過 「原型」(prototype) 來實現繼承實作。

建構式

在 JavaScript 中,一個 Function 是不是建構式並不是取決於它的宣告方式,而是取決於它是不是用 new 來執行的。如果是用 new 執行一個 Function 時,我們就稱做這種呼叫為 建構式呼叫。當我們用建構式呼叫去執行一個 Function,這個 Function 就會被當作建構式。

使用 new 建構式呼叫的時候,實際上會有幾件事會被執行:

  • 首先會新建出一個物件。
  • 將物件的 .__proto__ 指向建構子的 prototype ,形成原型串鏈。
  • 將建構子的 this 指向 new 出來的新物件。
  • 回傳新建物件 ( 如果建構式本身沒有回傳東西的話 )。

先來看一下 function constructor 的實際應用,透過 function 的方式來建立一個新的物件,如果我們想要建立出同屬性名稱但不同屬性值的物件內容,我們可以把物件的屬性值變成參數,如此就能透過此 function constructor 建立出許多不同的物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.log = function () {
console.log(this.firstName+' '+this.lastName+ ', age:' + this.age);
}
};

var roman = new Person('Roman','Gonzalez', 18);
roman.log(); // Roman Gonzalez, age:18

var bobby = new Person('Bobby','Lashley', 20);
bobby.log(); // Bobby Lashley, age:20

console.log(roman.log === bobby.log) // false

name 跟 age 這兩個屬性在每一個 instance 都不一定會相同,而 log 這個 method 其實都在做同一件事情,而且還佔用了兩份空間。意思就是他們其實是兩個不同的 function 。

因為每一個 instance 彼此之間是可以共享方法的,所以我們可以透過把 log 這個 function 抽出來,指定在 Person.prototype 上面,這樣所有用 Person 建構出來的 instance 都可以共享這個方法,將可以有效減少記憶體占用的問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
};

Person.prototype.log = function () {
console.log(this.firstName+' '+this.lastName+ ', age:' + this.age);
}

var roman = new Person('Roman','Gonzalez', 18);
var bobby = new Person('Bobby','Lashley', 20);

console.log(roman.log === bobby.log) // true

共用的屬性或方法,不用每次都幫實體建立一份,提出來放到 prototype 即可。

以下是嘗試存取屬性時會發生的事:

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
// 利用含有 a 與 b 屬性的 f 函式,建立一個 o 物件:
let f = function () {
this.a = 1;
this.b = 2;
}
let o = new f(); // {a: 1, b: 2}

// 接著針對 f 函式的原型添加屬性
f.prototype.b = 3;
f.prototype.c = 4;

console.log(o.a); // 1
// o 有屬性「a」嗎?有,該數值為 1。

console.log(o.b); // 2
// o 有屬性「b」嗎?有,該數值為 2。
// o 還有個原型屬性「b」,但這裡沒有被訪問到。
// 這稱作「property shadowing」。

console.log(o.c); // 4
// o 有屬性「c」嗎?沒有,那就找 o 的原型看看。
// o 在「o.[[Prototype]]」有屬性「c」嗎?有,該數值為 4。

console.log(o.d); // undefined
// o 有屬性「d」嗎?沒有,那就找 o 的原型看看。
// o 在「o.[[Prototype]]」有屬性「d」嗎?沒有,那就找 o.[[Prototype]] 的原型看看。
// o 在「o.[[Prototype]].[[Prototype]]」是 null,停止搜尋。
// 找不到任何屬性,回傳 undefined。

增加原生原型的屬性和方法

上面都是設定自己建立的物件原型,也能修改預設的原生原型,但不要不小心把原本內建的屬性或方法給無意間覆蓋掉。

例如在 Array.prototype 上面加上函式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Array.prototype.push("Cyting")

var myname = [];
myname[0];    //"Cyting"


var ting = [];
ting[0];     //"Cyting"

Array.prototype.top = function () {
return this[0];
};

myname.top(); //"Cyting"
ting.top(); //"Cyting"

String.prototype.isLengthGreaterThan = function(limit){
return this.length > limit;
}

console.log("Cyting".isLengthGreaterThan(4)); // True

在 Array.prototype 中 push 一段字串進去,接著新增 top 函式回傳字串內容;只要宣告出陣列型別的資料型態,都會有該字串內容,也都能呼叫 top 函式。

修改 Prototype 方法

我們可以修改某個 function constructor 的 prototype 內容,當改變了該函式建構式中 prototype 內的方法,那麼所有根據這個建構式所建立的實例(不管是修改 prototype 之前或之後所建立的實例),都會套用更新後的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
};

Person.prototype.log = function () {
console.log(`Hi ${this.firstName} ${this.lastName}`);
}

var roman = new Person('Roman','Gonzalez', 18);
roman.log(); // Hi Roman Gonzalez
var bobby = new Person('Bobby','Lashley', 20);
bobby.log(); // Hi Bobby Lashley

// 修改原本的 log 方法
Person.prototype.log= function() {
console.log(`Hello, ${this.firstName} ${this.lastName}`);
}

roman.log(); // Hello, Roman Gonzalez
bobby.log(); // Hello, Bobby Lashley

原型鏈 Prototype Chain

當物件試圖存取一個物件的屬性時,其不僅會尋找該物件,也會尋找該物件的原型、原型的原型……直到找到相符合的屬性,或是到達原型鏈的尾端。

所有的物件裡面都會包含原型(prototype)這個物件,在 JavaScript 中這個物件的名稱為 __proto__ 。如果在原本的物件中找不到指定的屬性名稱或方法時,就會進一步到 __proto__ 裡面來找。

以上面 var roman = new Person(‘Roman’,’Gonzalez’, 18); 為例,當呼叫 roman.log() 方法,因為 roman 這個 instance 本身並沒有 log 這個 function,而 roman 是 Person 的 instance,所以如果在 roman 本身找不到,就會嘗試從 Person.prototype 去找。

我們想要取得某個物件的原型物件時,就可以透過以下兩個方式:

1
2
3
4
5
//可以使用 .__proto__ 來取得 [[Prototype]] 
roman.__proto__ === Person.prototype // true

//也可以用 Object.getPrototypeOf(..)來取得 [[Prototype]]
Object.getPrototypeOf(roman) === Person.prototype // true

roman 的 __proto__ 會指向 Person.prototype ,當發現 roman 沒有 log 這個 method 的時候,JavaScript 就會試著透過 __proto__ 找到 Person.prototype ,去看 Person.prototype 裡面有沒有 log 這個 method。

假如 Person.prototype 還是沒有呢?那就繼續依照這個規則,去看 Person.prototype.__proto__ 裡面有沒有 log 這個 method,就這樣一直不斷找下去。直到該物件的 __proto__ 為 null 為止,意思就是找到最上層了。

而上面這一條透過 __proto__ 不斷串起來的鍊,就稱為原型鍊。透過這一條原型鍊來呼叫自己 parent 的 method,讓 JavaScript 達到類似其他物件導向語言般的類別、繼承功能。

1
2
3
4
5
6
7
8
// roman.__proto__ 會指向 Person.prototype
console.log(roman.__proto__ === Person.prototype) // true

// Person.prototype.__proto__ 會指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) // true

// Object.prototype.__proto__ 會指向 null,這就是原型鍊的頂端了
console.log(Object.prototype.__proto__) // null

hasOwnProperty

如果想知道一個屬性是存在 instance 身上,還是存在於它屬於的原型鍊當中,可以用 hasOwnProperty 這個方法:

1
2
3
var roman = new Person('Roman','Gonzalez', 18);
console.log(roman.hasOwnProperty('log')); // false
console.log(roman.__proto__.hasOwnProperty('log')); // true

hasOwnProperty 只會檢查該物件,而不會檢查整條原型串鏈;可以透過迴圈來列出整個原型鏈且為可列舉的屬性;prop 是自訂的變數,會把該物件的屬性存在這個變數中,接著讀取下一個屬性,重覆直到沒有屬性為止,透過 for…in 把該物件中的所有屬性名稱和屬性值都列出來,列出的項目也包含被添加過的屬性或方法:

1
2
3
4
5
6
7
for (var prop in roman) {
console.log(prop);
}
//firstName
//lastName
//age
//log

instanceof

檢查物件是否為指定的建構子所建立的實體,位於 instanceof 左邊的運算元是物件,右邊的是函式,若左邊的物件是由右邊函式所產生的,則會回傳 true,否則為 false。

1
2
3
roman instanceof Person    // true
roman instanceof Object // true
roman instanceof Array // false

A instanceof B 就是拿來判斷 A 是不是 B 的 instance 只要能在 A 的原型鍊裡面找到 B 的 prototype,就會回傳 true。

isPrototypeOf

與 instanceof 不同之處只在於運算元的資料型別不同而已,但功能是相同的。

1
2
3
Person.prototype.isPrototypeOf(roman)  // true
Object.prototype.isPrototypeOf(roman) // true
Array.prototype.isPrototypeOf(roman) // false

如果 A.isPrototypeOf(B) 返回 true 則 B instanceof A 一定返回 true 。

Object.prototype

當我們嘗試在某個物件存取一個不存在該物件的屬性時,它會繼續往它的「原型物件」[[prototype]] 去尋找,直到找到 Object.prototype 才停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 這兩個互為彼此的 instance
console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true

// Function 的 __proto__ 會指向 Function.prototype
// Function.prototype 的 __proto__ 會指向 Object.prototype
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.__proto__.__proto__ === Object.prototype); //true

// Object 的 __proto__ 會指向 Function.prototype
console.log(Object.__proto__ === Function.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__); //null

constructor

每一個 prototype 都會有一個有 constructor 屬性,而這個屬性會指向構造函數,例如 Person.prototype 的構造函數就是Person。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
};

Person.prototype.log = function () {
console.log(`Hi ${this.firstName} ${this.lastName}`);
}
var roman = new Person('Roman','Gonzalez', 18);


// 往原型鍊去找
console.log(roman.constructor === Person); // true
console.log(roman.hasOwnProperty('constructor')); // false

// Person 的 constructor 就是 Person
console.log(Person.prototype.constructor === Person); // true
console.log(Person.prototype.hasOwnProperty('constructor')); // true

X.prototype.constructor === X ,將 X 用 Function, Person, Object 帶進去都會是 true。

Object.create() and new operator 差異

以下為使用 Object.create 方式:

1
2
3
4
5
6
7
8
9
10
var dog = {
eat: function() {
console.log(this.eatFood)
}
};

var maddie = Object.create(dog);
console.log(dog.isPrototypeOf(maddie)); //true
maddie.eatFood = 'NomNomNom';
maddie.eat(); //NomNomNom

執行流程如下:

  1. 建立一個 dog 物件。
  2. 使用 Object.create(dog) 將 將 maddie 的 [[Prototype]] 指向 dog 。
  3. 檢查 maddie 的 prototype 是不是 dog,回傳 true 。
  4. 設定 maddie 的 eatFood 屬性值。
  5. 呼叫 eat function。
  6. Javascript 透過 prototype chain 找到 eat method 。

以下為使用 new operator 方式:

1
2
3
4
5
6
7
8
9
10
var Dog = function(){
this.eatFood = 'NomNomNom';
this.eat = function(){
console.log(this.eatFood)
}
};

var maddie = new(Dog);
console.log(maddie instanceof Dog); // True
maddie.eat(); //NomNomNom

執行流程如下:

建立一個 maddie 物件。

  1. 透過 function constructor 將 maddie 的 [[Prototype]] 指向 Dog 的 Prototype。
  2. 設定建構式中的 this 到物件。
  3. 回傳所建立的物件。
  4. 檢查 maddie 是不是 Dog 的 instance ,回傳 true 。
  5. 呼叫 eat function。
  6. Javascript 透過 prototype chain 找到 eat method 。

再看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Dog(){
this.pupper = 'Pupper';
};

Dog.prototype.pupperino = 'Pups.';
var maddie = new Dog();
var buddy = Object.create(Dog.prototype);

//Using Object.create()
console.log(buddy.pupper); //Output is undefined
console.log(buddy.pupperino); //Output is Pups.

//Using New Keyword
console.log(maddie.pupper); //Output is Pupper
console.log(maddie.pupperino); //Output is Pups.
1
console.log(buddy.pupper); //Output is undefined

大家有注意到 buddy.pupperundefined 嗎?即使 Object.create() 將 prototype 指定為 Dog ,但是 buddy 並沒有存取到建構式中的 this.pupper ,兩者之間的主要差別就是 new Dog 會執行建構式中的程式碼,而 object.create 並不會執行建構式裡面的內容。

以下為另一個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Test(){
this.prop1 = 'prop1';
this.prop2 = 'prop2';
this.func1 = function(){
return this.prop1 + this.prop2;
}
};

Test.prototype.protoProp1 = 'protoProp1';
Test.prototype.protoProp2 = 'protoProp2';
var newKeywordTest = new Test();
var objectCreateTest = Object.create(Test.prototype);

/* Object.create */
console.log(objectCreateTest.prop1); // undefined
console.log(objectCreateTest.protoProp1); // protoProp1
console.log(objectCreateTest.__proto__.protoProp1); // protoProp1

/* new */
console.log(newKeywordTest.prop1); // prop1
console.log(newKeywordTest.__proto__.protoProp1); // protoProp1

此範例比較三個不同的建立方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Test(){
this.prop1 = 'prop1';
this.prop2 = 'prop2';
this.func1 = function(){
return this.prop1 + this.prop2;
}
};

Test.prototype.protoProp1 = 'protoProp1';
Test.prototype.protoProp2 = 'protoProp2';

var a = new Test();
var b = Object.create(Test.prototype);
var c = Object.create(Test);

讓我們來看一下 a ,b ,c 三個物件內容在 console 輸出的結果:

結論

所謂的 Prototype 就是原型,所有函式上面都有預設有一個公開的 Prototype 屬性,而被此建構式新建出來的物件都可以透過 [[Prototype]] 參考連結到這個原型物件上來存取其上的屬性,也就能建立出 類似物件導向概念 的程式。

整理一下本篇中出現的名詞:

  • Prototype

    也就是原型物件。

    所有函式上面都有預設有一個公開的 Prototype 屬性(原型物件)。被函式建立出的物件都可以存取其對應的原型物件。

  • [[Prototype]]

    用來讓物件能夠連結到其原型物件上的一個參考。

    這是一個不允許外部存取的屬性。

  • __proto__

    相當於可存取的 [[Prototype]],為了方便存取 Prototype 而生。

    雖已於 ES6 中列為標準,但為了效能考量還是不建議使用。

參考文獻

  1. https://blog.techbridge.cc/2017/04/22/javascript-prototype/

  2. https://cythilya.github.io/2018/10/26/prototype/

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

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

  5. https://pjchender.github.io/2018/08/01/js-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-javascript-object-oriented-javascript/

  6. https://medium.com/@jonathanvox01/understanding-the-difference-between-object-create-and-the-new-operator-b2a2f4749358

  7. https://stackoverflow.com/questions/4166616/understanding-the-difference-between-object-create-and-new-somefunction

JavaScript Copy




如果我想要複製物件的值,卻不希望在複製後,只要更動其值,原變數也會受到影響,該怎麼做呢?

本篇將介紹關於 JavaScript 中的「淺拷貝」以及「深拷貝」。


原始型別在賦值時一般是直接傳值的方式(pass by value):

1
2
3
4
5
6
let a= 1;
let b = a;
b = 2;

console.log(a); //1
console.log(b); //2

但是物件會是傳指標(pass by Reference):

1
2
3
4
5
6
let obj1 = { a:1, b:2, c:3};
let obj2 = obj1;
obj2.b = 4

console.log(obj1) //{ a:1, b:4, c:3}
console.log(obj2) //{ a:1, b:4, c:3}

淺拷貝 Shallow Copy

原本 ab 是指向同一塊記憶體位址;透過重新賦值的方式,讓 b 指向一個新的記憶體位址:

1
2
3
4
5
6
7
8
9
var a = {name: 'William'}

var b = a;

b = {name: "Teddy"};

console.log(a.name); // William

console.log(b.name); // Teddy

同樣的範例還有這個,我們直接把 a 取出的值放到屬性名稱後面:

1
2
3
4
5
6
7
8
9
10
var a = {name: 'William', age: 70};

var b = {name: a.name, age: a.age};

b.name = "Teddy";


console.log(a.name); // William

console.log(b.name); // Teddy

也可以使用 Object.assign 的方式來複製:

1
2
3
4
5
var a = {name: 'William', age: 70};
var b = Object.assign({}, a);
b.name = "Teddy";
console.log(a.name); // William
console.log(b.name); // Teddy

但是以上使用淺拷貝只能複製一層。

當拷貝到第二層的時候就會發生問題:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var obj = {

a: 1,

b: {

b1: 2,

b2: 3

}
};
var obj2 = Object.assign({}, obj);


obj2.a= 10;

obj2.b.b1= 20;


console.log(obj.a); // 1

console.log(obj.b.b1); // 20

物件 obj2 改變 a 並不會影響物件 obja ,但是改變再深一層的物件的值,就會影響到原本 ab.b1 了!

讓我們在看一個例子:

1
2
3
4
5
6
7
8
let obj1 = {a:{b:5}};
let obj2 = {a:obj1.a};//都把a指派到{b:5}這個物件的reference
obj2.a.b = 10

console.log(obj1) //{a:{b:10}}; 被指派到10
console.log(obj2) //{a:{b:10}};
console.log(obj1 === obj2) //false 實際上不是同物件    
console.log(obj1.a === obj2.a) //true 但是第二層物件實際上相同

當我們要確保物件是整個複製,而不是只複製 reference 時就需要用到 Deep Copy

深拷貝 Deep Copy

JSON.parse(JSON.stringify(object_array)):

  • JSON.parse():把字串轉成物件
  • JSON.stringify():把物件轉成字串。

我們可以把物件先轉換成 JSON 再 parse 回來

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {
    a: 1,
    b: {
        b1: 2,
        b2: 3
    }
};

var obj2 = JSON.parse(JSON.stringify(obj));

obj2.a= 10;
obj2.b.b1= 20;

console.log(obj.a); // 1
console.log(obj.b.b1); // 2

也可以透過使用第三方套件(ex: JQuery、lodash)的方式:

Lodash
1
2
3
4
5
6
7
8
9
var obj = {
    a: 1,
    b: {
        b1: 2,
        b2: 3
}
};

var obj2 = cloneDeep(obj);
Jquery

深拷貝和淺拷貝對應的參數是 true 或 false 。
預設情況是 false(淺拷貝),而且 false 不能夠寫出來,也就是你想要淺拷貝,那就什麼參數都不用帶。如果想帶入參數,只能傳入 true(深拷貝)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var obj = {};
var obj2 = {
    a: 1,
    b: {
        b1: 2,
        b2: 3
    }
};
// 深拷貝

$.extend(true, obj, obj2);

obj.b.b1= 20;

// b 並沒有因為 a 的改動也跟著改動
console.log(obj2.b.b1); // 2

// 至於沒有加上參數就是淺拷貝
$.extend(obj,obj2);

參考文獻

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

  2. https://dustinhsiao21.com/2018/01/07/javascript-shallow-copy-and-deep-copy/