WebSocket-簡易server端實作

server 端環境建置

首先,創立一個簡易的 WebSocket 資料夾專案,裡面有一個 index.js 檔案,以及一個 server 資料夾

1
2
3
WebSocket
|--index.html
|--server(forder)

啟用 terminal,進入 WebSocket 專案資料夾中做初始化設定:

1
npm init

接著,系統執行過程中,會被要求輸入幾個欄位(可以直接按 enter 略過到下一個欄位)

  • package name: 你這個 Project 要叫什麼名字
  • version: 你決定這個 Project 現在該是第幾版
  • description: Project 基本介紹
  • entry point: 進入點,如果要跑你的 Project 應該要執行哪個檔案
  • test command:
  • git repository:
  • keywords:
  • author: 作者(自己)
  • license: 你這個 Project 是採用什麼授權的

結束後,可以看到 server 這個資料夾底下,自動產生了一個 Package.json

接著,在 server 資料夾中,手動新增 index.js 檔案

1
2
3
4
5
WebSocket
|--index.html
|--server(forder)
|-package.json
|-index.js

資料架構建置完,接著要在其中安裝 WebSocket:

1
npm install ws


安裝完畢後,專案結構中會多出幾個檔案及資料夾,package-lock.json 中紀錄安裝套件相容的版本。

到目前為止,基本的環境建置完成。

建置後端 server

這裡示範模擬 server,以本機端 5001 埠,作為配置 server 的接口。
在資料夾中的 index.js 檔案中,寫入以下程式碼:

1
2
3
4
5
6
7
8
9
10
//導入WebSocket模組
var WebSocket = require('ws');

//引用Server類
var WebSocketServer = WebSocket.Server;

//建立實體(實例化)
var wss = new WebSocketServer({
port: 5001,
});

也可以將上述三行程式碼,簡易化為下述兩行寫法

1
2
3
4
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({
port: 5001,
});

啟用 Server

