标签为“這些那些你可能不知道我不知道的Web技術細節”的页面如下
你可能不知道在JS世界裡的特殊物件
特殊物件清單
JavaScript是一個有著龐大使用族群的程式語言,但是因為其歷史淵源和不同考量等因素下,其中有不少令人萬丈摸不著頭緒的設計。自連class
都只作為保留字而無實際作用的時候,就已經有在接觸,在後續越了解越多,想想應該是能來分享一些,其中一些我知道的特殊物件。
undefined
null
this
super
NaN
Infinity
new
new.target
Object.prototype
- 先有
Function
還是先有Object
- 先有
Symbol
Symbol.for()
、Symbol.keyFor()
document.all
typeof document.all
arguments
hashbang
- HTML comment
'use strict'
globalThis
window
document
其中有一些並不是真正的物件,但都是一些執行環境下支援特殊寫法。或許有一些並沒有實際作用,但可能很多人並不知道,畢竟平常大概也沒有人會這樣寫吧!所以其實也就是一些JavaScript裡無關緊要的有趣小地方。
當然…當中有一部分也有可能成為你日後會踩入的陷阱(抗)。那麼就先來說說undefined
和null
吧!
undefined
undefined
是一個屬於undefined
的物件。(但可能不是唯一)
typeof undefined; // -> "undefined"
In all non-legacy browsers, undefined is a non-configurable, non-writable property. (Even when this is not the case, avoid overriding it.) – from MDN
儘管在現今主流的瀏覽器都是不可改變全域的undefined
變數:
這些那些你可能不知道我不知道的web技術細節-目錄與完賽感想
終於、終於30天啦啦啦!!!
原本其實是有猶豫今年要不要報名的,因為去年結果讓我有點失落憔悴…
然後這次也沒有組團,沒拖人下水成功。雖然也還是有發現一些認識的朋友今年也有參加,但還是一度在猶豫要不要報名,所以我幾乎是拖到最後一天才報名的哈哈。
工作的這兩年,遇到的事情這樣看來應該不算少。我有把一些我有興趣的議題記錄下來的習慣,雖然回去看也就一些很零碎的關鍵字,甚至有些一度回想不起來是什麼玩意兒。 這些東西我是有可能另外寫出來做記錄發表的,所以這次抱著反正之後也還是想寫,那就參加寫吧的想法報名,沒完賽就算了。
不過其實原本是有兩個參賽主題的,但最後只選擇了一個。一方便是另外一個對現在我來說不太好組織,另外就是時間安排上,我是很後來才真正決定要報名的。
而且在開賽前幾天確診Orz
隔離了7天
「這些、那些、你可能不知道、我不知道的Web技術細節」記錄了一些受到工作同僚、朋友聊天討論啟發,進一步研究原本我不知道或沒那麼清楚的Web技術細節。儘管足有接近30篇,但其實與我原本記下的關鍵字還是少了不少。像是WebAssembly、WebRTC、WebGL、Mono Repo、Micro Frontend等等。有些東西我有一些接觸,也有不少是還需要花費大量時間學習的。
而且這個系列,每一篇都至少花 超過兩個小時 構思撰寫。並且我實在不是很像破壞每一篇的獨立和完整性。所以有些篇數對於一次要閱讀完怕是會有些吃力,但基本每一篇都可以獨立參考閱讀,而不需要在意閱讀順序性。
目錄(依發表時間序)
你可能不知道的Web API--Web Locks
前言
Web Locks相關的API目前還是實驗性質的,這意味著未來可能有所變動,會與本片內容提及用法、作用有差異。雖然是實驗性質,但目前主流瀏覽器都已經支援。
使用方式
最基本用法是透過navigator.locks.request()
取得一把鎖,如果無法取得就必須等待直到能夠取得。如果取得了,就可以執行後續callback的動作。通常callback是一個異步函式,舉例來說寫法會如下:
navigator.locks.request('lock-1', async (lock) => {
console.log('get lock-1');
console.log('do something');
console.log('release lock-1');
});
callback的執行區域,被稱作是 關鍵區域 (Critical section)。
如果設計的恰當,關鍵區域只會有一個在執行。把上面再改寫一下:
var lock_name = 'lock-1';
navigator.locks.request(lock_name, (lock) => {
console.log(`A: get lock ${lock.name}`);
return new Promise(res => {
/// 10秒後釋放鎖
setTimeout(() => {
console.log(`A: release lock ${lock.name}`);
res(); // release lock
}, 10000 /*ms*/);
})
})
navigator.locks.request(lock_name, (lock) => {
console.log(`B: get lock ${lock.name}`);
return new Promise(res => {
/// 5秒後釋放鎖
setTimeout(() => {
console.log(`B: release lock ${lock.name}`);
res(); // release lock
}, 5000 /*ms*/);
})
})
A: get lock lock-1 A: release lock lock-1 B: get lock lock-1 B: release lock lock-1
在上面範例,有兩個程式區塊A和B需要使用到lock-1
這把鎖。A需要消耗10秒,並優先取得了鎖;B必須等待10秒後,才會開始執行。
可以透過將Promise
的resolve()
或reject()
傳遞出來,來決定什麼時候要釋放鎖:
你可能不知道的(Web)API--FinalizationRegistry(GC)
你可能不知道的Web API–FinalizationRegistry(GC)
FinalizationRegistry是和WeakMap、WeakSet、WeakRef在ES12一同進入到語言規範裡的兩個API。其實後面幾個更容易使用到,但我今天偏偏就是要來聊聊前者–FinalizationRegistry
。
在說說為什麼這個API有點雞肋可能沒什麼人知道之前,還是先介紹介紹這個API的用法。這個API的作用是在變數物件在被記憶體回收以前,可以註冊一些清理動作。比如可以建立物件:
var registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} is cleaned`);
})
var obj1 = { toString() { return "<Object obj1>"} };
registry
的callback function將快被記憶體回收的訊息打印出來。如果我們希望了解obj1
和obj2
何時被回收,可以用.register()
註冊:
registry.register(obj1, obj1.toString());
那麼當obj1
或obj2
不再可以被存取的時候,就有可能被記憶體回收,進而列印出訊息出來:
obj1 = null;
至於為什麼
obj1
不能使用delete
,可以參考「你可能都不瞭解的JS變數祕密 - 一文了解無宣告、var、let、const變數細節」
(突然發現這也是你可能不知道系列呢XD)
你可能不知道的Web API--postMessage
前言
postMessage()
是少數可以讓兩個不同頁面交換訊息的方式。如其名,傳遞訊息,postMessage()
接收一段文字訊息,將這個文字訊息傳遞給通知的對象。通知的對象可以監聽message
事件獲取訊息。
關於postMessage()
實際上現在瀏覽器網頁API上,存在的postMessage()
API不只一個。有window.postMessage()
、Worker.postMessage()
、BroadcastChannel.postMessage()
和Client.postMessage()
,它們有著類似的使用方式。除了不同頁面溝通外,對於建立的Web Worker執行緒也有相似方式傳遞訊息,除Worker.postMessage()
外,在Web Worker環境下還可以建立BroadcastChannel
物件,使用BroadcastChannel.postMessage()
方法。至於Client.postMessage()
是在Service Worker使用的,有在寫PWA(Progressive Web Application,漸進式網頁應用程式)才比較會用到。
本節主要討論的是最基本的window.postMessage()
。
基本用法
基本上的用法可以傳遞一個字串作為訊息發送出去,像是window.postMessage("msg")
。然後監聽message
事件。所以我們可以做一個簡單的例子。
現在建立兩個頁面index.html
作為主畫面,sub.html
作為用iframe
嵌入在主畫面的子畫面。
主畫面內容如下,除了一個發送訊息的文字話框外,還有一個接收訊息的文字畫框,以及一個發送訊息的按鈕。
<!------ index.html ------>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>[DEMO] postMessage (Main)</title>
</head>
<body>
<form>
<textarea cols="30" id="msg" name="" rows="10"></textarea>
<br/>
<textarea cols="30" id="rev" name="" rows="10" disabled></textarea>
<br/>
<button>send</button>
</form>
<hr/>
<iframe frameborder="0" src="sub.html"></iframe>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
相對來說子畫面就簡單一些,只有一個接受訊息的地方。它會將接受到的訊息原封不動的回傳回去。
你可能不知道的CSS Injection
前言
當你只是簡單地設置了一個CSP以後:
Content-Security-Policy: defalut-src 'self';
會發現誒~為什麼inline-script也不工作了?
<script type="text/javascript">console.log('Hello, World');</script>
要解決這個問題,同樣必須計算腳本雜湊值,然後設置新的內容安全政策:
Content-Security-Policy: defalut-src 'self';script-src 'sha256-2cq9aRSFdLqAC0FNx8cqcUjxA2Bmk5ZjlSvbIPQ1x/U=';
當然不止這種做法,還可以設置
nonce
等方式。
你可能不知道的內容安全策略(Content-Security-Policy, CSP)
前言
當我們知道了XSS,瞭解到對於外部資源的引用檢查是何等重要。那麼除了開發上需要注意意外,還可以怎麼做?
服務器設置CSP相關回應頭
CSP,全名Content Security Policy,也就是內容安全政策。這是為了告訴瀏覽器,這個頁面允許什麼行為,讓瀏覽器幫忙在檢查一次。與設個相關的主要有兩個Response Headers:Content-Security-Policy
和Content-Security-Policy-Report-Only
。
Content-Security-Policy
可以設定一些政策,當瀏覽器發現也面內容行為不符合這些政策的時候,就會被阻擋下來。
如果在一開始調整,有大量不確定受到影響的頁面,並不希望行為直接被阻擋下來,可以改先使用Content-Security-Policy-Report-Only
。當發現不符合政策的頁面內容時,先發送到後端的一個端點記錄下來。再所有被發現存在問題的頁面都已經修正後,再改成設置為Content-Security-Policy
。
繼續以前一個例子來用:A網站 http://a.127.0.0.1.nip.io:8000/index 有以下內容:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>CSP</title>
<link rel="stylesheet" href="http://b.127.0.0.1.nip.io:8000/xss.css" type="text/css" media="screen" />
</head>
<body>
<h1>Hello, World</h1>
</body>
<script type="text/javascript" src="http://b.127.0.0.1.nip.io:8000/xss.js"></script>
</html>
這次也真夠糟糕的,引用到兩份B網站 http://b.127.0.0.1.nip.io:8000 存在問題的內容–
/xss.css
和/xss.css
在部署的時候可以在伺服器回應的HTTP Response加入Header:
Content-Security-Policy: defalut-src 'self';
這次這兩個不安全的內容就會被瀏覽器阻擋下來。
如果確定外部資源內容是需要的話該怎麼辦?
你可能不知道的跨站腳本攻擊(Cross-Site Scripting,XSS)
前言
跨站腳本攻擊,英文Cross-Site Scripting,縮寫原本應該是CSS,但與階層樣式表–Cascading Style Sheets的縮寫相同,所以通常已X當做「交叉」的Cross,就變成是XSS。
在今天算是一個很嚴重的漏洞攻擊,因為有可能做到身份偽造,然後去進行資料竊取或破壞。但防禦跨站腳本攻擊,不單單只是前端開發工程師的責任,很大程度上也與服務如何部署有關係。
什麼是跨站腳本攻擊
跨站腳本攻擊,是在頁面上存在從其他來源引入的腳本,而這些腳本帶有惡意行為。要注意的是,腳本並不只是指JavaScript,HTML、CSS或其他資料內容也有可能是惡意注入的對象。
The malicious content sent to the web browser often takes the form of a segment of JavaScript, but may also include HTML, Flash, or any other type of code that the browser may execute.
惡意的JavaScript
比如說在A網站 http://a.127.0.0.1.nip.io:8000/index.html 有以下內容:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>XSS</title>
</head>
<body>
<h1>Hello, World</h1>
</body>
<script type="text/javascript" src="http://b.127.0.0.1.nip.io:8000/xss.js"></script>
</html>
在這個頁面中,使用了另一個網站B http://b.127.0.0.1.nip.io:8000 的JavaScript。但是其實xss.js
的檔案並不是自己可以掌控的,它的內容可能是:
你可能不知道的HTTP Header--If-Match和該怎麼設計Web API
前言
問個 假如我今天有個表單在前端要送,然後我想要先檢查這個表單有沒有被其他人更新
這是在一次和朋友的討論中有人提出來的問題。
恩…這是很典型資料競爭的問題,但並不是多個執行緒存取一份資料,而是多個瀏覽器客戶端(Browser Client)存取同份伺服器資源。
通常,我們在討論RESTful - 一種當下很常見的Web API設計方式的時候,只會提到建立(Create / POST)、讀取(Read / GET)、更新(Update / PUT)、補充(PATCH)和刪除(DELETE),也就是一般的CRUD。甚少會在深度討論一些細節,就如開頭提到的問題。並且通常在瀏覽網站,也很少去注意各個網站的API設計方式。
那麼這個問題要怎麼解決呢?
不管怎麼說,伺服器上的資料正確性最終需要由伺服器處理保護。 可以簡單一些在資料上添加版本(Version)或最後修改時間(Last Update time)的欄位。當收到更新或刪除訊息,該欄位應該保持一致。不過這就會出現一個很奇妙的現象:
> GET /data1
< 200 OK
< Content-Type: application/json
<
< {"name": "Bob", "age": 18, "version": 1}
當我們有一個資料data1
其版本為1
,如果我們需要更新的話,下面的訊息代表什麼意思?
> PUT /data1
> Content-Type: application/json
>
> {"name": "Alice", "age": 18, "version": 1}
< 204 No Content
這是一個更新,版本是不是應該跳到2
?但是按照前述這個版本應該要與記錄一致才會被修改,但是這樣似乎又與PUT
的意圖衝突?
或許有一些設計是檢查更新版本-1
是不是與當前記錄版本
相同作為檢查判斷條件。但是想想,其實版本號的決定,似乎不該由Client傳進的資料決定,而應該由後端資料管理行為決定。
那麼來介紹另一種設計方式,其實HTTP一般性規範都幫你準備好了,只是在我經驗觀察上,似乎並沒有那麼易用(也可能是因為了解的人真的不多),並不經常看到這樣的設計。這可能會包含一些你可能不知道的HTTP狀態碼(HTTP Status Code)和HTTP Headers。
你可能不知道的JS自動型別轉換
前言
如果要針對一個 物件陣列 取最大值該怎麼做?
一天,一位同事這麼問到。
首先需要先知道的是一般在JavaScript物件是無法比較大小的,所以這句話的意思是將物件特定屬性作為比較參考值,或是將物件數個屬性計算成一個可以比較的值後作為參考值,再進行比較尋找最大值。
起初,針對這個問題我第一想到的就是三種方式:
- 使用
Array.prototype.reduce()
- 使用
Array.prototype.sort()
- 使用
Math.max()
最後一個你可能會很好奇:物件無法比較,並且Math.max()
又不像Python的max()
可以輸入key
函式,這樣可以比較嗎?
沒錯,這個方式是存在問題的,但原因並不是因為「物件無法比較」,而是結果型別的問題,這個問題之後會提到。並且這也引發了今天的議題–「JS自動型別轉換」。
物件陣列怎麼取最大值?
這並不是今天主題的重點,也就不賣關子了,直接給出做法。
同樣拿Person類別為例子:
class Person {
name = "";
#birthday = new Date(); // 私有屬性
get birthday() { return this.#birthday }; // 調整處
addr = ""
get age() {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name, birthday, addr = "") {
this.name = name;
this.#birthday = birthday; // 調整處
this.addr = addr;
}
hello() {
console.log(`Hello, ${this.name}.`);
}
}
現在有10個年幼不依的人:
var people = [
"Alice",
"Bob",
"Candy",
"Danel",
"Frank",
"Grant",
"Harry",
"Iris",
"Joe",
"Kevin"];
people = people.map(name => new Person(name, new Date(Math.floor(Math.random() * 100) + 1990, 1), ""))
現在如果希望找到年紀最大的,這裡提供幾種方式:
你可能不知道的JS物件私有屬性
前言
前幾年我曾經寫過「7天搞懂JS進階議題」系列文章,你可以在我的網站或是CoderBridge閱讀。其中在番外篇提到過「隱私成員」,在當時因為JavaScript並沒有隱私屬性的設計,所以想實現,當時使用了閉包和屬性描述器來處理。
時過境遷,在我寫完發表沒多久,ES11(2020)也正式推出了,其中就有關於私有屬性和私有方法的設計。根據Can I Use使用支援程度已經超過九成,也就是在今天主要瀏覽器除了一些舊版本和特別的瀏覽器外應該多數也都支援了。
原先JavaScript設計的問題
class Person {
name = "";
birthday = new Date();
addr = ""
get age() {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name, birthday, addr = "") {
this.name = name;
this.birthday = birthday;
this.addr = addr;
}
hello() {
console.log(`Hello, ${this.name}.`);
}
}
var bob = new Person(/* name = */ "Bob",
/* birthday = */ new Date(2004/* year */,
1 /* mouth */,
1 /* day */),
/* addr = */ "臺灣")
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
bob.hello();
bob.birthday = new Date(); // 變更生日
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年0歲
如果設計了一個類別Person
有名字(name
)、生日(birthday
)、地址(addr
)等屬性,從嘗試性來說生日在出生以後就不能夠變了,但是上面程式碼片段當我們建立一個實例物件bob
後,依然可以變更生日。
你可能不知道的WebAuthN(FIDO)
名詞解釋
AuthN 和 AuthZ 分別是 Authentication 和 Authorization 的簡寫,也就是驗證和授權。
不光兩個英文字像…簡寫形式一樣容易讓人混淆🐷
不過比起全名或是 A12n 或 A11n ,這樣好分辨多了Orz…
那麼 WebAuthN 就是 Web 和 AuthN 的結合,也就是在 Web 上的身份驗證。這裡特別指的是由FIDO聯盟推出的FIDO2的其中一部分。特徵就是讓使用者在瀏覽器瀏覽網頁時,可以利用辨識、臉部辨識等方快速登入。至於FIDO是Fast IDentity Online的縮寫,也就是「快速線上身份識別」,也就不難理解聯盟成立的目的為何。
身份驗證的方式
在之前系列「用Keycloak學習身份驗證與授權」–淺談身份驗證與授權(1)和再談身份驗證與授權中,將整個身份驗證、授權到取得資源處理業務邏輯分成幾個部分看。
這次主要談的是WebAuthN,也就是身份驗證這一塊。能夠證明身份的方式通常又分成這麼幾種:
- 只有你知道。像是密碼、一次性密碼(OTP)、簡訊驗證碼等等。
- 只有你持有。像是汽機車鑰匙、硬體金鑰、手機手錶(手機或智慧手錶在附近自動解鎖)。
- 只有你天生是。指紋、虹膜、DNA。
你可能不知道cookie是怎被保存、保存在哪裡?
前言
再來是cookie最後,也是我認為最有意思的一部分–「cookie是怎被保存、保存在哪裡」 。
服務後端
對於開發後端服務的軟體開發人員,自己應當知道cookie存在哪裡,如何才可以將HTTP Request的狀態保留對應下來。而一般前端人員也不需要很清楚的知道cookie儲存在哪裡,只要認為儲存在瀏覽器內或記憶體內就好。
瀏覽器外存在哪裡
但是重開機後有一些cookie也存在,這表示必定有一個檔案儲存地方儲存這cookie。
以Google Chrome在Windows平臺為例,Cookie可能儲存在%LocalAppData%\Google\Chrome\User Data\Default\cookies
或%LocalAppData%\Google\Chrome\User Data\Default\Network\cookies
,這要看是什麼版本的若有在更新目前應該是後者。
這是以SQLite作為檔案型資料庫儲存的,因此可以使用對應的工具打開來看看:
你可能不知道cookie有些只能走特定道路(HTTP)
回信
當瀏覽器收到並儲存cookies後,在下一次的request就會將cookies跟著帶著送出去,回到Server。對於無狀態的HTTP,就可以使用這樣的方式讓Server回憶這個Request之前是誰,做到保留狀態。
同樣地接者調整前篇的程式碼片段。這次要將Server收到的Cookies直接作為Response的內容返回。先來添加需要的package:
from fastapi.requests import Request
然後添加一個endpoint–GET /cookies
,將Server收到的Cookies直接作為Response的內容返回:
@app.get('/cookies')
def cookies(request: Request):
return request.cookies
現在瀏覽 http://127.0.0.1.nip.io:8000/cookies 就可以收到傳給Server的Cookies作為JSON返回。
AJax with credition
你可能不知道cookie可以寄城市還可以分路段
預設收件區
Set-Cookie
就像寫信,除了內容當然還有收件區號。如果並沒有給Domain,就是當前的Domain。比如當瀏覽 http://localhost:8000/index1.html ,其記錄的Domain就是localhost
。
這個Domain就像是在寫信時填寫的區號,比如「桃園市楊梅區」的區號就是326
。
填寫收件區號
現在可以複製你可能不知道cookie是怎麼被製造出來的裡的DEMO程式碼內容,在此基礎上繼續修改,填寫收件區碼。
你可能不知道cookie是怎麼被製造出來的
前言
你是タコたち嗎?你喜歡吃cookies嗎?那麼你知道cookies是怎麼被製造出來的嗎?
cookie是怎麼被製造出來的
cookies是在瀏覽器儲存的小小資料片段,通常來說當瀏覽器發出request時,有可能同時將cookie發送出去。利用這個特性,可以將通常來說無狀態的HTTP保有記憶,做到登入功能、追蹤行爲等等。
雖然我們可以透過瀏覽器開發工具新增cookie。
你可能不知道的Function.prototype.bind()
前言
Function有三種用法,除了一般呼叫方式外,還可以使用Function.prototype.call()
或Function.prototype.apply()
方法。此外,Function還有一個很常見,偶爾會與後兩個用法混淆的方法–Function.prototype.bind()
。沒錯,這節就是要來說說Function.prototype.bind()
和另外兩者的差異,以及常見用法和你可能不知道的Function.prototype.bind()
。
<Fn>.call()
/<Fn>.apply()
和 <Fn>.bind()
的差異
由於過去其實我是寫過bind()
的相關內容的。所以我個人並不曾將三者搞混,蠻能區分用法上的不同的。不過在偶然幾次討論程式碼應該如何寫的過程中,發現偶爾會有人弄不清楚何時應該使用bind()
?何時使用其他兩者?
回頭看我過去所寫的,也蜻蜓點水的點到過call()
和apply()
。它們三者的參數形式確實有些像,特別是bind()
和call()
都接受一個thisArg
參數和多個參數展開。
所以Function.prototype.bind()
有甚麼不同之處嗎?
Function.prototype.call()
和Function.prototype.apply()
與一般函式呼叫寫在同一節裡,他們三著共同點是「會真的執行函示內容」。與他們不同的是Function.prototype.bind()
並不會真的執行函式,它會返回一個新的函式。
function helloWorld() {
console.log(`Hello World`)
}
helloWorld(); // 會印出 Hello World
helloWorld.call(); // 會印出 Hello World
helloWorld.apply(); // 會印出 Hello World
helloWorld.bind(); // 不會印出 Hello World。返回一個函式物件
新的函式物件與原本的可能沒有什麼差異:
var newFn1 = helloWorld.bind();
newFn1(); // 會印出 Hello World
雖然上面程式碼很像是直接賦值給變數,但還是有些差異。
var newFn2 = helloWorld;
newFn2 === helloWorld; // true。直接賦值的話是同一個函式物件
newFn1 === helloWorld; // false。使用bind()會產生一個新的函式物件,儘管它們用起來可能很像,但依然不同
直接賦值的話是同一個函式物件;相對來說,使用bind()
會產生一個新的函式物件。儘管它們用起來可能很像,但依然不同。
常見用法
在JavaScript裡面this
是一個特別的存在。它經常會有隱含綁定和隱含遺失的狀況。
為什麼你需要知道Function的三種用法
前言
在設計函式與呼叫函式前,或許得認識到一些限制,這些限制有可能造成需要使用不同的設計方式或呼叫方式。就來談一下一些在JavaScript語言裡的一些限制吧!
安全整數範圍
JavaScript裡關於「整數」是有範圍限制在的,按照規範這個值的範圍是(±2**53)
內,也就是-9007199254740991~9007199254740992
。這個值你可以透過Number.MIN_SAFE_INTEGER
和Number.MAX_SAFE_INTEGER
取得。
BigInt
在ES11後多加了一個基本類別BigInt
,儘管這個類型的使用方式和Number
並不相容1。但是在過去寫過的7天搞懂js進階議題中曾經使用過。如果你有需要超過-9007199254740991~9007199254740992
範圍的整數,可以考慮使用BigInt
。
陣列長度
Array
會需要留意:屬性.length
的最大值爲2**32-1
也就是4294967295
。
這意味著以下一些操作是會出問題的
var arr = Array(4294967296); // 超出最大範圍
{
let arr = Array(4294967295);
arr.push(0); // 超出最大範圍
}
此外,-1
的索引值並不是像Python會得到最後一個元素2。實際上經過以下操作:
var arr = [1,2,3,4];
arr[-1] = 5;
console.log(arr);
最後arr
的結果應該會像是:
[-1: 5, 1, 2, 3, 4]
另外.length
也不會算上-1
的索引值。
你可能不知道Function的三種用法
前言
原預計標題「你可能不知道的Math.max()
三種用法」。因為這是在調整Math.max()
時引發的話題。在這之後過了幾周,有另外一個同事詢問Function.prototype.call()
、Function.prototype.apply()
的差異。
因此,接下來將來看看「你可能不知道Function的三種用法」。除了一般的呼叫外,還有<Fn>.call()
和<Fn>.apply()
。試想已經有參數陣列args
:
var args = Array(15).fill(0);
args.forEach((arg, i, arr) => arr[i] = Math.floor(Math.random()*50));
如果要將args
傳遞給函式Math.max()
執行,通常可以這麼做:
Math.max(...args);
這相當於:
Math.max.call(null, ...args);
此外你還可以這麼做:
Math.max.apply(null, args);
這三種作法都可以得到相同結果:
Math.max(...args) === Math.max.call(null, ...args); //true
Math.max(...args) === Math.max.apply(null, args); // true
除此之外,因為Math.max()
的處理特性,恰好可以使用函式型開發方式中reduce
的概念,也確實可以使用args.reduce()
去得到與上面相同的結果:
Math.max(...args) === args.reduce((m, c) => Math.max(m, c))
接下來也會談到一些Array.prototype.reduce()
的事情。
你可能不知道Array.prototype.forEach()沒跟你說的事情
Array.prototype.forEach()
的用法
自知道Array
有forEach
的方法後,我自己是還蠻愛用的。
var names = ["World", "Bob", "Alice"]
names.forEach(name => console.log(`Hello, ${name}`))
並且與其他多數Array
支援的callback方法一樣,有多個很有效的參數:
var names = ["World", "Bob", "Alice"]
names.forEach((name, idx, arr) => {
console.log(`Hello, ${name}`)
arr[idx] += "."
})
console.log(names); // ["World.", "Bob.", "Alice."]
我們甚至可以用而外的thisArg
來處理某些事情:
var obj = {
"World": undefined,
"Bob": undefined
};
function checkHello(name) {
if (name in this)
return void (this[name] = "Yes");
return void (this[name] = "No");
}
var names = ["World", "Bob", "Alice"];
names.forEach(checkHello, obj);
console.table(obj);
結果:
name | result |
---|---|
World | Yes |
Bob | Yes |
Alice | No |
關於性能
在通常情況下,不會由瀏覽器處理大量的資料。通常而言forEach()
的需要時間基本沒有什麼差別:
你可能不知道的Call Stack
前言
Call Stack,中文「呼叫堆疊」,是一個很重要的概念。這並不是Web相關技術中特有的,不過為了解釋後續的內容,我決定安插一節說一下Call Stack的概念。
Call Stack
Stack 是一個先進後出(First-In-Last-Out / FILO)的資料結構。就像一本本書疊起來,然後只能一本本從最上面開始拿下來。
Call Stack 就是每次函式呼叫,都會將函數的環境狀態保存進Stack,函數的環境狀態通常叫做「Call Frame」,而儲存Call Frame的Stack,就是Call Stack。
最明顯的例子就是遞歸函式,比如:
function sum(accum, end) {
if (end === 0)
return accum;
return sum(accum + end, end -1);
}
sum(0, 5);
sum()
是將 0 到 end
之間的整數累加起來,且end
必須是大於等於0的整數。
當呼叫sum(0, 5)
的時候,Call Stack便會儲存這筆資訊
你可能不知道的即時更新方案:multipart/x-mixed-replace
multipart/x-mixed-replace
除了Polling、Long Polling、Server Send Event(SSE)和WebSocket以外,還可以透過multipart/x-mixed-replace
來更新資料。
multipart/x-mixed-replace
和Server Send Event(SSE)一樣,只能夠由Server單向傳送資料給瀏覽器。
不同的是它可能不能使用JavaScript處理更新的資料,但現在主流瀏覽器多數還是支援其中部分特性,這使得從前端部分實現非常簡單。
不再支持 XMLHttpRequest 中的 multipart 属性和 multipart/x-mixed-replace 响应。这是一个 Gecko 独有的特性,从来没被标准化过。你可以使用Server-Sent Events, Web Sockets (en-US)或者在 progress 事件中查看 responseText 属性的变化来实现同样的效果。1
Lab
資源
這次實驗會透過不斷讀取不同圖片,讓瀏覽器上不斷更新圖片內容。首先是圖片資源:
這些圖片資源名稱是: 1.jpg
、2.jpg
、3.jpg
。後續會輪流讀取回傳給瀏覽器。
前端畫面
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>即時更新內容 - multipart/x-mixed-replace</title>
</head>
<body>
<img alt="" src="/fake_video"/>
</body>
</html>
基本上這次連JavaScript都不需要寫,只要載入一張圖片資源即可。
你可能不知道的即時更新方案:WebSocket
WebSocket
WebSocket進一步解決了Long Polling會遇到的兩個問題:
- 取得Response後,需要在建立一次Reqeust。
- 僅能夠單向傳輸更新資訊。
不過WebSocket並不是超文本傳輸協定(HyperText Transfer Protocol,HTTP),但確實由HTTP開始的。因此首先是在瀏覽器發起Request之後,要進行協議的切換。Server會回傳切換資訊。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: gx+UXBfX3qJcxachxkN/n8/3+WQ=
Sec-WebSocket-Extensions: permessage-deflate
Date: Sun, 04 Sep 2022 10:36:52 GMT
在這之後就可以進行雙向傳輸,當然以可以用於更新畫面資料。
優點
- 雙向傳輸
- 連線可以重複使用
缺點
實現複雜。不是所有瀏覽器都支援,不過現在主流瀏覽器基本支援。對於伺服器也有一定要求,在我經驗上許多免費服務器是無法使用相關技術的。
Lab
你可能不知道的即時更新方案:Server Send Event
Server Send Event
Server Send Event(SSE)解決了Long Polling會需要建立多次Request的問題。相比起Long Polling「取得Response後,需要在建立一次Reqeust」。Server Send Event在同一次HTTP連線中,由Server送出多次更新資料。
優點
連線可重複使用。
相比起Long Polling「取得Response後,需要在建立一次Reqeust」。Server Send Event在同一次HTTP連線中,由Server送出多次更新資料。
缺點
僅能夠由Server傳送訊息到瀏覽器的單向傳輸。
Lab
前端頁面
<!-- www-data/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>即時更新內容 - Sever Send Event</title>
</head>
<body>
<h1 id="content"></h1>
</body>
<script defer type="module">
const DEFAULT_TIMEOUT = 30000 /*ms*/;
const HEATBEAT_INTVAL = 5000 /*ms*/;
const contentEl = document.querySelector('#content');
let evtSource = new EventSource("/connect");
/* recive default message
evtSource.addEventListener('message', async (event) => {
console.log(`recive message: ${event.data}`);
})
*/
/* recive special event - update */
evtSource.addEventListener('update', (event) => {
contentEl.innerText = event.data;
});
</script>
</html>
前端頁面實現也算是簡單的,透過EventSource()
建立Server Send Event來源的連線。透過監聽message
或<event>
來取得更新資訊。
後端API
你可能不知道的即時更新方案:Long Polling
Long Polling
Polling 有兩個明顯的缺點:
- 就算資料沒有更新,也會有一次 Request / Response 的來回。
- 資料有了更新,也需要等待下一次的Request才會知道。
Long Polling解決了這兩個問題。
雖然行為概觀上與Polling一樣,是一次Request後得到Response,才再發出一個Request。但是Reponse的回傳可能會拉的老長,過了許久才回應,像彗星(Comet)一樣。也確實有一種方式就叫做彗星(Comet),Long Polling是他的變種1。
在Long Polling模式下,有一個長期連線,這個連線在資料一更新時,變會回傳Response,並結束此輪連線,然後再發起一次長連線請求,以做到即時更新的效果。
有一段時間在Facebook、Plurk可以見得此種方式。甚至現在還在Facebook、Plurk還是可以見得一些Request長時間沒有Response,不過我無法確定是否同為Long Polling。
下圖是Plurk的部份網路請求節圖。其中可以看到一個/connect
相關的請求,並過一段時間後才返回Response,隨後又建立一比連線:
你可能不知道的即時更新方案:Polling
靜態網頁 & 動態網頁
全球資訊網路(World Wide Web, WWW)最早用於學術研究機構,用於分享研究報告成果。起初常見型態為:資訊分享者自行建立Web伺服器,提供HTML靜態頁面,讓資訊受者透過瀏覽器取得資訊。
英國科學家提姆·柏內茲-李於1989年發明了全球資訊網。1990年他在瑞士CERN的工作期間編寫了第一個網頁瀏覽器。網頁瀏覽器於1991年1月向其他研究機構發行,並於同年8月向公眾開放。1
「資訊分享者自行建立Web伺服器,提供HTML靜態頁面,讓資訊受者透過瀏覽器取得資訊」這從現在來看,可能分成兩個不同維度的類型:
- Web 1.0: 資訊分享者自行建立Web伺服器。用戶只能單向被動的接受由權威內容服務提供商提供的內容,用戶大部分為內容消費者,而網站則由內容驅動2
- 靜態內容網頁。網頁內容並不會因為內容接收者的不同、時間地區的不同而有所不同。
隨後出現部落格平台、線上論壇,出現可以提供資訊內容的網站使用者。在此,網站內容不再簡單以HTMl方式儲存,更多的內容儲存於資料庫,由使用者提供,動態的提供給瀏覽器。這是 動態網頁 和 Web 2.0 。
此外伴隨者瀏覽器腳本語言與API的逐漸統一,確立了 ECMAScript / JavaScript 在瀏覽器的地位,結束了過往百家爭鳴的情況(不過現在還有一些原生支援特殊腳本語言的瀏覽器存在,但JavaScript已成主流)。 似動非動,吸引人眼球由JS、CSS建立動畫效果的網頁內容開始玲瑯滿目地出現。
1996年11月,網景正式向ECMA(歐洲電腦製造商協會)提交語言標準。1997年6月,ECMA以JavaScript語言為基礎制定了ECMAScript標準規範ECMA-262。JavaScript成為了ECMAScript最著名的實現之一。除此之外,ActionScript和JScript也都是ECMAScript規範的實作語言。3
其中也還有另一個令人興奮、今天廣泛使用的技術出現: Ajax(Asynchronous JavaScript and XML)。
在現今,Ajax技術主要以XMLHttpRequest
和fetch
兩個API呈現。也可能隱藏在jQuery4、Axios等比較高層次的套件函式庫之下。
這也使得由瀏覽器處理、在前端渲染生成不同內容的畫面成為可能。儘管這不是當前唯一的技術方式,但有許多別具特色的功能、前後分離的網站都依賴者這項能力。
接著就讓我們來探索幾個取得更新資訊的模式吧!
你可能不知道URL的路徑編碼Percent-encoding
前言
語言文字事件有趣的事情,英文由26個字母反覆組合,描述各種現象。中文永字八筆,構築千萬象形文字。在電腦的世界裡,一切的一切都是由無數個0和1所表示,你所從螢幕見到的任何文字、圖片、影片都是。在文字的表示法中,有一個最早誕生的編碼方式–ASCII Code。從某種角度來說,它也是在電腦世界中最安全的表示方式。
我們介紹過如何安全的儲存JavaScript程式碼而不受到編碼影響。如何在有限的字元中,在網頁HTML表達多國語言的文字。分享過DNS與電腦是如何認識IDN。這次,要來說說URL裡關於Path字段的編碼方式。
URL格式
當我們瀏覽https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third
1,實際上可以將這個URL拆分成好幾個部分來看。
+-------------------+---------------------+
| Part | Data |
+-------------------+---------------------+
| Scheme | https |
| User | bob |
| Password | bobby |
| Host | www.lunatech.com |
| Port | 8080 |
| Path | /file;p=1 |
| Path parameter | p=1 |
| Query | q=2 |
| Fragment | third |
+-------------------+---------------------+
https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third
\___/ \_/ \___/ \______________/ \__/\_______/ \_/ \___/
| | | | | | \_/ | |
Scheme User Password Host Port Path | | Fragment
\_____________________________/ | Query
| Path parameter
Authority
你可能不知道隱藏在Domain裡的編碼punycode
前言
2017年我有幸取得一年免費的「.台灣」域名。自此我也花了一點時間了解什麼是國際化域名(Internationalized Domain Name,IDN)。
在今天你可能也看過不少並非由英文、數字組成的域名,如果是中文或是你的母語,或許會感到一種貼切感,有些又比羅馬拼音更好記。但你知道的嗎?這可能是隱藏在網址列的假象。
國際化域名(Internationalized Domain Name,IDN)
國際化域名(英語:Internationalized Domain Name,縮寫:IDN)又稱特殊字元域名,是指部分或完全使用特殊的文字或非拉丁字母組成的網際網路域名1
早期的頂級域名只有.com
、.gov
、.org
、.net
、.int
、.edu
、.mil
。
這些頂級域名最初指的都是美國。後來才開始有了國家級域名如.tw
。加上作用後同樣視為頂級域名如.com.tw
。
隨後也出現了.jobs
、.info
、.tv
、.cc
等等為後綴的域名。
域名的功用性,個人感覺某種程度在降低。有些域名的申請越來越容易。
在「.台灣」後綴前,似乎就已經有非英文組成的域名了。譬如:https://中文.tw/ 。
現在,除了.tw
的後綴,還可以申請.台灣
(繁體)、.台湾
(簡體)的後綴。譬如: https://中文.台灣/ 。
國際化域名(IDN)可以提供一個好記、在地化的名字,有時候或許可以提升品牌形象。
國際化域名編碼(英語:Punycode)
不過,早期的DNS其實並不認得這麼多字。實際上這部分瀏覽器事先做了轉換了,像是 https://中文.tw/ ,在我使用的Mozilla Firefox Browser還可以短暫看到https://xn--fiq228c.tw/
。是的這就是 https://中文.tw/ 編碼後的結果。透過在瀏覽器網址列輸入xn--fiq228c.tw
,同樣可以進入 https://中文.tw/ 。
你可能不知道HTML Code Number
我們都知道的 <
和 >
如果需要在HTML裡面讓瀏覽器顯示<
和>
,可不能直接寫。你應該要這樣寫:
<p>
example: <input/>
</p>
來呈現內容
example: <input/>
其中lt
表示: LESS-THAN ;而gt
表示:GREATER-THAN。這是他們的符號名稱,同時還有號碼名稱。
可以使用10進制或16進制來表示這兩個符號,因此還可以寫成:
<p>
example: <input/>
</p>
<p>
example: <input/>
</p>
關於HTML編碼這件事情
與其他程式語言的原始碼儲存方式一樣,HTML文件是以純文字方式存在的。 這也就存在一個問題:如果不告訴電腦該用什麼編碼方式解讀它,就可能解讀成亂碼。
比如以下HTML內容使用Big5儲存:
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8"/>
<title>你好,世界</title>
</head>
<body>
<h1>你好,世界</h1>
<p>怎麼跟得上臺灣</p>
</body>
</html>
你可能得到這樣的結果:
你可能不知道在JavaScript裡的萬國碼
那時、天下人的口音言語、都是一樣。
他們往東邊遷移的時候、在示拿地遇見一片平原、就住在那裏。
他們彼此商量說、來罷、我們要作甎、把甎燒透了。他們就拿甎當石頭、又拿石漆當灰泥。
他們說、來罷、我們要建造一座城、和一座塔、塔頂通天、爲要傳揚我們的名、免得我們分散在全地上。
耶和華降臨要看看世人所建造的城和塔。
耶和華說、看哪、他們成爲一樣的人民、都是一樣的言語、如今旣作起這事來、以後他們所要作的事、就沒有不成就的了。
我們下去、在那裏變亂他們的口音、使他們的言語、彼此不通。
於是耶和華使他們從那裏分散在全地上.他們就停工、不造那城了。
因爲耶和華在那裏變亂天下人的言語、使衆人分散在全地上、所以那城名叫巴別。〈就是變亂的意思〉
– 創世記11
從一封亂碼郵件開始說起
自己個人很少書寫電子郵件,收到的郵件也多數是某種活動、告知事項或廣告類信件。 有些時候會收到一些需要回信或是主動發信件的情況。絕大多數文字語言上都不是什麼問題。但就是偶爾會有一兩封信件在眼前呈現的是我看不懂得符號。
最近,我就收到過一封信件…是亂碼。
會呈現亂碼的原因,有很大程度是顯示軟體錯誤的解讀了信件內容。這種情況可能可以透過取得原始信件資料,在使用其他編輯器和解碼方式打開顯示。
以信件來說,常見的分成兩種類型:
- 純文字
- HTML文件
特別是HTML文件,會有一段以<meta/>
標記內容使用的編碼方式(charset
)。
你可能不知道void也是一個運算子
void也是一個運算子
你可能不知道void
也是一個ECMAScript的保留關鍵字 (reserved keyword),也可能不知道它也是一個 運算子。
誒黑~!
因爲這張卡的效果就是:
什麼都不給
你可以像是一元運算子那樣使用它:
var a = 1;
a = -a; // -1
a = !a; // false
a = typeof a; // boolean
a = void a; // undefined
也可以將表達式用括號包起來,像是函式呼叫:
var a = 100
a = void(a) // undefined
實際上這個行爲更像是先針對表達式
(a)
求值,在使用運算子void
。過程中不存在一般意義上的函式呼叫。
這些那些你可能不知道我不知道的Web技術細節系列(前言)
前言
「這些、那些你可能不知道、我不知道的Web技術細節」,這是個我在這短短兩年實際工作上,遇到的一些原先並不清楚,甚至不知道的Web技術。
有一些或許是新的技術規範、有一些只是很冷門的技術知識、也有一些與實際實現有關。
你可能已經知道,也可能不清楚。邀請你與我一起探索這些冷門舊技術、鮮爲人知的新技術的技術細節喔~