「control + ` 」快捷鍵開啟 vscode 中的終端機。在終端機可以先確認當前位置,用下述指令進入 server 資料夾中:

1
cd server

在 server 中,輸入下述指令,啟用 server

1
node index.js

這樣的狀態就代表 server 正在運轉啟用中

為了確認 server 為啟動狀態,可以由前端測試是否可以連接上後端。
在專案中的 index.html 檔案代表前端(用戶端),在其中輸入指令,透過 server 端的 port:5001 接口,讓前端與後端建立連結:

1
2
3
4
5
6
7
8
9
10
var sock = new WebSocket('ws://localhost:5001');
sock.onopen = function(event) {
//當為連結狀態,執行下述動作
console.log('Connected successfully!');

//製造ㄧ秒時間差,再傳送訊息給server
setTimeout(function() {
sock.send('Hey there');
}, 1000);
};

啟用瀏覽器 locolhost:,查看開發工具 network,可以看到順利連結上 server,並執行從瀏覽器傳送訊息給 server 端。

server 端偵聽事件

首先建立 WebSocket 的 server 實體,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//先引用Server類
//再new出實體,並asign給wss變數
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({ port: 5001 });

//當偵聽到連結建立時,執行動作
wss.on('connection', function(ws) {
console.log('Connected!');

//當偵測收到訊息時,執行動作
ws.on('message', function(message) {
console.log('Receieve:' + message);
});
});

用 vsCode 打開 terminal 查看 sever 端的 log 訊息,可以檢查看到確實有執行動作。

Server 端回傳訊息給前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wss.on('connection', function(ws) {
console.log('Connected!');

//當偵測收到訊息時,執行動作
ws.on('message', function(message) {
console.log('Receieve:' + message);

if (message == 'Hello') {
//從server回送訊息給前端
ws.send('Hey there from the server');
console.log('1');
} else {
ws.send('what did you say?');
console.log('2');
}
});
});

從瀏覽器的開發工具 NetWork>WS 可以檢查到前、後端訊息傳遞的狀況。

WebSocket-基礎介紹與實體物件

WebSocket 介紹

What is WebSocket?

WebSocket 是一種通訊協定(protocal),可在單個 TCP 連接上進行全雙工通訊。
在 WebSocket API 中,瀏覽器和伺服器只需要完成一次交握,兩者之間就可以建立永續性的連接,並進行雙向資料傳輸。

HTTP vs. WebSocket

最普遍常見的 protocal 是 HTTP,不過 HTTP 有個缺陷:溝通只能由客戶端(browser)發起,無法做到由服務端(server)主動向客戶端發送訊息。效率低,必須不停地發送請求確認連結。

WebSocket 與 HTTP 最大的不同是,WebSocket 是一個持續的雙向連線,不需要重新連線,不需要重新傳送 request,反應更即時。

  • HTTP 是單向請求,必須由 browser 先發出一個 request,server 端才會回傳一個 response
  • WebSocket 則是完成一次”握手”驗證之後,即可執行雙邊的訊息傳送,或是由 server 端主動回傳訊息

Why WebSocket?

當我們需要客戶端 browser 需要與 server 端保持即時、持續的雙向溝通。例:Chat apps、Real time data analytics、Social media

WebSocket 實體的方法與屬性

建立一個 WebSocket 物件

首先必須建立一個 WebSocket 物件,才能讓瀏覽器、伺服器以 WebSocket 協定進行通訊,物件一但被建立,就會自動與伺服器連線

WebSocket 的建構子,有兩個參數:

  1. url
    URL 的協議類型必須是 ws:// (非加密連線)或是 wss:// (加密連線)

  2. protocols,選擇性參數
    一個字串,或是字串組成的陣列,因此一個 Server 可以實作多個 WebSocket 子協定。

1
2
3
4
5
6
7
8
//僅傳入連線用的URL
new WebSocket('URL');

//指定伺服器使用某個sub protocol
new WebSocket('URL', 'prorocol');

//指定伺服器執行多個sub protocols
new WebSocket('URL', 'protocols[]');

下方簡單建立了一個新的 WebSocket,連到位於 http://echo.websocket.org 的伺服器。

1
var MySocket = new WebSocket('ws://echo.websocket.org');

將這段程式碼貼到瀏覽器的 console 中,執行完,查看 Network>WS,已經連上 echo.websocket.org 伺服器。

且再查看 Headers 中,可以看到狀態「101 Web Socket Protocol Handshake」

WebSOcket 的其他屬性及方法,參考這裡

查詢 WebSocket 狀態

WebSocket 物件中的 readyState 屬性是表示目前狀態,有四種值,分別代表不同的狀態。

  • WebSocket.CONNECTING:值為 0,表示正在連接中
  • WebSocket.OPEN:值為 1,表示已連結成功,可以進行通訊
  • WebSocket.CLOSING:值為 2,表示正在關閉
  • WebSocket.CLOSED:值為 3,表示為關閉狀態

可以透過狀態判斷,來做一些事,以下舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch (MySocket.readyStatus) {
case WebSocket.CONNECTING:
//do something
break;
case WebSocket.OPEN:
//do something
break;
case WebSocket.CLOSING:
//do something
break;
case WebSocket.CLOSED:
//do something
break;
default:
//this would never happen
break;
}

WebSocket.onopen

WebSocket 物件中的onopen屬性,可以用於定義當連結成功後的回調函式,也就是當 WebSocket 完成連結時,會執行函式中的動作。

1
2
3
mySocket.onopen = function(event) {
//do something
};

若要指定多個回調函式,可以用addEventListener方法。

1
2
3
mySocket.addEventListener('open', function(event) {
//do something
});

WebSocket.onclose

WebSocket 物件中的onopen屬性,可以用於定義當關閉連結後的回調函式,也就是當 WebSocket 關閉連結時,會執行函式中的動作。

1
2
3
4
mySocket.onclose = function(event) {
//do something
//dandle close event
};

若要指定多個回調函式,可以用addEventListener方法。

1
2
3
mySocket.addEventListener('close', function(event) {
//handle close event
});

WebSocket.onmessage

WebSocket 物件中的onmessage屬性,用於定義收到 server 傳來資料後的回調函式,也就是當偵聽到 sever 傳來訊息的事件,會執行函式中的動作。

1
2
3
4
mySocket.onmessage = function(event) {
var data = event.data;
//do something for data
};

若要指定多個回調函式,可以用addEventListener方法。

1
2
3
4
mySocket.addEventListener('message', function(event) {
var data = event.data;
//do something for data
});

WebSocket.onerror

WebSocket 物件中的onerror屬性,用於定義報錯時的回調函式,也就是當偵聽到錯誤事件,會執行函式中的動作。

1
2
3
mySocket.onerror = function(event) {
//handle error event
};

若要指定多個回調函式,可以用addEventListener方法。

1
2
3
mySocket.addEventListener('error', function(event) {
//handle error event
});

傳資料給伺服器

WebSocket 的send()方法,用於向伺服器傳送訊息。

1
mySocket.send('message to server');

可以傳送的格式,包含字串、Blob、ArrayBuffer。

若要傳送複雜的資料給伺服器,可以用 JSON 格式傳送物件,以下以聊天程式應用舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var time = new Date();

//創建WebSocket實體
var mySocket = new WebSocket('wss://echo.websocket.org');

//當WebSocket狀態為連線時,執行的動作
mySocket.onopen = function() {
var msg = {
type: 'message',
text: document.getElementById('text').textContent,
id: 'clientID',
data: time.getTime(),
};

//先將資料轉為JSON格式,再傳資料給server
mySocket.send(JSON.stringify(msg));
};

//當瀏覽器從伺服器接收到訊息,偵聽事件被傳入onmessage並觸發函式執行
mySocket.onmessage = function() {
setTimeout(function() {
document.getElementById('text').innerHTML = '<b>Message already sent.</b>';
}, 5000);
};

參考codepen 執行結果

從伺服器接收訊息

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
mySocket.onmessage = function(event) {
setTimeout(function() {
document.getElementById('text').innerHTML = '<b>Message already sent.</b>';
}, 1000);

console.log(event);
var f = document.getElementById('chatbox');
var text = '';
var msg = JSON.parse(event.data);
var time = new Date(msg.date);
var timeStr = time.toLocaleTimeString();

//僅示範switch判斷,此案例會接到的為'message'
switch (msg.type) {
case 'id':
clientID = msg.id;
setUserName();
break;
case 'message':
text =
'使用者 <em>' + msg.id + '</em> :<b>' + msg.text + '<br></b>' + '<small>登入於 ' + timeStr + '</small><br>';
break;
default:
break;
}
if (text.length) {
setTimeout(function() {
document.getElementById('chatbox').innerHTML = text;
}, 2000);
}
};

參考codepen 執行結果

關閉連線

WebSocket 的close()方法,用於結束 WebSocket 連線。

1
mySocket.close();

關閉連線後,再次查看連線狀態為 3,代表狀態為 CLOSED 關閉的狀態。

參考資料

JavaScript-獲取當前時間

Date 物件是基於世界標準時間(UTC) 1970 年 1 月 1 日開始的毫秒數值來儲存時間。

Date 建構子語法

透過建構子創建 Date 實體物件,建構子有下述四種:

1
2
3
4
5
6
7
8
9
10
11
12
13
//預設為「當前時間」的物件
new Date();

//傳入「某時間」距離標準時間「毫秒數」
new Date(value);

//表示時間日期的字串。
//這個字串應該要能被 Date.parse()方法解析
//格式因瀏覽器而不同,強烈不建議使用解析字串的方式建立 Date 物件。
new Date(dateString);

//不需要全部參數皆傳入,部分參數為選用
new Date(year, month, day, hour, minutes, seconds, milliseconds);
  • year:表示年份的整數。當數值落在 0 到 99 之間,表示 1900 到 1999 之間的年份。參考下面的範例.
  • month:表示月份的整數。由 0 開始(一月)到 11 (十二月)。
  • day:選用。表示月份中第幾天的整數值。
  • hour:選用。表示小時數的整數值。
  • minute:選用。表示分鐘數的整數值。
  • second:選用。表示秒數的整數值。
  • millisecond:選用。表示毫秒數的整數值。

創建 Date 實體

1
2
3
4
5
//先創建一個Date實體
var time = new Date();

//試著印出實體查看
console.log(time); //Wed May 22 2019 10:42:52 GMT+0800 (台北標準時間)

Getter

下方示範幾個常用的 getter,更多請參考這裡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//先創建一個Date實體
var time = new Date();

//獲取當前時間(取得的值為一個毫秒数值)
var theTime = time.getTime(); //1558492972644

var timeDetails = {
year: time.getFullYear(),
month: time.getMonth() + 1,
date: time.getDate(),
hour: time.getHours(),
minute: time.getMinutes(),
second: time.getSeconds(),
};

Conversion getter

下方示範將 Date 物件轉換為可閱讀的字串型式,並回傳 Date 的部分資訊

1
2
3
time.toLocaleString(); //2019/5/22 上午10:42:52
time.toLocaleDateString(); //2019/5/22
time.toLocaleTimeString(); //上午10:42:52

更多 Date 參考:MDN web doc-Date

JavaScript-參數(argument)與其餘參數(spread)

函式執行時的參數變數

重新複習一下,當函式被呼叫執行時,會創建一個新的執行環境,而在創建階段的同時,JavaScript引擎會設定幾個東西:

  • 「變數環境」用來包住變數
  • 給範圍鍊(Scope Chain)的「外部參考環境」
  • 特殊關鍵字「this」
  • 另一個特殊關鍵字「arguments」

即使我沒有在函式中宣告過arguments這個變數,當我在執行環境中直接呼叫arguments這個關鍵字,會印出有傳入值的所有參數。

1
2
3
4
function greet(firstName,lastName,language){
console.log(arguments);
}
greet('Amy','Lin','En');

印出的東西會是由斜體中括號 [] 呈現,有點像陣列,可是不太一樣,我們稱作為array-like,只有部分陣列的功能。

預設參數為undefined

在JavaScript中,可以呼叫函式卻不傳入任何參數。因為當函式被呼叫執行時,在創建階段就會先在記憶體空間設定好這些值為undefined,所以並不會出錯。

1
2
3
4
5
6
7
function greet(firstName, lastName, language){
console.log(firstName);
console.log(lastName);
console.log(language);
}

greet();

當我執行印出函式中的參數時,因為未傳入、賦予值給參數,所以就會印出預設值為undefined。
Uploading file..._zdbxuzz93

由左到右傳入參數值

JavaScript可以處理為傳入參數的函式,也可以處理只傳入部分參述的函式。

1
2
3
4
5
6
7
8
9
10
function greet(firstName, lastName, language){
console.log(firstName);
console.log(lastName);
console.log(language);
console.log('--------------')
}

greet('Amy');
greet('Amy','Lin');
greet('Amy','Lin','English')

也就是說,當我宣告一個函式時,函式中的參數有三個,而當我呼叫執行時,我可以選擇不傳入值,也可以選擇只傳入一個或兩個,當然也可以傳入三個,可是要記得,一定是由左至右傳入設定做處理。

ES6語法:預設參數

當不確定每次函式被呼叫時,會傳入幾個參數,卻不希望未被設定的參數值為undefined,在ES6版本中,可以自定義為參數設定預設值。

1
2
3
4
5
6
7
function greet(firstName, lastName, language='English'){
console.log(firstName);
console.log(lastName);
console.log(language);
}

greet();

也就是當呼叫函式時,若沒有給予language這個參數的值,則會在函式中賦值’language’給該參數。

不過因為這種ES6語法只適用於部分瀏覽器,所以下述寫法,也可以達成相同的目的。

1
2
3
4
5
6
7
8
9
function greet(firstName, lastName, language){

language = language||'English';

console.log(firstName);
console.log(lastName);
console.log(language);
}
greet();

當呼叫greet()卻沒有傳入language參數的值,執行到上述第三行程式碼時,則會判斷出language為undefined,所以賦予值為’English’。

下方獨立解析這行「自定義參數預設值」程式碼意思。

1
language = language||'English'

其中看到兩個運算子,分別是’=’以及’||’,因為’||’的優先權較高,所以會先執行這個部分,接著才去執行’=’。

首先,執行’||’左邊的程式碼,若language為undefined,會被強制轉型為falese,而繼續執行’||’右邊的程式碼,獲得’English’的這個值。

接著,執行’=’的動作,就會將’English’賦值給變數language。

其餘參數(spread parameter)

其餘參數是arguments被傳入函式時,還沒有被指定變數名稱的引數。

1
2
3
4
5
function greet(firstName, lastName, ...otherP){
console.log(otherP);
}

greet('Amy','Lin','female','English')

其餘參數的變數名稱可以自定義,範例中使用otherP代表其餘參數。
所以剩餘沒有被直接寫出來的參數,就會被包進這個otherP陣列中。

JavaScript-「沒有」重載函式(function overloading)

首先,先了解其他程式語言有的重載函式(function overloading)特性,在C#、C++、Java中,都有重載函式的概念。

重載函式

多個函式可以重複使用同一個函式的名稱,只要有不同數量的參數,就會被判斷為不同個函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//這裡僅用JS的語法表示其他程式語言的重載函式,但在JS中,無法正常執行
function greet(){
console.log('hi');
}
function greet(name, age){
console.log(name);
console.log(age);
}
function greet(height,weight,age){
console.log(height);
console.log(weight);
console.log(age);
}

//呼叫函式時,帶入不同數量的參數
//就會自動判斷是呼叫哪一個函式
greet();
greet('Amy','20');
greet('70','180','male');

JS「沒有」重載函式

在JavaScript中,函式就是物件,一個變數名稱只能代表一個物件,若用同一個名稱宣告多個函式,後方宣告的函式內容就會覆蓋前方的,所以JavaScript沒有處理重載函式的功能。

通用模式處理

在一般的狀況,若參數值得狀況很多樣,並且要讓函式判別遇到不同參數,需要做出不同的處理過程,可以針對傳入參數做判斷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function greet(firstName, lastName, language){

language = language||'English';

if(language === 'English'){
console.log('Hi!'+firstName+' '+lastName);
}
if(language === 'Spanish'){
cosole.log('Hola!'+firstName+' '+lastName);
}
}

greet('Amy','Lin','English');
greet('John','Chen','Spanish');

不過,為了簡化呼叫時的傳入資訊,可以改寫為下方常見的模式寫法。
額外新增兩個函式,在其中去執行呼叫不同傳入參數的函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function greet(firstName, lastName, language){

language = language||'English';

if(language === 'English'){
console.log('Hi!'+firstName+' '+lastName);
}
if(language === 'Spanish'){
cosole.log('Hola!'+firstName+' '+lastName);
}
}

function greetEnglish(firstName, lastName){
greet(firstName, lastName, 'English')
}
function greetSpanish(firstName, lastName){
greet(firstName, lastName, 'Spanish')
}

greetEnglish('Amy','Lin');
greetSpanish('John','Chen');

JavaScript-陣列

宣告陣列

1
2
3
4
5
//一般宣告語法
var arr = new Array();

//或使用陣列實體語法
var arr = [];
1
2
3
4
5
6
7
//在中括號中,用逗號分隔值
var arr = ['a','b','c']

//index從0開始,抓出陣列中的值
arr[0];//a
arr[1];//b
arr[2];//c

JS的陣列特性

在其他程式語言的陣列中,通常都能包含同一種型別的值。比如說,數字陣列、字串陣列、物件陣列。

可是在JavaScript中,因為「動態型別」的特性,陣列中可以混合各種不同型別的值,JavaScript會自動去判別每一個值得型別。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var arr = [
1,
'member',
false,
{
name:'Amy',
age:'20'
},
function(name){
var greeting = 'hi';
console.log(greeting + name)
},
'newMember'
];

console.log(arr);


若要執行陣列中的函式,可以使用陣列的index,呼叫出陣列中的東西,包含陣列中函式、字串、物件…等。

1
arr[4](arr[3].name);

JavaScript-"this"關鍵字

全域this

在全域環境中取用this,會指向全域物件。

1
console.log(this);

而在瀏覽器中執行,瀏覽器的全域物件為window物件。

函式中的this

每當一個函式被呼叫,會創造出一個新的執行環境,且同時會產生一個未曾宣告的”this變數”,然而,”this變數”的值指向哪個物件,會依據函式在何處被呼叫而決定。

下述例子中,先有一個函式,其中log出this。當使用a()呼叫執行,代表執行該函式的程式屬性(code),也就是函式中的全部程式。

1
2
3
4
function a(){
console.log(this);
}
a();

實行結果,在a函式中的this,同樣指向全域物件的Window。

下述另一個案例,使用函式表示式來設定一個物件函式,再將之assign給變數b。

1
2
3
4
var b = function(){
console.log(this);
}
b();

執行結果,在b函式中的this,同樣指向全域物件的Window。

函式中創建全域物件

下述程式碼統整目前的案例,有三個執行環境被產生:

  1. 全域環境
  2. 呼叫函式a,產生的區域環境
  3. 呼叫函式b,產生的區域環境

在每個執行環境中,分別產生三個不同的this變數,可是三個this都指向同一個位址(同一個全域物件window)。

1
2
3
4
5
6
7
8
9
function a(){
console.log(this);
}
var b = function(){
console.log(this);
}

a();
b();

小結,在函式中呼叫this,會指向全域變數,而非函式本身。

在下述程式碼中,在a函式中的this代表全域物件Window,所以宣告this.newVar代表宣告一個全域變數。
也因此,直接在全域log出變數newVar不會出錯,因為它確實在執行a函式時,就已經被宣告出來了。

1
2
3
4
5
6
function a(){
console.log(this);
this.newVar = 'hello';
}
a();
console.log(newVar);

執行結果,會將newVar印出值為’hello’,且若點開Window物件中,也可以看到確實有一個變數為newVar。

物件中的this

宣告一個物件c,在其中有屬性name,也有方法log,接著用c.log()執行物件c中的方法log。

1
2
3
4
5
6
7
var c = {
name:'The c obj',
log: function(){
console.log(this);
}
}
c.log();

執行結果,在物件c的方法log中的this,代表物件c。

小結,當函式是物件的方法時,在其中呼叫的this變數會指向該物件本身。換句話說,this變數會指向包含它的物件。

在物件方法中修改物件的屬性

物件c的log方法中,因為this代表物件c本身,所以用this.name代表物件c的屬性,藉此更改其值。

1
2
3
4
5
6
7
8
var c = {
name:'The c obj',
log: function(){
this.name = 'update c obj'
console.log(this);
}
}
c.log();

執行結果,可以看到物件c的屬性name確實被修改了。

self宣告模式,解決JS的不完美

在物件d的方法log中,宣告一個函式物件setName,在其中的this變數並不會指向物件d,而是指向全域物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var d = {
name: 'The d obj',
log: function(){
this.name = 'update d obj';
console.log(this.name);

var setName = function(newName){
this.name = newName;//這裡的this會設置到全域物件window中
console.log(this.name);
}
setName('update again!');
console.log(this.name);
}
}

許多人會認為這是JavaScript本身的bug,但是事實上他就是這麼運作,因此若要避免產生錯誤,業界有一套常用的模式來應付這種狀況。

通常會在物件方法的第一行,宣告一個新的變數self,並且讓this指派給他,因為物件的assign是call by reference,因此self會指向this同一個位置,確保兩者指向同一物件e。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var e = {
name: 'The e obj',
log: function(){
var self = this;
self.name = 'update e obj';
console.log(self.name);

var setName = function(newName){
self.name = newName;//這裡的this會設置到全域物件window中
console.log(self.name);
}
setName('update e again!');
console.log(self.name);
}
}
e.log();

當有了一個變數self,之後修改或呼叫,就直接用self以確保是指向物件e。

JavaScript-傳值與傳參考

傳值(by value)

首先宣告一個變數a,並且assign一個純值(primitive)給a,則在記憶體中會有以個0x001位置存有該值,等著被a參照。
接著,宣告一個變數b,並且將a指派給b (b = a),實際上的執行狀況,會將a參照到的記憶體位置中的值複製到一個新的記憶體位置0x002,再讓b可以參照到它。

以下方程式碼為例,當執行到第6行,若重新assign一個值給a,並不會影響到b的值,因為a跟b是指向不同的記憶體位置,兩者的值不會互相被影響。

1
2
3
4
5
6
7
8
9
//call by value(primitives)
var a = 3;
var b;

b = a;
a = 2;

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

傳參考(by reference)

首先有一個變數a,將一個物件(包含函式)指派給該變數,則在記憶體中會有一個位置0x001存有該物件,並透過記憶體位置被a參照到。
接著,宣告一個變數b,並且將a指派給b (b = a),此時並不會得到一個新的物件以及新的記憶體位置,而是會讓b也參照到跟a同個物件所在記憶體中的位置,也就是同樣指向0x001。
這也代表,如果a的值被改變,b的值也同樣會被改變,因為它們兩者指向的物件就是同一個物件。

以下方程式碼為例,當執行到第6行,執行mutate,改變a物件中的值,接著再分別印出a、b的值,會發現兩者的結果皆被改變為mutate後的值了。

1
2
3
4
5
6
7
8
9
// call by reference(all objects, including functions)
var a = { greeting:'hi'};
var b;

b = a;
a.greeting = 'hello';

console.log(a);//{ greeting:'hello'}
console.log(b);//{ greeting:'hello'}

傳參考(用於參數)

在第5行,b已經指向a同個物件的記憶體,而當第9行將b作為傳入參數作為函式中obj物件,在第7行被mutate之後,最終a跟b所印出的值皆相同,因為兩者皆是指向同個記憶體位置中的同個物件。

1
2
3
4
5
6
7
8
9
10
11
12
//call by reference (even as parameters)
var a ={ greeting:'hi'};
var b;

b = a;
function changeGreeting(obj){
obj.greeting = 'hola';
}
changeGreeting(b);

console.log(a);//{ greeting:'hola'}
console.log(b);//{ greeting:'hola'}

使用等號運算子,創建一個新的記憶體空間

透過”=”運算子創建物件,藉由這個物件實體語法,因為不知道{ greeting:'hola'}是否已經存在於記憶體,所以會另外創建一個記憶體空間給新的物件。

1
2
3
4
5
6
7
8
9
//equals operator sets up a new memory space(new address)
var a = { greeting:'hi'};
var b = a;

a.greting = 'hello';
a = { greeting:'hola'};

console.log(a);//{ greeting:'hola'}
console.log(b);//{ greeting:'hello'}

JavaScript-函數陳述式與函數表示式

陳述式vs.表示式

  • 陳述式(Statement)只是去做某件事。
  • 表示式(Expression)是程式碼的單位,位,會形成一個值,但不一定要存於一個變數中。

    陳述式

    if本身只是一個陳述句,它會去做其他事,但是不會回傳任何值。而在if的()中的條件是表示式,因為他會回傳一個值,true或false。
    1
    2
    3
    4
    var a;
    if (a === 3) {

    }

表示式

以下三行分別的程式碼,都是一個表示式,因為他們執行完之後,都會回傳一個值。

1
a = 3;

1
1 + 2;//3
1
a = {language:'english'}

以下執行結果可證,程式碼會回傳一個值,這個值可以是數值、字串或物件,無論是什麼型態都可以。

函數陳述式

當它被呼叫執行,它不會回傳值

1
2
3
4
function greet(){
console.log('hi');
}
greet();

函數表示式

先宣告一個變數,並將一個函式assigm給該變數。因為函式在JavaScript中是一個物件,所以當該函式被指派給anonymousGreet變數時,在記憶體中的,anonymousGreet變數就會指向函式物件

1
2
3
4
var anonymousGreet = function(){
console.log('hi');
}
anonymousGreet();

在這個例子中,該函式中並沒有name,是一個匿名函式,不過因為被assign給變數,所以可以透過該變數名去參照它。

所以在下圖中特別被標記出的這段程式碼為函數表示式,當整段程式碼執行時,會叫出函式物件並作回傳。

函數表示式並不會被hositing

1
2
3
4
5
6
7
anonymousGreet();//Uncaught TypeError: undefined is not a function.

var anonymousGreet = function(){
console.log();
}

anonymousGreet();//hi

在程式碼的創建階段,會在記憶體中創建變數anonymousGreet,預設值為undefined。
開始執行,第1行呼叫函式,卻發現anonymousGreet為undefined值,而非一個函式,因此就會被出錯。
因此正確寫法,應該是要在執行完第3行,assign一個函式給變數之後,才能如第7行呼叫。

將函數表示式作為傳入參數

在JavaScript中,函式為一級函式,也就是函式可以視為物件,因此也可以作為將函式表示式作為另外一個函式的傳入參數。

1
2
3
4
5
6
function log(a){
a();
}
log(function(){
console.log('hi');
})

JavaScript-函式就是物件

一級函式(First class function)

在JavaScript中,函式可以被視作一級值。
一級函式(First class function),指的是可以處理程式語言中對其他任何型別(物件、字串、布林值、純值)的方式來處理它;換句話說,我們也可以對一級函式做出任何其他型別能做得到的事,包含以下:

  • 將函式賦值給一個變數
  • 將函式當成傳入參數,成為另外一個函式的引數
  • 函式可以作為回傳值(在一個函式中,回傳另一個函式)

函式是特殊型態的物件

在JavaScript中的函式有物件的特色,是一種特殊型態的物件。

  • 函式中可以附屬有純值(primitive)(用name/value pair表示)
  • 函式中可以附屬有物件
  • 函式中可以附屬有其他函式

另外,函式包含兩個特別的屬性,一個是名稱(name),一個是執行的程式內容(code)。

  • 在name的部分,函式可以是匿名的(anonymous),也就是可以不一定要有name。
  • 而code的部分,也就是我們撰寫的程式碼內容。換句話說,我們撰寫的程式碼並非函式,而只是函式這個特殊物件中的一個屬性而已。而這個屬性可以透過”()”方法被呼叫、執行。

程式碼說明

首先宣告一個函式為greet,因為在JavaScript中的函式同等於物件,因此我們用”.”為greet創造language屬性的值。在其他程式語言中,這樣會出錯,但是在JavaScript中是可行的。

1
2
3
4
5
6
7
8
function greet(){
console.log('hi');
}

greet.language = 'english';

console.log(greet);
console.log(greet.language);

執行印出結果

以下解釋整個創建、執行的過程。


首先在創建階段會在全域記憶體中先創建一個全域物件,他的名稱是greet。而他的程式屬性,包含所寫的程式碼內容。而用”()”呼叫,即可執行該函式。

© 2020 Leah's Blog All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero